Собираем
информацию
по крупицам
Статьи - Компьютерное

Отладчик 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. Делайте локальную копию этой документации себе - данный сайт часто недоступен по пол-года.

 

 

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

 



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

Поделиться этой страницей


Статистика


RSS подписка

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


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