Отладка приложений

Точки прерывания и пошаговый проход


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

>Концепция установки точки прерывания довольно проста. Все, что нужно сделать — это получить адрес памяти, где требуется установить точку прерывания, сохранить код машинной команды (его значение), расположенный в этом месте, и записать по этому адресу инструкцию точки прерывания. В семействе Intel Pentium мнемоника инструкции точки прерывания выглядит как INT з, а код операции — ОхСС, так что нужно сохранить только единственный байт по адресу, где вы устанавливаете точку прерывания. Другие CPU, такие как Intel Merced, имеют иные размеры кода операции, поэтому придется сохранять больше данных по этому адресу.

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

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

int CPUHELP_DLLINTERFACE _stdcall



SetBreakpoint ( PDEBUGPACKET dp ,

ULONG ulAddr ,

OPCODE * pOpCode ) 

{

DWORD dwReadWrite = 0;

BYTE bTempOp = BREAK_OPCODE;

BOOL bReadMem;

BOOL bWriteMem;

BOOL bFlush;

MEMORY_BASIC_INFORMATION mbi;

DWORD dwOldProtect;

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!");

return ( FALSE); 

}

// Если текущая операционная система Windows 98 и адрес

 // больше 2 Гбайт, то просто выполните возврат,

 if ( ( FALSE = IsNT ()) && ( ulAddr >= 0x80000000))

 {

return ( FALSE); 

}

// Читать код операции по определенному адресу.

 bReadMem = DBG_ReadProcessMemory ( dp->hProcess ,

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

ASSERT ( FALSE != bReadMem); 

ASSERT ( sizeof ( BYTE) = dwReadWrite);

 if ( ( FALSE = bReadMem ) ||

( sizeof ( BYTE) != dwReadWrite)) 

{

return ( FALSE); 

}

// Готова ли эта новая точка прерывания переписать 

// код операции существующей точки прерывания?

 if ( BREAKJDPCODE = bTempOp)

{

return ( -1); 

}

// Получить страничные свойства для подчиненного отладчика.

 DBG_VirtualQueryEx ( dp->hProcess  ,

(LPCVOID)ulAddr,

&mbi ,

 sizeof ( MEMORY_BASIC_INFORMATION) ); 

// Перевести подчиненный отладчик в режим 

// "копирование-при записи" для страниц памяти,

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

mbi.BaseAddress ,

 mbi.RegionSize , 

PAGE_EXECUTE_READWRITE, 

&mbi.Protect ) ) 

{

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

 return ( FALSE);

 }

// Сохранить код заменяемой операции. 

*pOpCode = (void*)bTempOp; 

bТеmрОр = BREAK_DPCODE; 

dwReadWrite = 0;

// Код операции был сохранен, так что теперь

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

bWriteMem = DBG_WriteProcessMemory ( dp->hProcess ,

(LPVOID)ulAddr , 

(LPVOID)SbTempOp,

 sizeof ( BYTE) , 

sdwReadWrite ); 

ASSERT ( FALSE != bWriteMem); 



ASSERT ( sizeof ( BYTE) == dwReadWrite);

if ( ( FALSE == bWriteMem ) ||

( sizeof ( BYTE) != dwReadWrite)) 

{

return ( FALSE); 

}

 // Вернуть защиту к состоянию, которое предшествовало

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

// Change the protection back to what it was before

// I blasted thebreakpoint in.

VERIFY ( DBG_VirtualProtectEx ( dp->hProcess ,

mbi. BaseAddress, 

mbi.RegionSize ,

 mbi.Protect , 

SdwOldProtect ));

// Сбросить кэш инструкций в случае, если эта память была в кэше CPU

 bFlush = DBG_FlushInstructionCache ( dp->hProcess ,

(LPCVOID)ulAddr, 

sizeof ( BYTE) );

 ASSERT ( TRUE = bFlush);

 return ( TRUE);

 }

После установки точки прерывания CPU выполнит ее и сообщит отладчику, что произошло исключение EXCEPTION_BREAKPOINT (0x80000003) — здесь-то и появляется проблема. Если это регулярная точка прерывания, то отладчик определит место ее размещения и покажет его пользователю. После того как пользователь решает .продолжить выполнение, отладчик должен проделать некоторую работу, чтобы восстановить состояние программы. Точка прерывания переписала часть памяти, поэтому если вы, как автор отладчика, просто позволите процессу продолжаться, то выполните неправильную кодовую последовательность, и подчиненный отладчик, вероятно, завершится аварийно. Поэтому следует передвинуть указатель текущей инструкции назад, к адресу точки прерывания и заменить ее кодом операции, который был сохранен при установке этой точки. После восстановления кода операции можно продолжать выполнение.

