Собираем
информацию
по крупицам

Отладчик GDB

Отладка в gdb: как отловить обращение к переменной или к нужному свойству класса
27-02-2011
21:08:36

Недавно мне пришлось отлаживать один крупный проект, который содержал большие куски говнокода. Необходимость в отладке возникла из-за того, что в одном дистрибутиве (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. Делайте локальную копию этой документации себе - данный сайт часто недоступен по пол-года.

 

 

Всем удачи, и безглючных программ.

 


К списку "Компьютерное"

Интересное на сайте


Linux: как перестать удивляться, и начать работать » Как установить "John the ripper" под Linux и как ей пользоваться

Программа "John the ripper" служит для восстановления забытых паролей. Официальный сайт программы - http://www.openwall.com/john/   Установка &nb...


Штучки-дрючки » Самодельный диафильм

Недавно я нашел в чулане диапроектор и коробку с диафильмами, которые много лет назад засунул на самую дальнюю полку. Увидев эти коробки, я тут же всп...


Классическая анимация » Прыгающая подушка

Оборудование: Pentium-IV, Wacom Graphire3 CTE-630 Среда: Flash 8 Год: 2005   Первая и, видимо, последняя попытка нарисовать мини-мультфильм по т...

RSS подписка

Подпишитесь на новости сайта по RSS


http://www.oaorosek.ru/ руководящий документ росэк.

Внимание!

На этом сайте разрабатывается программа MyTetra и её родственные проекты.

Доступны к просмотру следующие базы знаний:

База Xintrea (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18)

База Rarrugas (1, 2)

База Balas

База YellowRaven

База Yurons

База Lesnik757

База Shandor

База Sirrichar

 

Подробности на странице MyTetra Share.

 WebHamster.Ru
 Домик любопытного хомячка
Яндекс индекс цитирования
Почтовый ящик