Отладка C++ кода: как перестать бояться segmentation fault и полюбить GDB
Вы когда-нибудь тратили три часа на поиск бага, который в итоге оказывался забытым break в switch или лишним ; после if? Если да — добро пожаловать в клуб. Отладка C++ — это искусство, граничащее с мазохизмом. Но, как и любое искусство, его можно освоить.
В этой статье мы разберём не просто «как запустить GDB», а реальные техники, которые превращают отладку из гадания на кофейной гуще в детективную работу Шерлока Холмса. Мы поговорим о современных санитайзерах, реверсивной отладке и даже о том, как отловить баг, который воспроизводится «только в пятницу после обеда».
1. Классика жанра: GDB и LLDB
Начнём с фундамента. GNU Debugger (GDB) и его конкурент LLDB — это швейцарские ножи отладчика. Но большинство использует их на 10% возможностей. Давайте исправим это.
Точки останова с условиями
Вы ставите точку останова в цикле из 10 000 итераций и жмёте continue 9999 раз? Остановитесь. Используйте условные брейкпоинты:
(gdb) break main.cpp:42 if i == 5000
Это сэкономит часы жизни. В LLDB синтаксис похож:
(lldb) breakpoint set --file main.cpp --line 42 --condition 'i == 5000'
Watchpoints — ловим изменение переменной
Представьте: у вас есть переменная, которая меняется неизвестно где. Ставим watchpoint:
(gdb) watch my_variable
(gdb) rwatch my_variable # сработает на чтение
(gdb) awatch my_variable # сработает и на чтение, и на запись
Теперь программа остановится ровно в тот момент, когда кто-то коснётся вашей переменной. Магия, правда?
2. Reverse Debugging: путешествие во времени
Самый страшный сон программиста: программа упала, но вы не знаете, как она дошла до этого состояния. Реверсивная отладка (обратная отладка) позволяет «отмотать» выполнение назад.
В GDB для этого нужна поддержка record:
(gdb) record
(gdb) continue
... программа упала ...
(gdb) reverse-step # шаг назад
(gdb) reverse-continue # продолжить в обратную сторону
В коммерческих инструментах (например, UndoDB или rr от Mozilla) это работает ещё круче. rr (Record and Replay) — бесплатный инструмент, который записывает выполнение программы, а потом позволяет проигрывать его снова и снова:
$ rr record ./my_program
$ rr replay
(gdb) continue
... нашли место падения ...
(gdb) reverse-step
Совет: используйте rr для поиска гонок данных (data races) — он детерминированно воспроизводит каждое выполнение.
3. Санитайзеры: прививка от undefined behavior
Современный C++ (начиная с Clang и GCC 4.8+) имеет встроенные инструменты, которые не снились нашим отцам. AddressSanitizer (ASan), UndefinedBehaviorSanitizer (UBSan) и ThreadSanitizer (TSan) — это must-have.
Пример: ловим утечку памяти
// compile with: g++ -fsanitize=address -g main.cpp -o main
#include <iostream>
void leak() {
int* p = new int(42);
// забыли delete
}
int main() {
leak();
return 0;
}
Запускаем — и видим:
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from:
#0 0x7f... in operator new(unsigned long)
#1 0x400... in leak() main.cpp:5
#2 0x400... in main main.cpp:10
ASan покажет точный стек вызовов, включая номер строки, где произошло выделение. Больше никаких «где-то утекает память».
UBSan против неопределённого поведения
// compile with: g++ -fsanitize=undefined -g main.cpp
int main() {
int x = INT_MAX;
int y = x + 1; // переполнение signed int — UB!
return 0;
}
UBSan тут же сообщит: runtime error: signed integer overflow. И вы узнаете о баге до того, как он превратится в трудноуловимый глюк.
4. Valgrind: старый, но верный друг
Если санитайзеры — это современная медицина, то Valgrind — это дедовский метод, который до сих пор работает. Он медленнее (программа тормозит в 10-20 раз), но зато не требует перекомпиляции.
$ valgrind --leak-check=full ./my_program
Valgrind отлавливает:
- Утечки памяти (включая «всё ещё достижимые» блоки)
- Использование неинициализированной памяти
- Переполнение буфера
- Двойное освобождение (double free)
Единственный минус — Valgrind не понимает современные аллокаторы и может выдавать ложные срабатывания на std::vector или std::string. Но для самописных структур он незаменим.
5. Статический анализ: профилактика лучше лечения
Лучший баг — тот, который не попал в код. Статические анализаторы проверяют исходный код без его запуска. Для C++ лучшие бесплатные варианты:
- Clang-Tidy — встроен в Clang, находит проблемы с производительностью, стилем и потенциальные баги.
- CPPCheck — лёгкий, быстрый, находит классические ошибки (выход за границы массива, утечки).
- PVS-Studio — коммерческий, но для открытых проектов бесплатен. Находит 99% багов, включая логические.
Пример: что найдёт Clang-Tidy
// clang-tidy --checks=* main.cpp
void example() {
std::string s = "hello";
s[10] = 'x'; // выход за границы — undefined behavior
}
Clang-Tidy выдаст: warning: array index 10 is past the end of the array [clang-diagnostic-array-bounds]. И это без единого запуска программы.