Вопрос: как переустанавливать точку прерывания, чтобы иметь возможность повторно останавливаться в этом месте? Если 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) 

{

BOOL bSetContext;

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

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

{

TRACED ( "SetSingleStep : invalid parameters\n!");

return ( FALSE);

}

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

dp->context.EFlags |= TF_BIT;

bSetContext = DBG_SetThreadContext ( dp->hThread,

&dp->context);

ASSERT ( FALSE != bSetContext);

return ( 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 ();

 i != m_vDbgThreads.end () ;

 i++ ) 

{

// Приостановить этот поток. Если он уже имеет счетчик

// приостановок, меня это, на самом деле, не беспокоит. Именно

// поэтому точки прерывания и устанавливались на каждом потоке

// подчиненного отладчика.


Я нахожу активный поток

//в конечном счете случайно.

DBG_SuspendThread ( i->m_hThread);

// Поток приостановлен, можно получить контекст.

CONTEXT ctx;

ctx.ContextFlags = CONTEXT_FULL;

// Поскольку, если используется ASSERT, приоритет этого потока

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

// "висеть" на панели сообщения, поэтому в if-операторе можно

// указать ошибку только с помощью оператора трассировки.

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

{

// Найти адрес, который указатель команд собирается

// выполнить. Это адрес, где будет устанавливаться

// точка прерывания.

DWORD dwAddr = ReturnlnstructionPointer ( &ctx);

COneShotBP cBP;

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

cBP.SetBreakpointLocation ( dwAddr);

// Активизировать ее.

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

{

// Добавить эту точку прерывания к списку Debug Break, 

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

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

 // иметь множественные потоки, связанные с одной и той же

 // командой, но я хочу установить на этот адрес

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

}

 }

else 

{

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

 GetLastError ());

#ifdef _DEBUG

// Поскольку функция GetThreadContext потерпела неудачу,

 // вероятно, следует посмотреть, что случилось. Поэтому

 // войдем в отладчик, выполняющий отладку отладчика WDBG.

 // Даже притом, что поток WDBG выполняется на уровне 

// приоритетов реального масштаба времени, вызов DebugBreak

 // немедленно удаляет этот поток из планировщика операционной

 // системы, поэтому его приоритет снижается. DebugBreak ();

 #endif

}

 }

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

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

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


Если подчиненный

 // отладчик прореагирует на сообщения или другую обработку, он будет 

// немедленно прерван. Однако, если он просто простаивает в цикле

 // сообщений, необходимо вынудить его к действию.

// Поскольку имеется идентификатор (ID) потока, будем просто посылать 

// потоку сообщение WM_NULL. Предполагается, что это простенькое

 // сообщение, так что оно не должно испортить подчиненный отладчик. 

// Если поток не имеет очереди сообщений, эта функция просто потерпит

 // неудачу для такого потока, не причинив никакого вреда,

 for ( i = m_vDbgThreads.begin () ;

 i!= m_vDbgThreads.end () ;

 i++ ) 



// Пусть этот поток продолжит выполнение

 //до очередной точки прерывания

. DBG_ResumeThread ( i->ro_hThread);

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

Для того чтобы учесть возможность тупиковой ситуации, нужно установить таймер, отметив момент добавления прерывания. По истечении определенного времени (отладчик Visual C+ использует 3 секунды), нужно предпринять некоторое решительное действие. Когда время пункта Debug Break заканчивается, нужно сначала установить один из указателей поточной команды на другой адрес, затем — точку прерывания в этом новом адресе и рестартовать поток. Когда эти специальные точки прерывания сработают, необходимо сместить указатель поточной команды назад, в его первоначальное положение.В WDBG антитупиковая обработка не реализована, эта возможность оставлена читателям в качеств упражнения в функции GWDBGProjDoc::OnDebugBreak (файл WDBGPROJDOC.CPP на сопровождающем компакт-диске). Полная инфраструктура для управления антитупиковой обработкой расположена там же и, вероятно, потребуется не больше пары часов для ее заполнения. Завершив ее реализацию, вы будете хорошо понимать, как работает WDBG.



Содержание раздела