Сборка для отладки
Отладка начинается с правильной сборки: флаг -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везде — меньше случайных мутаций.- Чистые функции — легко тестировать и воспроизводить.