Недавно мне пришлось отлаживать один крупный проект, который содержал большие куски говнокода. Необходимость в отладке возникла из-за того, что в одном дистрибутиве (Debian Lenny) этот гавнокод работал правильно. А в другом дистрибутиве (Debian Squeeze) этот же гавнокод вел себя совершенно по-другому.
Пользуясь программой kdbg (это интерфейс над дебаггером gdb), я проследил логику выполнения программы и обнаружил, что в какой-то момент несколько protected-свойств одного из объектов начинают содержать недопустимые значения, вследствие чего и искажается логика. Никаких сеттеров для этих свойств в коде небыло. Значения свойств выставлялись по каким-то условиям в самом классе, причем диапазон возможных значений был сильно ограничен. А те значения, которые я наблюдал, очень отличались от возможных.
Налицо была проблемма, что какой-то код писал данные не в ту область памяти. Обычно это происходит при выходе за границу массива.
Чтобы поймать момент и найти место, когда происходит запись кривых данных в интересуемое свойство класса, нужно установить точку наблюдения на ячейку памяти, занимаемую этим свойством. То есть, вначале нужно узнать адрес свойства для конкретного экземпляра класса, и затем установить watchpoint по данному адресу.
К сожалению, существующие материалы в интернете мало рассказывают о том, как отлаживать в gdb C++ программы, содержащие классы и свойства. Поэтому пришлось разбираться в нехоженном.
В среде kdbg есть возможность устанавливать watchpoint-ы, но по факту они в kdbg не работают. Дебаг пришлось делать в консольном gdb, так как в нем watchpoint-ы работают как положено. Предполагается, что читатель знаком с основами дебага в gdb, умеет ставить брекпоинты, выполнять программу по шагам, и знает как просматривать значения переменных.
Выяснение адреса для свойства класса
Проще всего выяснить адрес свойства для конкретного экземпляра класса можно следующим образом. Нужно поставить брекпоинт (breakpoint) на первые команды конструктора. В момент остановки по стеку вызовов и по некоторым переменным можно узнать, в каком конкретно экземпляре класса в данный момент находится отладка. Мне сделать это было просто - экземпляр проблеммного класса всего один в программе.
Итак, мы находимся в конструкторе. Нам будет помогать тот факт, что значение this в любом конструкторе известно, так как память для создаваемого экземляра класса всегда выделяется заранее.
Выяснение адреса переменной базового типа - bool, int, float, и т.д.
Если свойство представляет собой переменную базового типа, то узнать адрес этой переменной достаточно просто. В kdbg надо "развернуть" ветку this, выделить интересуемое свойство, сделать клик правой кнопкой мыши, и выбрать пункт "Watch expression". В области наблюдения появится строка наподобе:
Expression Значение
(*this).counter 135
В "чистом" gdb надо дать команду:
> print this.counter
$1=(int) 135
Этими командами мы узнали значение переменной и в случае kdbg правильное написание имени.
Затем узнать адрес этой переменной в kdbg можно так: копируем имя переменной из столбца Expression в поле ввода выражения (оно находится сверху, рядом с кнопками "Добавить" и "Удалить"). Затем надо обернуть это выражение скобками и добавить символ "&" вначале. И нажать "Добавить".
Пример:
Как написано выше, имеется имя переменной: (*this).counter. Чтобы узнать адрес, пишем в поле ввода выражений такое выражение: &((*this).counter). Нажимаем "Добавить". В области наблюдения появится строка:
Expression Значение
&((*this).counter) 0x82b3020
В "чистом" gdb для выяснения адреса свойства надо сделать те же действия, то есть дать команду:
> print &(this.counter)
$2=(int) 0x82b3020
Вообще я заметил, что в "чистом" gdb необязательно абсолютно правильно писать выражения в синтаксисе C/C++. Например, символы "->" можно заменять точкой ".", а писать символы "*" для информирования о типе указателя тоже необязательно. Это упрощает отладку.
Выяснение адреса свойства, заданного через указатель
Свойство может представлять собой указатель на какой-то базовый тип или указатель на другой объект (будет рассмотрено ниже). В этом случае действия те же самые! Просто, находясь в конструкторе, надо додебажить до того места, когда указатель проинициализируется. После чего тыкаем на него правой кнопкой мышки, выбираем "Watch expression". В области наблюдений появится строка:
Expression Значение
(*this).variable 0x859d6f0
Причем, эта строка может быть развернута, и в ней можно увидеть значение, на которое ссылается данный указатель.
Выяснение адреса свойства в объекте-свойстве
Часто бывает так, что проблеммная переменная находится в свойстве, которое представляет собой какой-то объект. И уже в этом объекте находится проблеммное свойство.
В этом случае, надо пошагово додебажиться до инициализации объекта-свойства. После чего можно "развернуть" дерево переменных до интересуемой переменной, и точно так же по правой кнопке мышки выбирать "Watch expression". В области наблюдений появится строка:
Expression Значение
(*(*this).window).active true
В "чистом" gdb для таких же действий можно дать команду:
> print this.window.active
$3=(bool) true
Как узнать адрес данной переменной? Точно так же - обернуть выражение имени в скобки и поставить амперсанд.
В kdbg делаем:
Expression Значение
&((*(*this).window).active)
+-> (bool *).active 0x859f161
В "чистом" gdb:
> &(this.window.active)
$4=(bool *) 0x859f161
Определять адреса переменных мы научились. Теперь переходим к практике.
Как отловить изменение ячейки с указанным адресом
Прерывание программы по обращению к указанному адресу называется watchpoint (аппаратная точка наблюдения). В микропроцессорах Intel x86 архитектуры 32 бита, начиная с i386, существует возможность установки аппаратных точек наблюдения. Благодаря этому, работа отлаживаемой программы практически не замедляется.
Далее предполагается, что отладка идет в gdb, так как в kdbg watchpoint-ы есть, но они не работают.
Предполагается, что в классе STApp существует переменная pWindow типа "указатель на объект STWindow", а у этого объекта STWindow есть bool-переменная bSleep. Значение этой переменной в какой-то момент портится. Нужно поймать этот момент и посмотреть код, который портит данные.
Ставим брекпоинт на первую инструкцию в конструкторе класса STApp. Запускаем программу на выполнение. После остановки пошагово доходим до момента, в котором проиницализирован объект pWindow. Далее убеждаемся, что инициализация прошла нормально:
(gdb) print this
$1 = (STApp * const) 0xbffff284
(gdb) print this.pWindow
$2 = (class STWindow *) 0x859ee10
(gdb) print this.pWindow.bSleep
$3 = false
Судя по исходному коду, изначально переменная bSleep должна быть false, так что все в порядке.
Выясняем адрес переменной bSleep:
(gdb) print &(this.pWindow.bSleep)
$4 = (bool *) 0x859f161
Видим, что адрес равен 0x859f161. Устанавливаем на этот адрес watchpoint. Адрес указывается через тип int, синтаксис следущий:
(gdb) watch *((int*)0x859f161)
Hardware watchpoint 2: *((int*)0x859f161)
Проверяем, как добавился этот watchpoint:
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x08070fa8 in STApp::STApp(int, char**)
at /St.2.0/Game/stApp.cpp:169
breakpoint already hit 1 time
2 hw watchpoint keep y *((int*)0x859f161)
Видим два прерывания программы: первое прерывание - это обычный breakpoint, который у нас сработал. Второе прерывание - это как раз созданный нами аппаратный watchpoint.
Когда сработал watchpoint
После установки watchpoint-а даем команду continue, чтобы программа продолжила свое выполнение. И в какой-то момент gdb остановит выполнение программы:
Hardware watchpoint 2: *((int*)0x859f161)
Old value = 1409833472
New value = 1413336268
button::compute (this=0x859f160, deltaT=0.100000001)
at /St/Window/button.cpp:14
14 fLastPressed += deltaT;
Прекрасно! Дебагер показал нам место, которое ошибочно записывает данные по адресу, по которому расположено свойство bSleep.
Посмотрим, что за код рядом с этой 14-й строкой:
void button::compute(float deltaT)
{
fBegin += deltaT;
fLastPressed += deltaT;
}
Ага, по всей видимости, некий объект класса button размещается где-то рядом со свойством bSleep, и при запоминании значения fLastPressed, пишет это значение туда, где лежит bSleep. Посмотрим стек вызовов:
(gdb) where
#0 button::compute (this=0x859f160, deltaT=0.100000001)
at /St/Window/button.cpp:14
#1 0xb644d5a3 in STBaseWindow::compute (this=0x859ee10, deltaT=0.100000001)
at /St/Window/stWindowsSDL.cpp:423
#2 0xb642d2b6 in STSceneGraph::render_scene (this=0xb1fda008)
at /St/Render/SceneGraph/stSceneGraph.cpp:412
#3 0xb6437106 in STSceneGraphSlot::render_scene (this=0xb1fda008)
at /St/Render/SceneGraph/stSceneGraphSlot.cpp:138
#4 0x08070b82 in STApp::Idle (this=0xbffff284)
at /St.2.0/Game/stApp.cpp:39
#5 0xb644db65 in STBaseWindow::MainLoop (this=0x859ee10)
at /St/Window/stWindowsSDL.cpp:275
#6 0xb63ff42c in App::loop (this=0xbffff284)
at /St/Application/app.cpp:49
#7 0x08071c71 in STApp::Run (this=0xbffff284, argc=1, argv=0xbffff364)
at /St.2.0/Game/stApp.cpp:495
#8 0x08073280 in main (argc=1, argv=0xbffff364)
at /St.2.0/Game/stMain.cpp:13
Заглянем в код, который вызывает метод button::compute(), он судя по информации из стека вызовов, расположен тут: /St/Window/stWindowsSDL.cpp:423. Выглядит этот код так:
for ( int c=0;c<25;c++ )
{
dEmulattionButton[c].compute(deltaT);
dButtons[c].compute(deltaT);
}
Ага, массивы объектов. И "магическая константа" 25. А какая размерность у этих массивов? Находим в заголовочном файле /St/Window/stWindowsSDL.h такой код:
...
button dButtons[24];
button dEmulattionButton[24];
...
Снова "магическая константа", но на этот раз 24. Думаю, тут все понятно - имеем классический гавнокод с магическими константами и выходом за границы массива.
Таким образом, мы выяснили, в чем была проблема, и исправить её не составит труда.
И напоследок
В Linux, по странной традиции, имена программ зачастую человеконечитаемые. Поэтому, обращаю внимание, что консольный дебагер называется gdb - Gnu DeBugger. А графический KDE-интерфейс к нему называется kdbg - Kde DeBugGer. То есть, буква g в начале gdb обозначает Gnu, а в конце kdbg означает суффикс Ger. Не путайте написание.
Ссылка по теме
Полная документация по GDB на русском яыке: http://mitya.pp.ru/gdb/gdb_toc.html. Делайте локальную копию этой документации себе - данный сайт часто недоступен по пол-года.
Всем удачи, и безглючных программ.