Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004

.pdf
Скачиваний:
321
Добавлен:
13.08.2013
Размер:
3.3 Mб
Скачать

ГЛАВА 4 Поддержка отладки ОС и как работают отладчики Win32

171

 

 

 

 

 

 

 

 

 

 

 

 

{

 

 

 

szDLLName[ 0 ] = _T ( '\0' ) ;

 

 

}

 

 

 

}

 

 

 

if ( _T ( '\0' ) != szDLLName[ 0 ] )

 

 

{

 

 

 

_tcsupr ( szDLLName

) ;

 

 

_tprintf ( _T ( "

DLL name

: %s\n" ) ,

 

szDLLName

 

) ;

 

}

 

 

 

else

 

 

 

{

 

 

 

_tprintf ( _T ( "UNABLE TO READ DLL NAME!!\n" ) ) ;

}

}

/*////////////////////////////////////////////////////////////////////// // Отображение событий выгрузки DLL.

//////////////////////////////////////////////////////////////////////*/

void DisplayDllUnLoadEvent ( UNLOAD_DLL_DEBUG_INFO & stULDDI )

{

_tprintf

(

_T

(

"DLL Unload Event

:\n" ) ) ;

_tprintf

(

_T

(

"

lpBaseOfDll

: 0x%08X\n" ) ,

 

 

stULDDI.lpBaseOfDll

) ;

}

/*//////////////////////////////////////////////////////////////////////

// Отображение событий OutputDebugString.

//////////////////////////////////////////////////////////////////////*/

void DisplayODSEvent ( HANDLE

hProcess ,

 

OUTPUT_DEBUG_STRING_INFO & stODSI

)

{

 

 

 

_tprintf ( _T ( "OutputDebugString Event

:\n" ) ) ;

 

_tprintf ( _T ( "

lpDebugStringData

: 0x%08X\n" ) ,

stODSI.lpDebugStringData

 

) ;

_tprintf ( _T ( "

fUnicode

: 0x%08X\n" ) ,

stODSI.fUnicode

 

) ;

_tprintf ( _T ( "

nDebugStringLength

: %d\n"

) ,

stODSI.nDebugStringLength

 

) ;

_tprintf ( _T ( "

String

: " ) ) ;

 

TCHAR szFinalBuff[ 512 ] ;

if ( stODSI.nDebugStringLength > 512 )

{

_tprintf ( _T ( "String to large!!\n" ) ) ; return ;

}

DWORD dwRead ;

см. след. стр.

172 ЧАСТЬ II Производительная отладка

BOOL bRet ;

//Интересно, что вызовы OutputDebugString независимо

//от того, является ли приложение полностью UNICODE овым,

//всегда работают со строками ANSI.

if ( false == stODSI.fUnicode )

 

{

 

// Читаем ANSI строку.

 

char szAnsiBuff[ 512 ] ;

 

bRet = ReadProcessMemory ( hProcess

,

stODSI.lpDebugStringData

,

szAnsiBuff

,

stODSI.nDebugStringLength ,

&dwRead

) ;

if ( TRUE == bRet )

 

{

 

MultiByteToWideChar ( CP_THREAD_ACP ,

0

,

szAnsiBuff

,

1

,

szFinalBuff

,

512

) ;

 

}

 

 

else

 

 

{

 

 

szFinalBuff[ 0 ] = _T ( '\0' ) ;

 

 

}

 

 

}

 

 

else

 

 

{

 

 

// Читаем UNICODE строку.

 

 

bRet = ReadProcessMemory ( hProcess

 

,

stODSI.lpDebugStringData

,

szFinalBuff

,

stODSI.nDebugStringLength *

 

 

sizeof ( TCHAR )

,

&dwRead

 

) ;

if ( FALSE == bRet )

 

 

{

 

 

szFinalBuff[ 0 ] = _T ( '\0' ) ;

 

 

}

 

 

}

 

 

if ( _T ( '\0' ) != szFinalBuff[ 0 ] )

{

_tprintf ( _T ( "%s\n" ) , szFinalBuff ) ;

}

else

{

_tprintf ( _T ( "UNABLE TO READ ODS STRING!!\n" ) ) ;

ГЛАВА 4 Поддержка отладки ОС и как работают отладчики Win32

173

 

 

}

}

/*////////////////////////////////////////////////////////////////////// // Отображение событий исключений.

//////////////////////////////////////////////////////////////////////*/

void DisplayExceptionEvent ( EXCEPTION_DEBUG_INFO & stEDI )

{

_tprintf ( _T ( "Exception Event

:\n" ) ) ;

 

_tprintf ( _T ( "

dwFirstChance

: 0x%08X\n" ) ,

 

stEDI.dwFirstChance

 

) ;

_tprintf ( _T ( "

ExceptionCode

: 0x%08X\n" ) ,

 

stEDI.ExceptionRecord.ExceptionCode

) ;

_tprintf ( _T ( "

ExceptionFlags

: 0x%08X\n" ) ,

 

stEDI.ExceptionRecord.ExceptionFlags

) ;

_tprintf ( _T ( "

ExceptionRecord

: 0x%08X\n" ) ,

 

stEDI.ExceptionRecord.ExceptionRecord

) ;

_tprintf ( _T ( "

ExceptionAddress

: 0x%08X\n" ) ,

 

stEDI.ExceptionRecord.ExceptionAddress

) ;

_tprintf ( _T ( "

NumberParameters

: 0x%08X\n" ) ,

 

stEDI.ExceptionRecord.NumberParameters

) ;

}

WDBG — настоящий отладчик

Думаю, лучший способ разобраться в работе отладчика — написать его, что я и сделал. Хотя WDBG вряд ли в ближайшее время заменит отладчики Visual Studio

.NET и WinDBG, он определенно делает почти все, что должен делать отладчик. WDBG имеется среди файлов, записанных на CD, прилагаемом к книге. На рис. 4 3 вы увидите отладку программы CrashFinder из главы 12 в WDBG. На рисунке CrashFinder застопорен на третьем экземпляре точки прерывания, которую я уста новил на функции GetProcAddress библиотеки KERNEL32.DLL. Окно Memory в вер хнем правом углу отображает второй параметр, строку InitializeCriticalSection AndSpinCount, передаваемую CrashFinder’ом конкретному экземпляру GetProcAddress. На рис. 4 3 WDBG делает именно все, что вы ожидаете от отладчика, включая отображение регистров, дизассемблированного кода и загруженных в настоящее время модулей и исполняющихся потоков. Выдающимся является окно Call Stack (стек вызовов), показанное в середине правой части рис. 4 3. WDBG не только отображает стек вызовов так, как вы ожидали, но и в полной мере поддерживает отображение локальных переменных и развертывание структур. Что вы не види те на этом рисунке и что должно там быть при первом запуске WDBG, это то, что WDBG поддерживает также точки прерывания, перечисление символов и их ото бражение в окне Symbols (символы), а также прерывание исполнения приложе ния в отладчике.

174 ЧАСТЬ II Производительная отладка

Рис. 4 3. WDBG в действии

В целом WDBG меня радует, так как он является отличным примером, и я гор жусь, что WDBG демонстрирует все внутренние приемы, обычно используемые в отладчиках. Однако, глядя на UI, можно заметить, что я не тратил много времени на отдельные его части. На самом деле все окна многооконного интерфейса яв ляются полями ввода. Я сделал это намеренно — оставил пользовательский ин терфейс простым, потому что не хотел, чтобы детали интерфейса отвлекали вас от существенно важной части кода отладчика. Я написал пользовательский интер фейс WDBG с применением библиотеки классов Microsoft Foundation Class (MFC), поэтому, если вы знаете ее, для вас не составит большого труда спроектировать более нарядный UI.

Прежде чем перейти к специфическим вопросам отладки, посмотрим побли же на WDBG. В табл. 4 2 перечислены основные подсистемы WDBG. Одной из моих целей при создании WDBG было определить промежуточный интерфейс между UI и циклом отладки. Имея промежуточный интерфейс, если понадобится сделать поддержку удаленной отладки в WDBG в сети, нужно будет просто заменить ло кальные DLL.

Табл. 4-2. Основные подсистемы WDBG

Подсистема

Описание

WDBG.EXE

Этот модуль содержит весь код UI. Кроме того, здесь произво

 

дится вся обработка точек прерывания. Основная часть работы

 

отладчика производится в WDBGPROJDOC.CPP.

ГЛАВА 4

Поддержка отладки ОС и как работают отладчики Win32

175

 

 

Табл. 4-2. Основные подсистемы WDBG (продолжение)

 

 

 

 

Подсистема

Описание

 

LOCALDEBUG.DLL

Этот модуль содержит цикл отладки. Так как я хотел использо

 

вать этот цикл отладки и в других проектах, пользовательский

 

код (WDBG.EXE в данном случае) применяет в цикле отладки

 

класс C++, порожденный от класса CdebugBaseUser (определяемо

 

го в DEBUGINTERFACE.H). Цикл отладки будет обращаться к

 

 

этому классу при возникновении каких либо событий отладки.

 

Пользовательский класс отвечает за синхронизацию.

 

 

WDBGUSER.H и WDBGUSER.CPP содержат координирующий

 

 

класс WDBG.EXE. WDBG.EXE использует простой тип синхрони

 

зации с помощью вызовов SendMessage. Иначе говоря, поток от

 

ладки посылает сообщение потоку UI и останавливается, пока

 

поток UI не вернет управление. Если событие отладки требует

 

ввода со стороны пользователя, отладочный поток останавли

 

вается после отправки сообщения о событии синхронизации.

 

При обработке потоком UI команды Go, он вызывает событие

 

синхронизации, и отладочный поток возобновляет работу.

 

LOCALASSIST.DLL

Этот модуль — просто оболочка для функций API, манипулиру

 

ющих памятью отлаживаемой программы и ее регистрами. Бла

 

годаря интерфейсу, определяемому в этом модуле, WDBG.EXE и

 

I386CPUHELP.DLL могут управлять также и удаленной отладкой

 

после замены этого модуля.

 

I386CPUHELP.DLL

Хотя этот вспомогательный модуль для процессоров IA32

 

 

(Pentium) специфичен для процессоров Pentium, его интер

 

 

фейс, определяемый в CPUHELP.H, не зависит от типа процессо

 

ра. Если вы захотите перенести WDBG на другой процессор,

понадобится заменить только этот модуль. Код дизассемблера в этом модуле восходит к примеру кода программы Dr. Watson, поставляемому с Platform SDK. Хотя дизассемблер работает, он требует обновления для поддержки последних версий процес соров Pentium.

Чтение памяти и запись в нее

Чтение из памяти отлаживаемой программы производится очень просто. Это де лает ReadProcessMemory. Отладчик имеет полный доступ к отлаживаемой програм ме, если он запустил ее, так как описатель процесса, возвращаемый событием от ладки CREATE_PROCESS_DEBUG_EVENT имеет права доступа PROCESS_VM_READ и PROCESS_VM_WRITE. Если ваш отладчик присоединяется к процессу посредством DebugActiveProcess, вы должны иметь к процессу, к которому вы присоединяетесь, права SeDebugPrivileges для чтения и записи.

Прежде чем я смогу рассказать о записи в память отлаживаемой программы, надо кратко объяснить важную концепцию «копирования при записи» (copy on write). Загружая исполняемый файл, Windows разрешает для совместного исполь зования различными процессами столько отображаемых страниц памяти, сколь ко возможно. Если один из этих процессов исполняется под отладчиком и одна из этих страниц содержит точку прерывания, то очевидно, что она может отсут ствовать на некоторых используемых совместно страницах. Как только какой то

176 ЧАСТЬ II Производительная отладка

из процессов, исполняющихся вне отладчика, начинает исполнять такой код, воз никает аварийное завершение из за исключения по точке прерывания. Чтобы обойти эту ситуацию, ОС наблюдает, что страница изменена для некоторого про цесса, и делает ее копию для этого процесса, который установил на ней точку прерывания. Так что, как только процесс начинает запись в некоторую страницу, ОС копирует ее.

Запись в память отлаживаемой программы почти столь же проста, как и чте ние. Однако, так как страницы памяти, в которые вы хотите производить запись, могут быть помечены как «только для чтения», вам сначала следует вызвать Virtual QueryEx для получения кода защиты текущей страницы. Зная состояние защиты, можно использовать функцию API VirtualProtectEx, чтобы установить состояние PAGE_EXECUTE_READWRITE для записи в нее, а Windows подготовилась бы выполнять «копирование при записи». Произведя запись, надо восстановить первоначальное состояние защиты страницы. Если этого не сделать, отлаживаемая программа может случайно произвести успешную запись на этой странице, вместо того чтобы за вершиться аварийно. Если исходное состояние защиты было «только для чтения», случайная запись в отлаживаемой программе будет приводить к нарушению до ступа. Если не восстановить состояние защиты, при случайной записи исключе ние вырабатываться не будет, и вы попадаете в ситуацию, при которой работа программы под отладчиком будет отличаться от работы программы вне его.

Есть одна интересная деталь работы отладочного API Win32: отладчик отвеча ет за получение строк для вывода при возникновении события OUTPUT_DEBUG_ST RING_EVENT. Информация, передаваемая отладчику, включает расположение и дли ну строки. Когда он получает это сообщение, отладчик читает память отлаживае мой программы. Так как вызовы OutputDebugString проходят через отладочный API Win32, задерживающий все потоки каждый раз при появлении события отладки, сообщения трассировки могут легко изменить поведение вашего приложения под отладчиком. Если многопоточность запрограммирована корректно, можете вы зывать OutputDebugString как угодно без воздействия на ваше приложение. Одна ко, если у вас есть ошибки в реализации многопоточности, вы можете случайно получить взаимную блокировку потоков из за незаметных изменений соотноше ний времен в связи с вызовами OutputDebugString.

Листинг 4 2 демонстрирует, как WDBG управляет событием OUTPUT_DEBUG_ST RING_EVENT. Заметьте: функция DBG_ReadProcessMemory является оболочкой функции ReadProcessMemory из LOCALASSIST.DLL. Хотя отладочный API Win32 предполагает, что вы можете принимать как строки UNICODE, так и строки ANSI в процессе обработки события OUTPUT_DEBUG_STRING_EVENT, начиная с Windows XP/Server 2003, она передает только строки ANSI, даже если вызов приходит от OutputDebugStringW.

Листинг 4-2. OutputDebugStringEvent из PROCESSDEBUGEVENTS.CPP

static

DWORD OutputDebugStringEvent ( CDebugBaseUser *

pUserClass

,

LPDEBUGGEEINFO

pData

,

DWORD

dwProcessId

,

DWORD

dwThreadId

,

OUTPUT_DEBUG_STRING_INFO & stODSI

)

 

 

 

ГЛАВА 4 Поддержка отладки ОС и как работают отладчики Win32

177

 

 

{

//OutputDebugString может выводить огромное количество символов,

//поэтому я буду выделять память каждый раз.

DWORD dwTotalBuffSize = stODSI.nDebugStringLength ;

if ( TRUE == stODSI.fUnicode )

{

dwTotalBuffSize *= 2 ;

}

PBYTE pODSData = new BYTE [ dwTotalBuffSize ] ;

DWORD dwRead ;

// Читать память.

BOOL bRet = DBG_ReadProcessMemory( pData >GetProcessHandle ( ) ,

stODSI.lpDebugStringData

,

pODSData

,

dwTotalBuffSize

,

&dwRead

) ;

ASSERT ( TRUE == bRet ) ; if ( TRUE == bRet )

{

TCHAR * szUnicode = NULL ;

TCHAR * szSelected = NULL ;

if ( TRUE == stODSI.fUnicode )

{

szSelected = (TCHAR*)pODSData ;

}

else

{

szUnicode = new TCHAR [ stODSI.nDebugStringLength ] ;

BSUAnsi2Wide ( (const char*)pODSData

,

 

szUnicode

,

 

stODSI.nDebugStringLength

) ;

 

int iLen = (int)strlen ( (const char*)pODSData ) ;

iLen = MultiByteToWideChar ( CP_THREAD_ACP

,

0

 

,

(LPCSTR)pODSData

,

iLen

 

,

szUnicode

 

,

stODSI.nDebugStringLength ) ;

szSelected = szUnicode ;

 

 

}

 

 

LPCTSTR szTemp =

 

 

pUserClass >ConvertCRLF ( szSelected

 

,

stODSI.nDebugStringLength );

if ( NULL != szUnicode )

см. след. стр.

178 ЧАСТЬ II Производительная отладка

{

delete [] szUnicode ;

}

//Послать преобразованную строку пользовательскому классу. pUserClass >OutputDebugStringEvent ( dwProcessId ,

dwThreadId

,

szTemp

) ;

delete [] szTemp ;

}

delete [] pODSData ; return ( DBG_CONTINUE ) ;

}

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

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

Концепция установки точек прерывания проста. Все, что вам надо сделать, — это иметь адрес памяти, где вы хотите установить точку прерывания, сохранить код операции (ее значение) в этой точке и записать по этому адресу код коман ды отладочного прерывания. Для семейства процессоров Intel Pentium команда отладочного прерывания имеет мнемонику INT 3 или код операции 0xCC, поэтому вам нужно сохранить только один байт, расположенный по адресу, где вы уста навливаете точку прерывания. Другие процессоры, такие как Intel Itanium, имеют другой размер кода операции, поэтому вам придется сохранять больший объем данных, находящихся по этому адресу.

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

Листинг 4-3. Функция SetBreakpoint из I386CPUHELP.C

 

int CPUHELP_DLLINTERFACE __stdcall

 

 

 

SetBreakpoint ( PDEBUGPACKET

dp

,

 

 

LPCVOID

ulAddr

,

 

 

OPCODE *

pOpCode

)

 

 

{

 

 

 

 

DWORD dwReadWrite = 0 ;

 

 

 

 

BYTE bTempOp = BREAK_OPCODE ;

 

 

 

 

 

 

 

 

 

 

 

 

 

ГЛАВА 4 Поддержка отладки ОС и как работают отладчики Win32

179

 

 

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

 

 

 

}

 

 

 

 

// Читать

код операции по заданному адресу.

 

 

bReadMem = DBG_ReadProcessMemory ( dp >hProcess

,

 

 

 

(LPCVOID)ulAddr

,

 

 

 

&bTempOp

,

 

 

 

sizeof ( BYTE ) ,

 

 

 

&dwReadWrite

) ;

 

ASSERT

( FALSE != bReadMem ) ;

 

 

 

ASSERT

( sizeof ( BYTE ) == dwReadWrite ) ;

 

 

if ( (

FALSE == bReadMem

) ||

 

 

(

sizeof ( BYTE ) != dwReadWrite ) )

 

 

{

 

 

 

 

return ( FALSE ) ;

}

//Не пытаемся ли мы заменить уже имеющийся код команды прерывания? if ( BREAK_OPCODE == 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!!" ) ;

 

см. след. стр.

180 ЧАСТЬ II Производительная отладка

return ( FALSE ) ;

}

// Сохраняем код операции, которую я собираюсь заменить. *pOpCode = (void*)bTempOp ;

bTempOp = BREAK_OPCODE ; dwReadWrite = 0 ;

// Код операции сохранен, устанавливаем

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

bWriteMem = DBG_WriteProcessMemory (

dp >hProcess

,

 

(LPVOID)ulAddr

,

 

(LPVOID)&bTempOp

,

 

sizeof ( BYTE )

,

 

&dwReadWrite

) ;

ASSERT

( FALSE != bWriteMem ) ;

 

 

ASSERT

( sizeof ( BYTE ) == dwReadWrite ) ;

 

if ( (

FALSE == bWriteMem

) ||

 

(

sizeof ( BYTE ) != dwReadWrite ) )

 

{

 

 

 

return ( FALSE ) ;

 

 

}

 

 

 

// Восстанавливаем защиту, которая

была до моего вмешательства.

VERIFY

( DBG_VirtualProtectEx (

dp >hProcess

,

 

 

mbi.BaseAddress

,

 

 

mbi.RegionSize

,

 

 

mbi.Protect

,

 

 

&dwOldProtect

) ) ;

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

//в кэше центрального процессора.

bFlush = DBG_FlushInstructionCache ( dp >hProcess

,

 

 

 

(LPCVOID)ulAddr ,

 

 

 

sizeof ( BYTE )

) ;

ASSERT

(

TRUE

== bFlush ) ;

 

return

(

TRUE

) ;

 

}

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

Соседние файлы в предмете Программирование на C++