Отладка C++ — метод вместо угадывания
Опытного инженера от джуниора отличает не «знание gdb», а способность подойти к незнакомому багу системно. Половина багов решается за минуту, если задать правильную последовательность вопросов. Другая половина не решается за неделю, если их задавать в неправильном порядке.
Карта темы
- Системный подход — воспроизведение, бинарный поиск, формулирование гипотез.
- Точки останова — HW vs SW, watchpoints, conditional breakpoints.
- Отладчики —
gdb,lldb, Visual Studio, удалённая отладка. - Санитайзеры и Valgrind — ASan, UBSan, TSan, MSan — что ловят и когда применять.
- Production-отладка — core dumps, structured logging,
perf,rr.
Системный подход
Стандартный workflow:
- Воспроизведи стабильно. Баг, который происходит «иногда», нельзя отлаживать — сначала найди минимальные шаги, после которых он происходит каждый раз.
- Сравни ожидаемое и фактическое. Чётко сформулируй, что должно было произойти, и что произошло на самом деле. Удивительно часто баг — в ожидании, а не в коде.
- Изолируй бинарным поиском. Половини зону подозрения. Закомментируй половину кода, или revert до половины коммитов (
git bisect), или отключи половину фич. - Сформулируй гипотезу и проверь её. Не «давай ещё попробую» — а «если бы X было true, то Y; проверим Y».
- Чини причину, не симптом. Если функция возвращает
null— найди почему она его возвращает, а не оберни вызов вif.
Точки останова
Software breakpoint — отладчик подменяет инструкцию по адресу на int 3 (x86) и при срабатывании восстанавливает оригинал. Бесплатны по количеству, но требуют, чтобы код был доступен на запись (в JIT-коде или ROM — нет).
Hardware breakpoint — CPU имеет 4 регистра отладки (DR0–DR3 на x86) для точек останова и watchpoints. Срабатывают по адресу инструкции или данных. Watchpoint на изменение переменной — единственный практичный способ найти, кто её повредил. Ограничение — 4 штуки одновременно.
Conditional breakpoint — точка с условием (break foo if x > 100). Удобно, но медленно: каждое срабатывание обрабатывается отладчиком.
Отладчики
gdb — стандарт под Linux. Команды: b (breakpoint), n/s (step over / into), bt (backtrace), p (print), info locals, watch. TUI-режим (Ctrl+x a) показывает исходник.
lldb — стандарт под macOS/Clang. Семантически похож, синтаксис чуть другой.
Visual Studio — лучший GUI-отладчик под Windows, поддерживает remote и intellitrace.
⚠️ Отлаживай debug-сборку или RelWithDebInfo. В оптимизированной release-сборке отладчик прыгает по неожиданным строкам — это inlining и переупорядочивание, не баг отладчика.
Санитайзеры и Valgrind
Скомпилируй с флагами и получи runtime-проверки:
- AddressSanitizer (
-fsanitize=address) — out-of-bounds, use-after-free, double-free. Накладные расходы ~2x по памяти и времени. - UndefinedBehaviorSanitizer (
-fsanitize=undefined) — переполнение знаковых, разыменование nullptr, мисалайнмент. Низкий overhead. - ThreadSanitizer (
-fsanitize=thread) — гонки данных. Несовместим с ASan, ~5–15x overhead. - MemorySanitizer (
-fsanitize=memory) — чтение неинициализированной памяти. Требует, чтобы вся используемая библиотека была собрана с MSan.
Valgrind (memcheck) — не требует пересборки, но в 10–50 раз медленнее ASan. Используется, когда пересобрать с санитайзерами нельзя.
⚠️ ASan и TSan не запускают одновременно — оба перехватывают аллокацию.
Production-отладка
Production-краш не воспроизводится локально? Стандартный набор:
- Core dumps. Включи через
ulimit -c unlimitedи/proc/sys/kernel/core_pattern. Анализируй:gdb binary core→bt. В Kubernetes — настройsecurityContext.allowPrivilegeEscalationи volume для дампов. - Structured logging — JSON-логи с request-id, чтобы trace проходил через сервисы.
perf record— собирает sampling profile без пересборки.rr(Record and Replay) — записывает выполнение, потом отлаживает детерминированно с reverse-step. Игнор-чейнджер для intermittent багов.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Менять несколько вещей одновременно | Непонятно, что исправило проблему |
Отлаживать release-сборку без -g | Отладчик прыгает по случайным строкам |
| Запускать ASan + TSan вместе | Конфликт перехватчиков, undefined behavior |
| Запускать санитайзер в production | 2-10x overhead, OOM под нагрузкой |
printf-отладка в многопоточном коде | Буферизованный вывод перемежается, лог нечитаем |
| Не включать core dumps до релиза | После креша в проде — никаких артефактов |
Чинить симптом (if (!ptr) return;), а не причину | Баг возвращается в другом месте |
| Условные breakpoints без необходимости | Программа тормозит в 100 раз |
Значение для собеседований
Debugging — почти всегда middle-вопрос. Интервьюер смотрит не на «знаешь команды gdb», а на ход мыслей: воспроизвёл → изолировал → гипотеза → проверка. Хороший ответ называет санитайзеры (особенно ASan и TSan) и понимает, когда какой применить.
Типичный неправильный ответ: «Я бы добавил printf везде и посмотрел, где сломается». Это не отладка, а угадывание. Правильно начинать с воспроизведения и бинарного поиска.
Популярные направления:
- Чем HW breakpoint отличается от SW? Сколько HW breakpoints доступно одновременно?
- Какой санитайзер найдёт data race? А use-after-free?
- Как отлаживать краш, который происходит только в production?
- Зачем нужны core dumps и как их анализировать?
- Что такое
rrи когда он лучше обычногоgdb?