Отладка программ¶
Сборка в режиме отладки¶
По умолчанию g++ генерирует бинарь без отладочной информации. Для полноценной работы с отладчиком нужно скомпилировать
с двумя флагами:
-g(или-g3для максимального объёма) — добавляет таблицы DWARF: соответствие адресов строкам исходника, имена переменных, типы, макросы;-O0(или-Og) — отключает или снижает оптимизации.
Флаг -g добавляет в ELF-бинарь секции .debug_info, .debug_line, .debug_abbrev и другие — они используются gdb
для отображения исходного кода. Подробнее о том, как задаётся флаг -g на стадии компиляции — в
статье Стадии сборки.
Отличия debug-сборки от release:
| Свойство | Debug | Release |
|---|---|---|
| Размер бинаря | Крупнее | Меньше |
| Скорость | Медленнее | Быстрее |
| Исходный код в gdb | Да | Нет |
| Значения переменных | Да | Часто «оптимизировано» |
Основы работы с gdb¶
Запуск¶
Или передать аргументы уже внутри gdb:
Точки останова (breakpoints)¶
(gdb) break main # по имени функции
(gdb) break file.cpp:42 # по файлу и строке
(gdb) info breakpoints # список всех breakpoint’ов
(gdb) delete 1 # удалить breakpoint #1
Пошаговое выполнение¶
(gdb) run # запустить
(gdb) next # (n) следующая строка, без захода в функцию
(gdb) step # (s) следующая строка, с заходом в функцию
(gdb) continue # (c) продолжить до следующего breakpoint’а
(gdb) finish # выполнить до конца текущей функции
Просмотр переменных¶
(gdb) print x # значение переменной x
(gdb) print *ptr # разыменовать указатель
(gdb) info locals # все локальные переменные текущего фрейма
(gdb) info args # аргументы текущей функции
(gdb) display x # автоматически печатать x после каждого шага
Стек вызовов¶
(gdb) backtrace # (bt) вывести стек вызовов
(gdb) bt full # стек + локальные переменные в каждом фрейме
(gdb) frame 2 # переключиться на фрейм #2
(gdb) up # перейти на фрейм выше
(gdb) down # перейти на фрейм ниже
Core dump: анализ упавшей программы¶
Когда процесс завершается из-за фатального сигнала (SIGSEGV, SIGABRT, SIGFPE, SIGILL и др.), ядро может
сохранить дамп памяти — снимок адресного пространства процесса в момент падения:
Чтобы core dump создавался, нужно снять лимит на его размер:
После падения появится файл core или core.<pid>. Анализ:
Внутри gdb core dump выглядит как программа, остановленная в момент краша:
(gdb) bt # стек вызовов в момент падения
(gdb) frame 2 # перейти к нужному фрейму
(gdb) info locals # локальные переменные в этом фрейме
(gdb) print errno # посмотреть errno
Так можно расследовать падение без повторного воспроизведения.
Watchpoints: наблюдение за памятью¶
gdb позволяет отслеживать чтение или запись конкретного адреса памяти:
(gdb) watch x # остановиться при изменении x
(gdb) rwatch x # остановиться при чтении x
(gdb) awatch x # остановиться при чтении или записи x
(gdb) info watchpoints # список watchpoints
Watchpoints работают через аппаратные регистры отладки (обычно не более 4 штук) и практически не замедляют программу.
Условные точки останова¶
(gdb) break main.cpp:42 if x > 10 # остановиться только если x > 10
(gdb) condition 1 x > 10 # добавить условие к breakpoint #1
Отладка многопоточных программ¶
(gdb) info threads # список потоков
(gdb) thread 2 # переключиться на поток #2
(gdb) thread apply all bt # backtrace для всех потоков сразу
set scheduler-locking on # заморозить остальные потоки при step/next
Sanitizers¶
Sanitizers — это инструменты от Google (изначально из проекта Chromium), встроенные в gcc и clang. Они
инструментируют код на этапе компиляции: вставляют проверки перед каждым доступом к памяти, перед каждой загрузкой
неинициализированного значения, перед каждым обращением к разделяемой переменной. Найденная ошибка печатается в stderr
со стеком вызовов и часто с дополнительным контекстом (какой malloc выделил блок, какой free его освободил).
В отличие от valgrind, sanitizer'ы не эмулируют CPU — они компилируются в сам бинарь. Это даёт в 5-20 раз меньшие
накладные расходы, но требует пересборки.
AddressSanitizer (ASan)¶
Ловит классические ошибки доступа к памяти:
- heap-buffer-overflow — выход за границы блока, выделенного через
malloc/new; - stack-buffer-overflow — выход за границы локального массива;
- global-buffer-overflow — выход за границы глобального массива;
- use-after-free (UAF) — обращение к освобождённой памяти;
- use-after-return — обращение к локальной переменной после возврата из функции (требует
ASAN_OPTIONS=detect_stack_use_after_return=1); - double-free, invalid free — освобождение того, что не выделялось malloc;
- memory leaks — забытые
free/delete(LeakSanitizer встроен в ASan).
Как работает: вокруг каждого выделенного блока ставится red zone — буферная область, помеченная как «отравленная» (
poisoned). Параллельно ведётся shadow memory — байт shadow на каждые 8 байт пользовательской памяти, кодирующий
доступность. Перед каждым load/store компилятор вставляет проверку shadow.
Память программы Shadow memory (1 байт на 8 байт)
┌───────────────────────────┐ ┌────┐
│ red zone (poisoned) │ │ FA │ ← FA = heap left redzone
├───────────────────────────┤ ├────┤
│ user data (32 байта) │ │ 00 │ ← 00 = accessible
│ │ │ 00 │
│ │ │ 00 │
│ │ │ 00 │
├───────────────────────────┤ ├────┤
│ red zone (poisoned) │ │ FA │ ← FA = heap right redzone
└───────────────────────────┘ └────┘
▲
└── любой доступ сюда → ASan report
Накладные расходы: ~2x по времени, ~2-3x по памяти. Несовместим с MemorySanitizer и ThreadSanitizer в одной сборке.
UndefinedBehaviorSanitizer (UBSan)¶
Ловит undefined behavior из стандарта C/C++:
- знаковое переполнение (
INT_MAX + 1); - сдвиг на величину ≥ ширины типа (
1 << 32дляint); - разыменование
NULLи misaligned pointer; - деление на ноль;
- выход за границы массива при известном размере;
- вызов функции через указатель неверного типа;
- использование значения после
static_castк неподходящему типу.
gcc -fsanitize=undefined -g prog.c -o prog
# Подключить отдельные проверки:
gcc -fsanitize=signed-integer-overflow,null,alignment -g prog.c -o prog
Накладные расходы минимальные — порядка 10-20%. UBSan можно включать вместе с ASan: -fsanitize=address,undefined.
ThreadSanitizer (TSan)¶
Обнаруживает data races — одновременный доступ к одной переменной из разных потоков без синхронизации, где хотя бы
один доступ — запись. Также ловит deadlock'и (через aux-режим detect_deadlocks).
Работает по алгоритму happens-before: TSan инструментирует каждый load/store и каждую операцию синхронизации (mutex, atomic, барьер), строит граф зависимостей и проверяет, упорядочены ли два конфликтующих доступа.
Накладные расходы: ~5-15x по времени, ~5-10x по памяти. Несовместим с ASan. Требует пересборки всех библиотек,
включая стандартную (иначе race в libstdc++ не виден).
MemorySanitizer (MSan)¶
Ловит чтение неинициализированной памяти. Каждому биту памяти соответствует shadow-бит «определён/не определён». Запись делает биты определёнными, чтение копирует shadow. Reports выдаётся только когда неопределённое значение влияет на наблюдаемое поведение (branch, syscall argument, output).
Только в clang (в gcc не поддерживается). Накладные расходы: ~3x. Требует пересборки всех зависимостей с MSan,
включая libc — на практике используется в проектах с собственной сборкой stdlib или через libc++ + msan
-инструментированную libc.
Сравнительная таблица¶
| Sanitizer | Что ловит | Накладные расходы | Компилятор | Совместим с |
|---|---|---|---|---|
| ASan | OOB, UAF, double-free, leaks | ~2x время, 2-3x RAM | gcc, clang | UBSan |
| UBSan | UB по стандарту C/C++ | ~10-20% | gcc, clang | ASan, TSan, MSan |
| TSan | data races, deadlocks | 5-15x время | gcc, clang | UBSan |
| MSan | use of uninitialized memory | ~3x время | только clang | UBSan |
Когда использовать что: ASan — в CI на каждый PR (находит большинство багов памяти), UBSan — всегда вместе с ASan, TSan — для многопоточных компонентов отдельным прогоном, MSan — нишево, требует собственной сборки stdlib.
perf¶
perf — Linux-нативный профайлер, использующий PMU (Performance Monitoring Unit) — аппаратные счётчики событий
внутри CPU. PMU считает события (instructions retired, cache misses, branch mispredictions) без программного оверхеда —
счётчик инкрементируется в железе. Часть этой инфраструктуры в ядре называется perf_events.
perf stat¶
Снимает агрегированные счётчики PMU за время работы программы:
perf stat ./prog
perf stat -e cycles,instructions,cache-misses,branch-misses ./prog
perf stat -d ./prog # detailed: добавляет L1/LLC cache
perf stat -p <pid> # уже запущенный процесс
perf stat -a sleep 5 # система целиком за 5 секунд
Типичный вывод:
1 234 567 cycles
2 345 678 instructions # 1.90 insn per cycle
12 345 cache-misses # 15.2 % of all cache refs
5 678 branch-misses # 0.45 % of all branches
insn per cycle (IPC) — главный индикатор: больше 1.0 — CPU хорошо загружен; меньше 0.5 — что-то его тормозит (memory
stalls, branch mispredictions).
perf record и perf report¶
Сэмплирующее профилирование: ядро каждые N циклов прерывает программу и записывает текущий стек в файл perf.data.
Затем perf report показывает, в каких функциях программа провела больше всего времени:
perf record -g ./prog # -g включает запись стеков (call graph)
perf record -F 999 -g ./prog # 999 Hz — сэмпл-частота
perf report # интерактивный TUI
perf report --stdio --sort=overhead,comm,dso | head
events ▼
┌──────────────────────────┐
│ kernel │ syscall, sched │
│ │ перерыв │ ← каждые ~1ms прерывание
│ ↓ │ │
│ process│ RIP, RSP │ ← снимок IP и стека
│ │ стек 32 фрейма │
│ ↓ │ │
│ диск │ perf.data │
└──────────────────────────┘
perf report:
35.2% app matrix_multiply
18.7% app hash_lookup
9.1% libc memcpy
...
Flamegraphs¶
Brendan Gregg придумал flamegraph — визуализацию профиля как стека прямоугольников: ширина = доля времени, высота = глубина стека. Top функции на вершине — это «hot spots».
Скрипты stackcollapse-perf.pl и flamegraph.pl берут из репозитория brendangregg/FlameGraph на GitHub.
strace и ltrace¶
strace трассирует системные вызовы процесса через ptrace (или, в новых ядрах, через seccomp-bpf с
--seccomp-bpf для меньшего оверхеда). ltrace — то же самое для вызовов функций библиотек (через перехват PLT).
strace ./prog # все syscall
strace -e trace=openat,read,write ./prog
strace -e trace=network ./prog # только сетевые
strace -e trace=%file ./prog # все, работающие с файлами
strace -f ./prog # -f: следить и за fork'нутыми
strace -c ./prog # -c: только сводка по syscall
strace -p <pid> # уже запущенный процесс
strace -o trace.log ./prog # вывод в файл
ltrace ./prog # вызовы libc и других .so
ltrace -e malloc+free ./prog # только malloc/free
Сводка strace -c:
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
45.12 0.012345 123 100 read
23.45 0.006789 67 101 write
...
Накладные расходы strace через ptrace — каждый syscall удваивается (вход/выход из ядра дважды). Для production
использовать bpftrace или perf trace.
valgrind beyond memcheck¶
valgrind — это framework для динамической инструментации; memcheck — лишь один из инструментов. Все они работают
через JIT-перекомпиляцию бинаря в промежуточное представление и обратно. Замедление 10-50x.
| Инструмент | Что делает |
|---|---|
| memcheck | OOB, UAF, утечки, неинициализированные чтения (аналог ASan + MSan, но медленнее) |
| callgrind | call graph + точное число выполненных инструкций на каждую функцию (для kcachegrind) |
| cachegrind | симулирует L1/LL cache и branch predictor, показывает cache miss rate по строкам кода |
| helgrind | детектор data race для pthreads (аналог TSan) |
| massif | heap profiler — график потребления heap во времени, кто аллоцирует |
valgrind --tool=callgrind ./prog # → callgrind.out.<pid>
kcachegrind callgrind.out.12345 # GUI-визуализация
valgrind --tool=cachegrind ./prog
cg_annotate cachegrind.out.<pid> # построчная аннотация
valgrind --tool=helgrind ./prog # ищет data races
valgrind --tool=massif ./prog
ms_print massif.out.<pid>
Когда выбирать valgrind, а когда sanitizer'ы: sanitizer'ы быстрее и точнее на UAF/OOB, но требуют пересборки. valgrind
работает с production-бинарём без перекомпиляции и даёт callgrind/cachegrind, которых у sanitizer'ов нет.
Связанные темы¶
- Стадии сборки — флаг
-gи его влияние на бинарь - Символы и манглирование — что такое таблица символов и почему
stripмешает отладке - Системные вызовы: введение —
straceкак альтернативаgdbдля трассировки взаимодействия с ОС - malloc и free: как это работает внутри — что находит ASan в heap
- Защита памяти — ASLR при отладке (
setarch -R)
Источники¶
man gdb— документация GNU Debuggerman 5 core— формат core dump файлаman ulimit/man bash(раздел BUILTINS) — управление лимитамиman strace,man ltrace,man perf,man valgrind- Debugging with GDB — полное руководство пользователя gdb
- AddressSanitizer wiki
- ThreadSanitizer wiki
- MemorySanitizer wiki
- UBSan documentation — clang
- Brendan Gregg: Linux perf examples
- Brendan Gregg: Flame Graphs
- FlameGraph repository
- Valgrind tool suite