Налагодження в gdb як відловити звернення до змінної або до потрібного властивості класу (статті -

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

Користуючись програмою kdbg (це інтерфейс над дебаггера gdb), я простежив логіку виконання програми і виявив, що в якийсь момент кілька protected-властивостей одного з об'єктів починають містити неприпустимі значення, внаслідок чого і спотворюється логіка. Ніяких сеттерів для цих властивостей в коді не було. Значення властивостей виставлялися з якихось умов в самому класі, причому діапазон можливих значень був сильно обмежений. А ті значення, які я спостерігав, дуже відрізнялися від можливих.

Існувала проблемма, що якийсь код писав дані не в ту область пам'яті. Зазвичай це відбувається при виході за кордон масиву.

На жаль, існуючі матеріали в інтернеті мало розповідають про те, як налагоджувати в gdb C ++ програми стосовно класів і властивості. Тому довелося розбиратися в неходженими.

У середовищі kdbg є можливість встановлювати watchpoint-и, але по факту вони в kdbg не працюють. Дебаг довелося робити в консольному gdb. так як в ньому watchpoint-и працюють як належить. Передбачається, що читач знайомий з основами дебага в gdb. вміє ставити брекпоінти, виконувати програму по кроках, і знає як переглядати значення змінних.

Отже, ми знаходимося в конструкторі. Нам буде допомагати той факт, що значення this в будь-якому конструкторі відомо, так як пам'ять для створюваного примірників класу завжди виділяється заздалегідь.

В "чистому" gdb треба дати команду:

> Print this.counter

Цими командами ми дізналися значення змінної і в разі kdbg правильне написання імені.

> print (This.counter)

Взагалі я помітив, що в "чистому" gdb необов'язково абсолютно правильно писати вирази в синтаксисі C / C ++. Наприклад, символи "->" можна замінювати точкою ".", А писати символи "*" для інформування про тип покажчика теж необов'язково. Це спрощує налагодження.

Властивість може являти собою покажчик на якийсь базовий тип або покажчик на інший об'єкт (буде розглянуто нижче). У цьому випадку дії ті ж самі! Просто, перебуваючи в конструкторі, треба додебажіть до того місця, коли покажчик проініціалізіруется. Після чого тикаємо на нього правою кнопкою мишки, вибираємо "Watch expression". В області спостережень з'явиться рядок:

Причому, цей рядок може бути розгорнута, і в ній можна побачити значення, на яке посилається даний покажчик.

Часто буває так, що проблеммная змінна знаходиться в властивості, яке представляє собою якийсь об'єкт. І вже в цьому об'єкті знаходиться проблеммное властивість.

В цьому випадку, треба крок за кроком додебажіться до ініціалізації об'єкта-властивості. Після чого можна "розвернути" дерево змінних до інтересуемой змінної, і точно так же по правій кнопці мишки вибирати "Watch expression". В області спостережень з'явиться рядок:

В "чистому" gdb для таких же дій можна дати команду:

> Print this.window.active

+-> (Bool *). Active 0x859f161

Далі передбачається, що налагодження йде в gdb, так як в kdbg watchpoint-и є, але вони не працюють.

Передбачається, що в класі STApp існує змінна pWindow типу "покажчик на об'єкт STWindow", а у цього об'єкта STWindow є bool -змінного bSleep. Значення цієї змінної в якийсь момент псується. Потрібно зловити цей момент і подивитися код, який псує дані.

Ставимо брекпоінт на першу інструкцію в конструкторі класу STApp. Запускаємо програму на виконання. Після зупинки покроково доходимо до моменту, в якому проініцалізірован об'єкт pWindow. Далі переконуємося, що ініціалізація пройшла нормально:

$ 1 = (STApp * const) 0xbffff284

(Gdb) print this.pWindow

$ 2 = (class STWindow *) 0x859ee10

(Gdb) print this.pWindow.bSleep

Судячи з вихідного коду, спочатку змінна bSleep повинна бути false. так що все в порядку.

(Gdb) print (This.pWindow.bSleep)

$ 4 = (bool *) 0x859f161

(Gdb) watch * ((int *) 0x859f161)

Hardware watchpoint 2: * ((int *) 0x859f161)

Перевіряємо, як додався цей watchpoint:

Num Type Disp Enb Address What

1 breakpoint keep y 0x08070fa8 in STApp :: STApp (int, char **)

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)

14 fLastPressed + = deltaT;

Подивимося, що за код поруч з цією 14-м рядком:

void button :: compute (float deltaT)

Ага, по всій видимості, якийсь об'єкт класу button розміщується десь поряд з потрібними bSleep. і при запам'ятовуванні значення fLastPressed. пише це значення туди, де лежить bSleep. Подивимося стек викликів:

# 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 - G nu D eB ugger. А графічний KDE-інтерфейс до нього називається kdbg - K de D eB ugG er. Тобто, буква g на початку gdb позначає G nu, а в кінці kdbg означає суфікс G er. Не плутайте написання.

Всім удачі, і безглючная програм.