aktden2 часа назад
GDB для тех, кто думает, что отладка — это фантастика
Уровень сложностиПростойВремя на прочтение6 минОхват и читатели2.1KC++*Из песочницыКак перестать бояться 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 # удалить брейкпоинт номер 1C++-специфика: точки останова на перегруженных функциях
Если у вас есть несколько функций с одинаковым именем, нужно указать тип параметров:
void print(int x) { ... }
void print(const std::string& s) { ... }Ставим брейкпоинты так:
(gdb) break print(int)
(gdb) break print(std::string)Продвинуто: точки останова по регулярному выражению
(gdb) rbreak ^.*::print$ # все методы print в любом классеКогда программа остановилась на брейкпоинте, можно пройтись по коду пошагово.
КомандаДействиеnext (или n)Выполнить текущую строку, не заходя внутрь функцийstep (или s)Выполнить текущую строку, заходя внутрь функцийfinishВыполнить до конца текущей функции и выйтиcontinue (или c)Продолжить выполнение до следующего брейкпоинтаВажное различие для C++:
Если вы выполните step на строке с вызовом функции из STL (например, std::vector::push_back), вы попадёте внутрь реализации STL - это может быть сложно для новичка. Используйте next, чтобы пропустить вызов, или finish, если случайно зашли внутрь.
std::vector<int> v = {1,2,3};
v.push_back(4); // если сделать step, попадёте внутрь STLGDB позволяет смотреть значения переменных в любой момент.
Основные команды:
(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:
КомандаДействиеlayout srcПоказать исходный кодlayout asmПоказать ассемблерlayout splitПоказать и код, и ассемблерlayout regsПоказать регистрыCtrl + X, 2Переключить количество оконCtrl + X, oПереключить активное окноCtrl + X, aВыйти из TUI-режимаВ TUI - режиме текущая строка кода подсвечивается, и вы видите, где находитесь, не вспоминая номера строк.
Что делать, если GDB не показывает переменные
Иногда GDB не может показать значение переменной. Вот основные причины и решения:
ПроблемаРешениеПеременная <optimized out>Перекомпилируйте с -O0 вместо -O2GDB не видит имена переменныхУбедитесь, что флаг -g присутствует при компиляцииНе видно локальные переменные в функцииВыполните info locals после того, как программа вошла в функциюВектор пустой, но GDB показывает мусорВ старых версиях libstdc++ используйте print vec._M_impl._M_start для просмотра указателя на данные
Основные команды GDB
Вот список команд, которые реально нужны новичку.
КомандаСокращениеЧто делаетrunrЗапустить программуbreakbПоставить точку остановаbacktracebtПоказать стек вызововnextnВыполнить строку (не заходя в функции)stepsВыполнить строку (заходя в функции)finishfinВыполнить до конца текущей функцииcontinuecПродолжить выполнениеprintpПоказать значение переменнойdisplaydispПоказывать переменную после каждого шагаinfo localsi localsПоказать все локальные переменныеinfo argsi argsПоказать аргументы функцииquitqВыйти из GDB
Заключение: что я понял
Когда я только начинал, GDB казался мне сложным и не понятным. Теперь я понимаю:
• GDB страшен только до первого успешного запуска. Как только вы увидите, как backtrace показывает точное место падения, страх уходит.
• Почти 80% задач решается 5-10 командами. Не нужно учить все возможности GDB - достаточно освоить базу.
• TUI-режим делает отладку визуальной. Когда видишь код и можешь шагать по нему, отладка перестаёт быть абстракцией.
• Умение отлаживать - это привычка, которая прокачивается практикой. С каждым разом вы будете находить баги быстрее и увереннее.Если вы до сих пор пользуетесь std::cout для отладки - попробуйте GDB. Потратьте один вечер, чтобы освоить эти команды. Это время окупится, когда вы будете ловить баги за минуты вместо часов. Удачи!Теги:• gdb
• с++
• отладкаХабы:• C++
Получайте больше инсайтов о систематизации бизнеса
Подписывайтесь на Telegram-канал Business Operations — ежедневные материалы о бизнес-процессах, операционном управлении и повышении эффективности
💬 Подписаться на канал→ Оригинальная статья