Добавить в избранное

Отладка C++-программ: GDB, санитайзеры и чтение core dump

Чтение ≈ 4 мин

Инструменты отладки C++: breakpoints и пошаговая отладка в GDB, AddressSanitizer и UndefinedBehaviorSanitizer, Valgrind для утечек, чтение backtrace из core dump и сборка с отладочной информацией.

Сборка для отладки

Отладка начинается с правильной сборки: флаг -g пишет отладочную информацию (номера строк, имена переменных), -O0 отключает оптимизации, которые «перемещают» код:

g++ -g -O0 main.cpp -o app          # отладочная сборка
g++ -g -O2 -fsanitize=address main.cpp -o app_asan  # прод+санитайзер

В CMake это CMAKE_BUILD_TYPE=Debug или RelWithDebInfo (оптимизация + отладочные символы).

GDB: базовый набор команд

gdb ./app
(gdb) run                    # запуск
(gdb) break main.cpp:25      # точка остановки
(gdb) break myFunc           # по имени функции
(gdb) break main.cpp:25 if x > 10   # условная точка
(gdb) continue               # продолжить
(gdb) next                   # шаг без входа в функцию
(gdb) step                   # шаг с входом
(gdb) print variable         # значение переменной
(gdb) backtrace              # стек вызовов
(gdb) frame 3                # подняться по стеку
(gdb) list                   # код вокруг текущей строки

Когда программа упала по segfault — run, она остановится в месте падения, bt покажет весь стек до момента краха. Это главный инструмент диагностики.

Полезные приёмы GDB

watch variable            # стоп при изменении переменной
info locals               # все локальные переменные кадра
p *ptr                    # разыменование указателя
p vector.size()           # вызов методов
p arr@10                  # 10 элементов из массива
display var               # печатать после каждого шага

Для STL-контейнеров есть pretty-printers — обычно уже встроены в gdb и распечатывают vector/string читаемо.

AddressSanitizer: память под присмотром

ASan ловит выходы за границы, use-after-free, переполнение буферов — на скорости, приемлемой для тестов:

g++ -g -fsanitize=address main.cpp -o app
./app

При ошибке ASan печатает: тип (heap-buffer-overflow, use-after-free), адрес, стек аллокации и стек доступа. Это быстрее, чем час в gdb.

int* p = new int[5];
delete[] p;
p[0] = 1;   // use-after-free — ASan покажет где выделено и где прочитано

UndefinedBehaviorSanitizer

UBSan ловит другое — знаковое переполнение, сдвиги за пределы, разыменование мисалайнд, нуль-указатели:

g++ -g -fsanitize=undefined main.cpp -o app_ubsan
./app_ubsan

Можно комбинировать: -fsanitize=address,undefined — вместе работают.

Valgrind: медленно, но всё видит

Valgrind не требует перекомпиляции — работает как виртуальная машина и проверяет каждое обращение к памяти:

valgrind --leak-check=full --track-origins=yes ./app

Находит утечки, использование неинициализированной памяти, невалидные чтения/записи. Цена — замедление в 10–50 раз, зато без ребилда.

Core dump: отладка после смерти

Когда прод упал и оставил core-файл:

ulimit -c unlimited               # разрешить core
./app                             # упадёт и оставит core
gdb ./app core                    # анализ
(gdb) bt                          # стек в момент смерти
(gdb) info registers              # регистры процессора
(gdb) frame N / list              # подняться по стеку, показать код

Отладочная печать и логирование

Иногда printf — честный инструмент: в реальном времени, на embedded, в местах где отладчик неудобен:

#define LOG(x) std::cerr << __FILE__ << ':' << __LINE__ << ' ' << x << '\n'

LOG("state=" << state << " cnt=" << cnt);

Для релиза — assert из <cassert>: проверяет инвариант и падает с местом ошибки.

Типичные ошибки при отладке

  • Оптимизированная сборка — переменные «отсутствуют», код прыгает. Собирайте -O0.
  • Нет -g — gdb видит только адреса, без исходников.
  • ASan на проде — замедление в 2-3 раза; для прод только в тестовых конфигурациях.
  • p obj без -g — типы неизвестны.
  • Пропускать assert — инварианты бесплатные в Debug и ловят логические баги.

Итог

Базовый арсенал: gdb с break/bt/print для точечной отладки, -fsanitize=address,undefined при подозрении на память и UB, valgrind для утечек без перекомпиляции, core dump для «почему упало на проде». Плюс тривиальное: собирайте с -g -O0, иначе отладчик бесполезен.

Чек-лист отладки нового бага

1. Воспроизведение — устойчивый повторяемый случай. 2. Локализация — printf/loc или бисект коммитов (git bisect). 3. Гипотеза — что именно сломано, как проверить. 4. Проверка — лог/ассерт/отладчик. 5. Фикс + регрессионный тест.

Мини-инструменты, которые помогают каждый день

  • assert(cond) — краш в Debug при нарушении инварианта; в Release выключается.
  • __FILE__, __LINE__, __func__ — в ваших логах координаты точки.
  • std::source_location (C++20) — то же самое, но красивее.
  • static_assert — ошибки вылавливаются компилятором ещё до запуска.
  • -Wall -Wextra -Werror — компилятор находит половину багов сам.

Логирование уровней

Минимальный логгер — три уровня и timestamp:

enum class Level { Debug, Info, Error };
void log(Level l, std::string_view msg) {
    static const char* names[] = {"DBG", "INF", "ERR"};
    std::cerr << timestamp() << " [" << names[(int)l] << "] " << msg << '\n';
}

Debug-логи под условной компиляцией или уровнем — они бесплатны в Release.

Полезные команды GDB за пределами базовых

(gdb) info locals          # все локальные переменные
(gdb) p *ptr               # разыменование
(gdb) p vector             # распечатка STL через pretty-printers
(gdb) watch x == 5         # сломаться когда x станет 5
(gdb) reverse-next         # шаг назад (при enable recording)
(gdb) tbreak main          # временный брейкпоинт — сработает один раз
(gdb) set var x = 10       # изменить переменную на лету

Символы и отладочная сборка

Без -g отладчик видит только адреса. CMake: -DCMAKE_BUILD_TYPE=Debug или RelWithDebInfo — оптимизированная сборка с символами, лучший компромисс для прода. При разборе дампа убедитесь, что бинарь собран с символами — иначе bt бесполезен.

Профилактика лучше лечения

  • Маленькие функции, мало состояния — меньше мест для багов.
  • assert на входе каждой функции — ловит нарушения контрактов рано.
  • Санитайзеры в CI — автоматическая проверка каждого коммита.
  • Юнит-тесты — регрессии ловятся до отладки.

Инструменты, которые стоит знать

  • std::source_location — точка вызова без макросов.
  • __PRETTY_FUNCTION__ — имя функции с шаблонами.
  • abort() / std::terminate — аварийная остановка с дампом.
  • signal(SIGSEGV, handler) — поймать segfault и залогировать.
  • backtrace() / backward-cpp — стектрейс без отладчика.

Логирование в проде

В проде отладчик недоступен — логирование единственный способ. Структурированные логи (JSON), уровни, timestamp, контекст (request id, user id) — и баг находится по логам, а не по воспроизведению. spdlog, log4cpp, g3log — готовые решения.

Профилактика: пишите код, который легко отлаживать

  • Маленькие функции — каждая проверяется отдельно.
  • assert на инвариантах — краш рядом с багом.
  • const везде — меньше случайных мутаций.
  • Чистые функции — легко тестировать и воспроизводить.
Поделиться
Понравился материал?

Комментарии

Добавить комментарий