Точки переривання і покроковий прохід

налагодження програм

Більшість програмістів не розуміють, що отладчики широко використовують точки переривання "за сценою", щоб дозволити основному отладчику управляти підлеглим. Хоча можна встановлювати точки переривання не безпосередньо, відладчик буде їх встановлювати, дозволяючи управляти такими завданнями, як покроковий прохід через (stepping over) викликану функцію. Отладчик також використовує точки переривання, коли необхідно виконати програму до зазначеного рядка вихідного файлу і зупинитися. Нарешті, відладчик встановлює точки переривання, щоб перейти в підлеглий відладчик по команді (наприклад, через вибір пункту меню Debug Break в WDBG).

У лістингу 4-4 показаний код функції SetBreakpoint. Читаючи цей код, майте на увазі, що функції DBG_ * належать бібліотеці LOCALASSIST.DLL і допомагають ізолювати різні підпрограми маніпуляції з процесом, полегшуючи додавання до WDBG функцій віддаленої налагодження. Функція SetBreakpoint ілюструє обробку (описану раніше в цій главі), необхідну для зміни захисту пам'яті при запису в неї.

Лістинг 4-4. Функція SetBreakepoint з 1386CPUHELP.C

int CPUHELP_DLLINTERFACE _stdcall

SetBreakpoint (PDEBUGPACKET dp.

DWORD dwReadWrite = 0;

BYTE bTempOp = BREAK_OPCODE;

ASSERT (FALSE == IsBadReadPtr (dp, sizeof (DEBUGPACKET)));

ASSERT (FALSE == IsBadWritePtr (pOpCode, sizeof (OPCODE)));

if ((TRUE == IsBadReadPtr (dp, sizeof (DEBUGPACKET))) ||

(TRUE == IsBadWritePtr (pOpCode, sizeof (OPCODE))))

TRACE0 ( "SetBreakpoint. Invalid parameters \ n!");

// більше 2 Гбайт, то просто виконайте повернення,

if ((FALSE = IsNT ()) (UlAddr> = 0x80000000))

bReadMem = DBG_ReadProcessMemory (dp-> hProcess.

(LPCVOID) ulAddr, SbTempOp. sizeof (BYTE), SdwReadWrite);

ASSERT (FALSE! = BReadMem);

ASSERT (sizeof (BYTE) = dwReadWrite);

if ((FALSE = bReadMem) ||

(Sizeof (BYTE)! = DwReadWrite))

// Чи готова ця нова точка переривання переписати

// код операції існуючої точки переривання?

if (BREAKJDPCODE = bTempOp)

// Отримати сторінкові властивості для підлеглого відладчика.

// Перевести підлеглий відладчик в режим

if (FALSE == DBG_VirtualProtectEx (dp-> hProcess.

ASSERT (. "VirtualProtectEx .failed !!");

// Зберегти код замінної операції.

// Код операції був збережений, так що тепер

// потрібно встановити точку переривання.

bWriteMem = DBG_WriteProcessMemory (dp-> hProcess.

ASSERT (FALSE! = BWriteMem);

ASSERT (sizeof (BYTE) == dwReadWrite);

if ((FALSE == bWriteMem) ||

(Sizeof (BYTE)! = DwReadWrite))

// Повернути захист до стану, який передував

// установці точки переривання

// Change the protection back to what it was before

// I blasted thebreakpoint in.

VERIFY (DBG_VirtualProtectEx (dp-> hProcess.

// Скинути кеш інструкцій в разі, якщо ця пам'ять була в кеші CPU

bFlush = DBG_FlushInstructionCache (dp-> hProcess.

ASSERT (TRUE = bFlush);

Питання: як заново точку переривання, щоб мати можливість повторно зупинятися в цьому місці? Якщо CPU підтримує покрокове виконання, перевстановлення точки переривання тривіальна. У покроковому режимі CPU виконує єдину інструкцію і генерує інший тип виключення - EXCEPTION_SINGLE_STEP (0x80000004). На щастя, все CPU, на яких виконуються 32-розрядні Windows, підтримують покрокове виконання. Для переходу в режим покрокового виконання процесорів Intel Pentium потрібно встановити (в одиничний стан) біт 8 регістра прапорів. Довідкове керівництво Intel називає його бітом пастки - Trap Rag (TF або прапором трасування). У лістингу 4-5 приведена функція Setsingiestep і дії, необхідні для установки біта TF. Після заміни точки переривання вихідним кодом операції відладчик відзначає в своєму внутрішньому стані, що він очікує покрокового виконання, встановлює в CPU відповідний режим і потім продовжує процес.

Лістинг 4-5. Функція SetSingleStep з 1386CPUHELP. C

BOOL CPUHELP_DLLIMNTERFACE _stdcall

SetSingleStep (PDEBUGPACKET dp)

ASSERT (FALSE == IsBadReadPtr (dp, sizeof (DEBUGPACKET)));

if (TRUE = IsBadReadPtr (dp, sizeof (DEBUGPACKET)))

TRACED ( "SetSingleStep. Invalid parameters \ n!");

// Для i386, просто встановити TF-біт.

bSetContext = DBG_SetThreadContext (dp-> hThread,

ASSERT (FALSE! = BSetContext);

Після того як основний відладчик розблокує процес, викликаючи функцію ContinueDebugEvent, цей процес після кожного виконання окремої інструкції негайно генерує покрокове виняток. Щоб упевнитися, що це було очікуване покрокове виняток, відладчик перевіряє свій внутрішній стан. Оскільки відладчик очікував таке виключення, т "знає", що точка переривання повинна бути переінстальовано. На кожному окремому етапі цього процесу покажчик команд просувається в позицію, що передує вихідної точки переривання. Тому відладчик може встановлювати код операції точки переривання назад в її вихідне положення. Кожен раз, коли відбувається виняток типу EXCEPTION_ SINGLE_STEP. операційна система автоматично скидає біт TF, так що немає ніякої необхідності скидати його з допомогою відладчика. Після установки точки переривання основної відладчик розблокує підлеглий, і той залишиться активним.

Всю обробку точки переривання реалізує метод CWDBGProjDOC. -andieBreakpoint. який можна знайти в файлі WDBGPROJDOC.CPP на супроводжує компакт-диску. Самі точки переривання визначені в файлах BREAKPOINTS і BREAKPOINT.CPP. Ці файли містять пару класів, які обробляють точки переривання різних стилів. Діалогове вікно WDBG Breakpoints дозволяє встановлювати точки переривання при виконанні підлеглого відладчика точно так же, як це робиться в отладчике Visual C ++. Здатність встановлювати точки переривання "на льоту" означає, що необхідно ретельно зберігати слід стану вторинного відладчика і стану точок переривання. Подробиці обробки включення і виключення точок переривання в залежності від стану підлеглого відладчика можна знайти в описі методу CBreakpointsDig :: OnOk в файлі BREAKPOINTSDLG.CPP на супроводжує компакт-диску.

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

Точки переривання, що встановлюються при реалізації пункту Debug Break, дещо відрізняються від тих, що використовує WDBG. Такі точки називають одноразовими (one-shot) точками переривання, тому що вони віддаляються відразу ж, як тільки спрацьовують. Отримання набору таких точок переривання представляє певний інтерес. Повне уявлення можна отримати, проаналізувавши функцію CWDBGProj Doc. OnDebugBreak з WDBGPROJDOC.CPP. а тут наведемо лише деякі повчальні подробиці. У лістингу 4-6 показана функція CWDBGProj Doc. OnDebugBreak з WDBGPROJDOC.CPP. Додаткові відомості про одноразові точках переривання наведені далі в розділі "Операції Step Into, Step Over u Step Out" цієї глави.

Лістинг 4-5. Обробка Debug Breake в WDBGPROJDOC.CPP

void CWDBGProjDoc. OnDebugBreak ()

ASSERT (m_vDbgThreads.size ()> 0);

// Ідея тут полягає в тому, щоб призупинити всі потоки

// підлеглого відладчика і встановити покажчик поточної інструкції

// для кожного з них на точку переривання. Таким чином, я можу

// гарантувати, що принаймні один з потоків буде

// відловлювати одноразові точки переривання. Одна з ситуацій,

// при якій установка точки переривання на кожному потоку не буде

// працювати, відбувається, коли додаток "висить". оскільки в

// обороті немає потоків, точки переривання ніколи не викликаються.

// Щоб виконати роботу в такий тупикової ситуації, я був змушений

// використовувати наступний алгоритм: '

// 1. Встановити точки переривання за допомогою даної функції.

// 2. Встановити прапорець стану, який вказує, що я очікую

// на точці переривання Debug Break.

// 3. Встановити фоновий таймер на очікування точки переривання.

// 4. Якщо одна з точок переривання зникає, скинути таймер.

// 5. Якщо таймер скидається, то додаток "висить".

// 6. Після таймера встановити покажчик інструкції одного з

// 7. рестартовать потік.

// 8. Коли ці спеціальні точки переривання спрацюють, очистити

// точку переривання і перевстановити покажчик команди

// назад в початкове положення.

// Підвищимо пріоритет цього потоку так,

// щоб пройти через установку цих точок переривання якомога

// швидше і оберегти будь-який потік підлеглого відладчика від

HANDLE hThisThread = GetCurrentThread ();

int iOldPriority = GetThreadPriority (hThisThread);

SetThreadPriority (hThisThread, THREAD_BASE_PRIORITY_LOWRT);

HANDLE hProc = GetDebuggeeProcessHandle ();

DBGTHREADVECT :: iterator i; for (i = m_vDbgThreads.begin ();

// Призупинити цей потік. Якщо він вже має лічильник

// припинень, мене це, насправді, не турбує. Саме

// тому точки переривання і встановлювалися на кожному потоці

// підлеглого відладчика. Я знаходжу активний потік

// в кінцевому рахунку випадково.

// Потік призупинено, можна отримати контекст.

// Оскільки, якщо використовується ASSERT, пріоритет цього потоку

// встановлений в реальному масштабі часу, і комп'ютер може

// "висіти" на панелі повідомлення, тому в if-операторі можна

// вказати помилку тільки за допомогою оператора трасування.

if (FALSE! = DBG_GetThreadContext (i-> m_hThread, ctx))

DWORD dwAddr = ReturnlnstructionPointer ( ctx);

// Встановити точку переривання.

if (TRUE == cBP.ArmBreakpoint (hProc))

// Додати цю точку переривання до списку Debug Break,

// тільки якщо точка переривання була успішно

// активізована. Підлеглий відладчик легко міг би

// мати множинні потоки, пов'язані з однією і тією ж

// тільки одну точку переривання. m_aDebugBreakBPs.Add (cBP);

TRACE ( "GetThreadContext failed! Last Error = Ox% 08X \ n",

// Оскільки функція GetThreadContext зазнала невдачі,

// ймовірно, слід подивитися, що трапилося. Тому

// увійдемо в відладчик, що виконує налагодження відладчика WDBG.

// Навіть притому, що потік WDBG виконується на рівні

// пріоритетів реального масштабу часу, виклик DebugBreak

// негайно видаляє цей потік з планувальника операційної

// системи, тому його пріоритет знижується. DebugBreak ();

// Все потоки мають встановлені точки переривання. тепер будемо

// всіх їх рестартовать і відправляти кожному потокове повідомлення.

// Причина для відправки таких повідомлень проста. якщо підлеглий

// відладчик прореагує на повідомлення або іншу обробку, він буде

// негайно перерваний. Однак, якщо він просто простоює в циклі

// повідомлень, необхідно змусити його до дії.

// Оскільки є ідентифікатор (ID) потоку, будемо просто посилати

// потоку повідомлення WM_NULL. Передбачається, що це простеньке

// повідомлення, так що воно не повинно зіпсувати підлеглий відладчик.

// Якщо потік не має черги повідомлень, ця функція просто потерпить

// невдачу для такого потоку, не заподіявши ніякої шкоди,

for (i = m_vDbgThreads.begin ();

// Нехай цей потік продовжить виконання

// до чергової точки переривання

PostThreadMessage (i-> m_dwTID, WM_NULL, 0, 0);

// Тепер знизити пріоритет до старого значення.

SetThreadPriority (hThisThread, iOldPriority);

Для того щоб зупинити підлеглий відладчик, потрібно примудритися "втиснути" точку переривання в потік команд CPU так, щоб можна було зупинятися в отладчике. Якщо потік виконується, то підібратися до відомої точці можна за допомогою API-функції suspendThread. призупиняє його. Потім, викликавши API-функцію GetThreadContext. визначити покажчик поточної команди. Маючи такий покажчик, можна повернутися до установки простих точок переривання. Встановивши точку переривання, потрібно викликати API-функцію ResumeThread. щоб дозволити потоку продовжувати виконання і зробити так, щоб він натрапив на цю точку.

Хоча втрутитися в відладчик досить просто, потрібно подумати ще про пару проблем. Перша полягає в тому, що ваша точка переривання може не спрацювати. Якщо підлеглий відладчик обробляє повідомлення або робить деяку іншу роботу, він буде перерваний. Однак, якщо підлеглий відладчик, перебуваючи в такому стані, очікує прибуття повідомлення, точка переривання не спрацьовуватиме, поки підлеглий відладчик не отримає повідомлення. Хоча можна було б вимагати від користувача перемістити мишу над підлеглим отладчиком, щоб згенерувати повідомлення. _MOUSEMOVE. але сам користувач може не прийти в захват від такого вимоги.

Щоб гарантувати, що підлеглий відладчик досягне точки переривання, потрібно послати йому повідомлення. Якщо все, що ви маєте, це дескриптор потоку, виданий налагоджувальний API, то незрозуміло, як перетворити цей дескриптор в відповідний дескриптор вікна (HWND)? На жаль, зробити це можна. Однак, маючи дескриптор потоку, завжди можна викликати функцію PostThreadMessage. яка відправить повідомлення в чергу поточних повідомлень. Оскільки обробка HWND-повідомлення накладається на вершину черзі поточних повідомлень, виклик PostThreadMessage зробить точно те, що потрібно.

Залишається зрозуміти, яке повідомлення слід відправити? Не можна відправляти повідомлення, яке могло б змусити підлеглий відладчик робити якусь реальну обробку, дозволяючи, таким чином, основним отладчику змінювати поведінку підлеглого відладчика. Наприклад, відправка повідомлення WM_CREATE. ймовірно, не була б гарною ідеєю. На щастя існує більш відповідне повідомлення - WM_NULL. яке ви, ймовірно, використовуєте як засіб налагодження при зміні повідомлень. Відправлення повідомлення WM_NULL за допомогою PostThreadMessage не приносить ніякої шкоди, навіть якщо потік не має черги повідомлень, а додаток є консольним. Оскільки консольні додатки завжди знаходяться в стані виконання, навіть якщо очікують клавішну команду, установка точки переривання в поточній виконується команді викличе переривання.

Інша проблема пов'язана з багатопоточність. Якщо ви збираєтеся припиняти тільки один потік, а додаток багатопотокове, то необхідно дізнатися, який потік слід призупинити? Якщо, перервавши виконання програми, встановити точку переривання в неправильному потоці, скажімо в тому, який блокований в стані очікування події, сигнал від якого надходить тільки, наприклад, під час фонової друку, то ваша точка переривання не спрацює, поки користувач не вирішить щось то надрукувати. Єдиний безпечний спосіб перервати багатопоточний додаток складається в припиненні всіх потоків та встановлення точки переривання в кожному з них.

Така методика добре працює з додатком, яке має тільки два потоки. Однак якщо потоків багато, то проблема залишається відкритою. Припиняючи кожен з потоків підлеглого відладчика, ви так змінюєте стан додатки, що з'являється небезпека загнати його в глухий кут. Щоб припиняти всі потоки, встановлювати точки переривання і відновлювати потоки без проблем, відладчик повинен підвищити пріоритет свого власного потоку. Підвищивши пріоритет до THREAD_BASE_PRIORITY_LOWRT. відладчик може так планувати свій потік, щоб потоки підлеглого відладчика не виконувалися, коли базовий відладчик маніпулює ними.

Поки алгоритм переривання багатопоточних додатків звучить розумно. Однак, щоб зробити пункт Debug Break повністю працюючим, необхідно вирішити ще одну, останню проблему. Якщо встановлений весь набір точок переривання у всіх потоках і ці потоки поновлюються, то все ще можлива ситуація, в якій не буде відбуватися переривань. Встановлюючи точки переривання, ви покладаєтеся на виконання, принаймні, одного з потоків, щоб викликати виключення точки переривання. А що трапиться, якщо процес перебуває в ситуації глухого кута? Нічого не станеться - ніякі потоки не виконуються, і ваші ретельно розміщені точки переривання ніколи не викличуть виключення.

Чи знаєте Ви, що спостерігачі - це операції, які використовують в якості аргументу об'єкти відповідного їм типу і повертають елемент іншого типу, вони використовуються для отримання інформації про об'єкт. Сюди відносяться, наприклад, операції типу size.

НОВИНИ ФОРУМУ
Лицарі теорії ефіру