0.3 - дебаггер и отладка приложения

Лабораторная работа 0.3 для студентов курса “Основы программирования” 1 курса кафедры ИУ5 МГТУ им Н.Э. Баумана.

Содержание

  1. Цель работы
  2. Введение
  3. Установка дебаггера
  4. Настройка дебаггера под VS Code
  5. Работа с дебаггером

Цель работы

Научиться осуществлять отладку приложения с использованием дебаггера.

Введение

В данной лабораторной работе будет рассказано как:

Установка дебаггера

Проверьте, что у вас установлен дебаггер GDB или LLDB. Для современных MacBook (с чипами M1 и новее) на текущий момент доступен только LLDB.

gdb --version

или

lldb --version

Прочитайте вывод в консоли. Если данная команда не найдена, то установите новейший доступный gdb или lldb через предпочитаемый пакетный менеджер (например, apt для Ubuntu, brew для MacOS).

Настройка дебаггера под VS Code

Откройте нужный проект в VS Code. В корне проекта откройте директорию .vscode (создайте ее, если она отсутствует).

Например, для проекта Debugger-Example в одноименной директории иерархия директорий будет выглядеть так:

Debugger-Example/
└── .vscode

Где Debugger-Example - это корень проекта.

В директории .vscode создайте файл launch.json.

Debugger-Example/
└── .vscode
    └── launch.json

Скопируйте в этот файл следующее содержимое. Это конфиг для запуска приложения под gdb. Внимательно прочитайте сам конфиг и описание к нему (ниже).

{
    "configurations": [
        {
            "name": "***",
            "type": "cppdbg",
            "request": "launch",
            "program": "***",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${fileDirname}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "gdb",
            "setupCommands": [
                {
                    "description": "Enable pretty-printing for gdb",
                    "text": "-enable-pretty-printing",
                    "ignoreFailures": true
                },
                {
                    "description": "Set Disassembly Flavor to Intel",
                    "text": "-gdb-set disassembly-flavor intel",
                    "ignoreFailures": true
                }
            ]
        }
    ]
}

Соответствующий конфиг для lldb:

{
    "configurations": [
        {
            "name": "***",
            "type": "cppdbg",
            "request": "launch",
            "program": "***",
            "args": [],
            "stopAtEntry": false,
            "cwd": "${fileDirname}",
            "environment": [],
            "externalConsole": false,
            "MIMode": "lldb"
        }
    ]
}

Сохраните конфиг. После этого во вкладке Run and Debug в списке конфигураций (сверху) появится новая конфигурация с тем именем, которое вы указали до этого.

enter image description here

Работа с дебаггером

Дебаггер позволяет осуществлять отладку приложения, используя следующие инструменты:

Чтобы дебаггер мог получить эту информацию из приложения, при компиляции следует подать флаг -g. Этот флаг встроит нужную информацию (известную как “дебажные символы”) прямо в исполняемый файл.

Например, следующая команда создаст исполняемый файл app с дебажными символами, используя main.cpp в качестве файла с кодом.

g++ -std=c++20 -g -Wall -Werror -Wextra -Wpedantic main.cpp -o app

Какие-то флаги можно опускать, если в них нет необходимости. В частности, не стоит постоянно компилировать приложение с флагом -g, т.к. это влияет на производительность самого приложения.

Пример

В проекте, о котором говорилось выше, создадим файл main.cpp и вставим туда следующий код:

#include <iostream>

struct S {
    int a = 0;
    double b = 0.;
};

void foo(int a, int* b, S& c) {
    std::cout << a << std::endl;

    int d = 10;
    std::cout << d << std::endl;

    if (b) {
        *b = 5;
    }

    c.a = 11;
    c.b = 15.25;
}

int main(int, char**) {
    int a = 17;
    S s{};

    foo(15, &a, s);

    std::cout << a << std::endl;
    std::cout << s.a << ' ' << s.b << std::endl;

    return 0;
}

С помощью следующей команды скомпилируем исполняемый файл a.out, добавив туда дебажные символы:

g++ -g main.cpp

Добавим точки останова на 23 и 28 строках. Для этого надо кликнуть левее номера строчки, чтобы появилась красная точка:

enter image description here

Теперь запустим приложение под дебаггером. Для этого надо во вкладке Run and Debug выбрать нужную конфигурацию, которую создали ранее, и нажать на зеленую стрелочку. Или нажать на кнопку f5 (по дефолту), если выбрана нужная конфигурация:

enter image description here

Приложение должно остановить свое исполнение на строке 23. Выглядеть это должно примерно следующим образом:

enter image description here

На скриншоте слева сверху в разделе Variables перечислены еще не созданные (но видимые дебаггеру, т.к. они находятся на стеке) локальные переменные. Слева снизу в разделе Call Stack находится стек вызовов, в котором содержится информация о том, какая функция сейчас исполняется и из каких функций был произведен вызов. Еще ниже находится список точек останова Breakpoints.

Сверху над кодом находится панель управления, с помощью которой можно управлять потоком исполнения приложения (слева направо):

В данном случае остановка произошла на строке 23, причем перед ее исполнением. Нажав на кнопку Step Over, можно перейти к следующей строке. При этом можно увидеть, что переменная a внутри функции main инициализировалась соответствующим значением.

enter image description here

При нажатии на кнопку Step Over еще один раз произойдет переход на строку 26 (пустые строчки пропускаются автоматически).

При нажатии на кнопку Step Into произойдет заход в функцию foo:

enter image description here

Список переменных изменился. Теперь показываются локальные переменные для функции foo: параметры a (со значением 15), b (с адресом переменной a функции main и значением, которое лежит по этому адресу, равным 17), c (переданной по ссылке из функции main), а также локальная переменная d.

Стек вызовов также теперь отображает вверху текущую функцию, в которой сейчас находится поток исполнения приложения, а ниже указана функция main, из которой был произведен вызов.

Дальше можно продолжить пошаговое выполнение приложения через Step Over, либо нажать на Step Out или Continue. В данном конкретном случае оба этих варианта приведут к остановке на строчке 28 в функции main.

В списке переменных можно увидеть как поменялись значения локальных переменных a и s, после того, как они были переданы в функцию foo по адресу и ссылке соответственно.

Теперь можно либо позволить приложению продолжить исполнение, нажав на Continue, либо прервать его через Stop.

Таким образом, с помощью дебаггера можно смотреть, как ведет себя приложение и отслеживать его состояние, используя пошаговое исполнение и точки останова.

Более продвинутое использование дебаггера оставляется на самостоятельное изучение.

Важное примечание:

При запуске приложения под дебаггером создается отдельный терминал, в который осуществляется ввод и вывод данных приложения. Найти его можно в списке терминалов в соответствующей вкладке. Дебажная консоль и терминал, к которому привязаны ввод/вывод приложения - это не одно и то же.