Как перестать бояться segmentation fault и научиться находить баги за несколько минут

Когда я начинал изучать C++, GDB казался мне чем-то из области фантастики. Чёрный экран, непонятные команды, какая-то магия для настоящих программистов-гуру. Мой метод отладки выглядел примерно так:

Я запускал программу, смотрел, какое число вывелось последним, и примерно понимал, где упало. Потом добавлял еще больше cout и повторял снова.
В первое время это отлично работало, но... пока проект не вырос до нескольких десятков файлов, а баги не стали проявляться раз в 10 запусков. Тогда я понял: либо я научусь пользоваться нормальным отладчиком, либо потрачу остаток жизни на перекомпиляции и строчки с "дошло/не дошло". Оказалось, что GDB - это не магия. 90% задач решается 4-8 командами, а главный страх - просто неизвестность.
В этой статье я расскажу, как преодолеть этот страх, и на реальных примерах, как GDB превращает поиск багов из гадания в системную работу.
cout - это плохой отладчик
Конкретный пример, где cout бессилен. Представьте программу, которая падает с Segmentation fault:

Запуск:
=== Запуск программы === 1. Начало processUser Segmentation fault (core dumped)
Мы знаем, что программа дошла до "1. Начало processUser", но не дошла до "2. Имя пользователя". Это значит, что падение произошло где-то между этими двумя строками. Но где именно? Внутри user.getName()? Может, проблема в setName? Мы не знаем.
С cout нам пришлось бы:
Добавить
coutвнутрьgetNameДобавить внутрь
setNameПерекомпилировать
Запустить снова
Повторять, пока не найдём точное место
С GDB эта задача решается за 15-30 секунд.
GDB (GNU Debugger) - это программа, которая позволяет заглянуть внутрь вашего кода во время выполнения.
С ней вы можете:
Поставить программу на паузу в любой момент
Посмотреть значения всех переменных, а не только тех, которые вы догадались вывести
Пройтись по коду пошагово, наблюдая, как меняются данные
Увидеть стек вызовов - цепочку функций, которая привела к падению
В отличие от cout, GDB не требует перекомпиляции после каждого изменения. Вы просто запускаете программу под отладчиком и исследуете её в реальном времени.
Чтобы GDB мог показывать имена переменных, строки кода и другую полезную информацию, нужно скомпилировать программу с отладочными символами.
Флаги компиляции:
-g - добавляет отладочную информацию
-O0 - отключает оптимизации (иначе компилятор может переставить код, и отладка станет запутанной)
g++ -g -O0 -o myprogram myprogram.cpp
Проверить, что отладочная информация есть, можно командой file:
file myprogram
Если в выводе есть with debug_info, всё сделано правильно.
Первый запуск
Запускаем GDB:
gdb ./myprogram
Вы увидите примерно такое:
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1 ... Reading symbols from ./myprogram... (gdb)
Появилось приглашение (gdb). Теперь можно запустить программу:
(gdb) run
Программа запустится и будет работать как обычно. Если она работает нормально - GDB ничего не делает, просто ждёт. Если программа упадёт - GDB поймает это событие и покажет место падения.
Попробуйте запустить наш пример с багом:
(gdb) run Starting program: ./myprogram === Запуск программы === 1. Начало processUser Program received signal SIGSEGV, Segmentation fault. 0x00005555555551a7 in std::string::operator= (this=0x0, __str=...) at /usr/include/c++/11/bits/basic_string.h:...
GDB сам остановился на месте падения и показал, что проблема в std::string::operator=.
Самая важная команда для новичка - backtrace (или сокращённо bt). Она показывает стек вызовов - цепочку функций, которая привела к падению.
(gdb) bt #0 0x00005555555551a7 in std::string::operator= (this=0x0, __str=...) at ... #1 0x0000555555555190 in UserData::setName (this=0x7fffffffddf0, n=...) at program.cpp:8 #2 0x0000555555555225 in main () at program.cpp:27
Теперь мы видим всю картину:
#0 - место падения: внутри std::string::operator=
#1 - вызов из UserData::setName на строке 8
#2 - вызов из main на строке 27
Проблема ясна: в UserData::setName мы разыменовываем nullptr (это видно по this=0x0 в кадре #1). GDB показывает, что указатель this (указатель на объект, который вызывает метод) равен нулю.
Важно для C++: Имена функций могут выглядеть странно (например, ZNSt7_cxx1112basic_stringIcSt11char_traitsIcESaIcEEaSEOS4_). Это называется "mangled names". Чтобы сделать вывод читаемым, используйте:
(gdb) set print pretty on (gdb) set print demangle on
После этого функции будут отображаться в нормальном виде: std::string::operator=.
Остановка программы
Часто нужно остановить программу до того, как она упадёт, чтобы посмотреть, что происходит. Для этого используются точки останова (breakpoints).
Основные команды:
(gdb) break main # остановиться в начале main (gdb) break program.cpp:42 # остановиться на строке 42 (gdb) break UserData::setName # остановиться при входе в функцию (gdb) info break # посмотреть все брейкпоинты (gdb) delete 1 # удалить брейкпоинт номер 1
C++-специфика: точки останова на перегруженных функциях
Если у вас есть несколько функций с одинаковым именем, нужно указать тип параметров:
void print(int x) { ... } void print(const std::string& s) { ... }
Ставим брейкпоинты так:
(gdb) break print(int) (gdb) break print(std::string)
Продвинуто: точки останова по регулярному выражению
(gdb) rbreak ^.*::print$ # все методы print в любом классе
Когда программа остановилась на брейкпоинте, можно пройтись по коду пошагово.
Команда | Действие |
|---|---|
| Выполнить текущую строку, не заходя внутрь функций |
| Выполнить текущую строку, заходя внутрь функций |
| Выполнить до конца текущей функции и выйти |
| Продолжить выполнение до следующего брейкпоинта |
Важное различие для C++:
Если вы выполните step на строке с вызовом функции из STL (например, std::vector::push_back), вы попадёте внутрь реализации STL - это может быть сложно для новичка. Используйте next, чтобы пропустить вызов, или finish, если случайно зашли внутрь.
std::vector<int> v = {1,2,3}; v.push_back(4); // если сделать step, попадёте внутрь STL
GDB позволяет смотреть значения переменных в любой момент.
Основные команды:
(gdb) print variable_name # показать значение переменной (gdb) print *pointer # показать значение по указателю (gdb) print vec.size() # можно вызывать методы (gdb) display variable_name # показывать значение после каждого шага (gdb) info locals # показать все локальные переменные (gdb) info args # показать аргументы текущей функции
Как GDB показывает C++-объекты:
Для std::vector:
(gdb) print myVector $1 = std::vector of length 5, capacity 8 = {1, 2, 3, 4, 5}
Для std::string:
(gdb) print myString $2 = "Hello, World!"
Если GDB говорит <optimized out>:
Это означает, что компилятор оптимизировал переменную. Чаще всего это происходит при компиляции с флагами -O2 или выше. Для отладки используйте -O0.
TUI-режим: видеть код во время отладки
Это то, что реально меняет опыт отладки, но про это почти нет статей на русском.
TUI (Text User Interface) - режим, в котором экран делится на две части: сверху вы видите исходный код, снизу - команды GDB.
Активация:
(gdb) tui enable
Полезные команды TUI:
Команда | Действие |
|---|---|
| Показать исходный код |
| Показать ассемблер |
| Показать и код, и ассемблер |
| Показать регистры |
| Переключить количество окон |
| Переключить активное окно |
| Выйти из TUI-режима |
В TUI - режиме текущая строка кода подсвечивается, и вы видите, где находитесь, не вспоминая номера строк.
Что делать, если GDB не показывает переменные
Иногда GDB не может показать значение переменной. Вот основные причины и решения:
Проблема | Решение |
|---|---|
Переменная | Перекомпилируйте с |
GDB не видит имена переменных | Убедитесь, что флаг |
Не видно локальные переменные в функции | Выполните |
Вектор пустой, но GDB показывает мусор | В старых версиях libstdc++ используйте |
Основные команды GDB
Вот список команд, которые реально нужны новичку.
Команда | Сокращение | Что делает |
|---|---|---|
|
| Запустить программу |
|
| Поставить точку останова |
|
| Показать стек вызовов |
|
| Выполнить строку (не заходя в функции) |
|
| Выполнить строку (заходя в функции) |
|
| Выполнить до конца текущей функции |
|
| Продолжить выполнение |
|
| Показать значение переменной |
|
| Показывать переменную после каждого шага |
|
| Показать все локальные переменные |
|
| Показать аргументы функции |
|
| Выйти из GDB |
Заключение: что я понял
Когда я только начинал, GDB казался мне сложным и не понятным. Теперь я понимаю:
GDB страшен только до первого успешного запуска. Как только вы увидите, как
backtraceпоказывает точное место падения, страх уходит.Почти 80% задач решается 5-10 командами. Не нужно учить все возможности GDB - достаточно освоить базу.
TUI-режим делает отладку визуальной. Когда видишь код и можешь шагать по нему, отладка перестаёт быть абстракцией.
Умение отлаживать - это привычка, которая прокачивается практикой. С каждым разом вы будете находить баги быстрее и увереннее.
Если вы до сих пор пользуетесь std::cout для отладки - попробуйте GDB. Потратьте один вечер, чтобы освоить эти команды. Это время окупится, когда вы будете ловить баги за минуты вместо часов. Удачи!
