MyTetra Share
Делитесь знаниями!
О, смотри-ка какое хорошее место. Дайте два!
Отладчик gdb: как отловить обращение к переменной или к нужному свойству класса
27.02.2011
02:53
Раздел: Компьютер - Программирование - Отладчик GDB

Недавно мне пришлось отлаживать один крупный проект, который содержал большие куски говнокода. Необходимость в отладке возникла из-за того, что в одном дистрибутиве (Debian Lenny) этот гавнокод работал правильно. А в другом дистрибутиве (Debian Squeeze) этот же гавнокод вел себя совершенно по-другому.



Пользуясь программой kdbg (это интерфейс над дебаггером gdb), я проследил логику выполнения программы и обнаружил, что в какой-то момент несколько protected-свойств одного из объектов начинают содержать недопустимые значения, вследствие чего и искажается логика. Обращаю внимание - искажались значения свойств в секции protected! Однако никаких сеттеров для этих свойств в коде небыло. Значения свойств выставлялись по каким-то условиям в самом классе, причем диапазон возможных значений был сильно ограничен. А те значения, которые я наблюдал, очень отличались от возможных.


Налицо была проблемма, что какой-то код писал данные не в ту область памяти. Обычно это происходит при выходе за границу массива.


Чтобы поймать момент и найти место, когда происходит запись кривых данных в интересуемое свойство класса, нужно установить точку наблюдения на ячейку памяти, занимаемую этим свойством. То есть, вначале нужно узнать адрес свойства для конкретного экземпляра класса, и затем установить watchpoint по данному адресу.


К сожалению, существующие материалы в интернете мало рассказывают о том, как отлаживать в gdb C++ программы, содержащие классы и свойства. Поэтому пришлось разбираться в нехоженном.


В среде kdbg есть возможность устанавливать watchpoint-ы, но по факту они в kdbg не работают. Дебаг пришлось делать в консольном gdb, так как в нем watchpoint-ы работают как положено. Предполагается, что читатель знаком с основами дебага, умеет ставить брекпоинты, выполнять программу по шагам, и знает как просматривать значения переменных.



Выяснение адреса для свойства класса


Проще всего выяснить адрес свойства для конкретного экземпляра класса можно следующим образом. Нужно поставить брекпоинт (breakpoint) на первые команды конструктора. В момент остановки по стеку вызовов и по некоторым переменным можно узнать, в каком конкретно экземпляре класса в данный момент находится отладка. Мне было просто - экземпляр проблеммного класса был всего один в программе.


Итак, мы находимся в конструкторе. Значение this в конструкторе известно, так как память уже выделена и в конструкторе происходит этап начальной инициализации свойств объекта.



Выяснение адреса переменной базового типа - bool, int, float, и т.д.


Если свойство представляет собой переменную базового типа, то узнать адрес этой переменной достаточно просто. В kdbg надо "развернуть" ветку this, выделить интересуемое свойство, сделать клик правой кнопкой мыши, и выбрать пункт "Watch expression". В области наблюдения появится строка наподобе:


Expression Значение

(*this).counter 135


В "чистом" gdb надо дать команду:


> print this.counter

$1=(int) 135


Затем узнать адрес этой переменной в 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


Устанавливаем на этот адрес 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


Так же в этом разделе:
← Содержание ...
 
MyTetra Share v.0.35
Яндекс индекс цитирования