Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004
.pdf
ГЛАВА 15 Блокировка в многопоточных приложениях |
553 |
|
|
DWORD dwOldProtect ;
//Возвращение параметра защиты в состояние,
//бывшее до перезаписи указателя на функцию. VERIFY ( VirtualProtect ( mbi_thunk.BaseAddress ,
mbi_thunk.RegionSize |
, |
mbi_thunk.Protect |
, |
&dwOldProtect |
) ) ; |
if ( NULL != pdwHooked )
{
// Увеличение общего числа перехваченных функций. *pdwHooked += 1 ;
}
}
}
// Увеличение указателей на обе таблицы. pOrigThunk++ ;
pRealThunk++ ;
}
// Все OK!
SetLastError ( ERROR_SUCCESS ) ;
return ( TRUE ) ;
}
HookImportedFunctionsByName не должна быть слишком сложной для понимания. После тщательной профилактической проверки каждого параметра я вызываю вспомогательную функцию GetNamedImportDescriptor, выполняющую поиск IMAGE_IM PORT_DESCRIPTOR для запрошенного модуля. Получив указатели на исходную и дей! ствительную IAT, я просматриваю исходную IAT и изучаю каждую функцию, им! портируемую по имени, чтобы узнать, есть ли она в списке paHookArray. Если фун! кция имеется в списке перехватываемых функций, я просто разрешаю запись в область памяти действительной IAT, записываю вместо адреса действительной функции адрес ловушки и возвращаю защиту памяти в исходное состояние. В исходный код BUGSLAYERUTIL.DLL я включил функцию блочного теста для Hook ImportedFunctionsByName, которая поможет вам со всем разобраться, если вы не очень внимательно следили за происходящим.
Теперь, когда вы представляете механизм перехвата импортируемых функций, займемся реализацией остальной части DeadlockDetection.
Детали реализации
Одна из моих основных целей при реализации DeadlockDetection состояла в том, чтобы сделать утилиту максимально ориентированной на использование данных и таблиц. Поразмыслив о том, как выполняется перехват функций DLL, вы пой! мете, что его механизм почти идентичен для всех функций, указанных в табл. 15!1. Функция!ловушка вызывается, определяет, отслеживается ли ее класс функций,
554 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
вызывает действительную функцию и (если для этого класса включено протоко! лирование) записывает информацию и выполняет возврат. Я должен был напи! сать ряд похожих функций!ловушек и хотел сделать их как можно проще. Слож! ные функции!ловушки — плодородная почва для ошибок, которые могут прокра! сться в ваш код, даже когда вы пытаетесь все упростить. Чуть ниже я расскажу про одну неприятную ошибку в коде DeadlockDetection в первом издании этой книги.
Лучше всего показать эту простоту, обсудив написание DLL DeadDetExt. Биб! лиотека DeadDetExt должна иметь три экспортируемых функции. Роль первых двух,
DeadDetExtOpen и DeadDetExtClose, очевидна. Интерес представляет DeadDetProcessEvent, вызываемая каждой функцией!ловушкой при наличии информации для записи. DeadDetProcessEvent принимает единственный параметр — указатель на структуру
DDEVENTINFO:
typedef struct tagDDEVENTINFO
{
// Идентификатор, определяющий содержание оставшейся части структуры.
eFuncEnum |
eFunc |
; |
|
// Индикатор |
предварительного |
или заключительного вызова. |
|
ePrePostEnum |
ePrePost |
; |
|
// Адрес возврата. Он нужен |
для нахождения вызвавшей функции. |
||
DWORD |
dwAddr |
; |
|
// Идентификатор вызвавшего |
потока. |
||
DWORD |
dwThreadId |
; |
|
// Значение, |
возвращаемое при |
заключительных вызовах. |
|
DWORD |
dwRetValue |
; |
|
//Информация о параметрах. Приводите этот элемент к указателю
//на структуру, соответствующую функции, как описано ниже. При доступе
//к параметрам обращайтесь с ними, как со значениями только для чтения.
DWORD dwParams ;
} DDEVENTINFO , * LPDDEVENTINFO ;
Весь вывод для какой!либо функции из листинга 15!1 основан на информа! ции, содержащейся в структуре DDEVENTINFO. Большинство полей DDEVENTINFO гово! рит само за себя, а вот dwParams требует пояснения. Это поле является на самом деле указателем на параметры в том порядке, в котором они расположены в па! мяти.
Вглаве 7 я рассказал о том, как параметры передаются в стек. Напомню, что параметры функций с соглашениями вызова __stdcall и __cdecl передаются спра! ва налево, а стек растет по направлению от старших адресов памяти к младшим. Поле dwParams структуры DDEVENTINFO указывает на последний параметр в стеке, т. е. слева направо. Чтобы обеспечить легкое преобразование dwParams, я прибегнул к приведению типов.
Вфайле DEADLOCKDETECTION.H содержатся объявления typedef, описываю! щие списки параметров каждой перехватываемой функции. Например, если бы поле eFunc соответствовало значению eWaitForSingleObjectEx, то для получения параметров нужно было бы привести тип dwParams к LPWAITFORSINGLEOBJECTEX_PARAMS. Чтобы увидеть все это творческое приведение типов в действии, изучите код биб! лиотеки TEXTFILEDDEXT.DLL (см. CD, прилагаемый к книге).
ГЛАВА 15 Блокировка в многопоточных приложениях |
555 |
|
|
Хотя обработка вывода относительно проста, сбор информации может оказаться сложным. Мне требовалось, чтобы DeadlockDetection перехватывала функции синхронизации из табл. 15!1, но я не хотел, чтобы функции!ловушки изменяли поведение действительных функций. Я также хотел получать параметры и возвра! щаемые значения и с легкостью писать функции!ловушки на C/C++. Я провел за отладчиком и дизассемблером немало времени, пока мне удалось сделать это правильно.
Первоначально я сделал все функции!ловушки сквозными (pass!through func! tion), чтобы они вызывали действительные функции непосредственно. Этот под! ход работал отлично. Затем я поместил параметры функций и возвращаемые ими значения в локальные переменные. Получение возвращаемого значения из дей! ствительной функции оказалось простым, но из!за того, что я начал реализацию DeadlockDetection на Visual C++ 6, у меня не было чистого способа получения адресов возврата в моих функциях!ловушках C/C++. Visual C++ .NET поддержива! ет внутреннюю (intrinsic) функцию _ReturnAddress, но в Visual C++ 6 такой возмож! ности не было. Мне нужно было значение DWORD прямо перед текущим указателем стека. Увы, в обычном C/C++ пролог функции уже выполнил бы все свои действия к тому времени, когда я смог бы получить управление, и указатель стека имел бы не то значение, которое мне было нужно.
Вы можете подумать, что указатель стека — это просто смещение, определяе! мое числом локальных переменных, но это не всегда так. Компилятор Visual C++ выполняет великолепную оптимизацию, так что при различных конфигурациях флагов оптимизации указатель стека может иметь разные значения. Так, когда вы объявляете переменную как локальную, компилятор может оптимизировать ра! боту с ней, сохранив в регистре, из!за чего она даже не появится в стеке.
Мне нужен был гарантированный способ получения указателя стека незави! симо от параметров оптимизации. В этот момент я начал думать, почему бы не объявить функции!ловушки как __declspec(naked) и не создать собственные про! лог и эпилог? Это дало бы мне полный контроль над регистром ESP независимо от параметров оптимизации. Кроме того, это облегчило бы и получение адреса возврата и параметров, так как они находятся по смещениям ESP+04h и ESP+08h соответственно. Помните, что мои пролог и эпилог не представляют собой ниче! го сверхъестественного, поэтому я все же выполняю обычные команды PUSH EBP и MOV EBP, ESP в прологе и MOV ESP, EBP и POP EBP в эпилоге.
Решив объявлять все функции!ловушки как __declspec(naked), я написал для обработки пролога и эпилога два макроса: HOOKFN_PROLOG и HOOKFN_EPILOG. Кроме того, я заблаговременно объявил в HOOKFN_PROLOG некоторые общие локальные переменные, нужные всем функциям!ловушкам. В число этих переменных вошли значение последней ошибки, dwLastError, и структура информации о событии, stEvtInfo, передаваемая в DLL DeadDetExt. Переменная dwLastError — просто еще один при! знак состояния, который мне нужно сохранять при перехвате функций.
При помощи функции SetLastError Windows API возвращает специальный код ошибки, предоставляя более подробную информацию в случае неудачи функции. Этот код ошибки может быть настоящим благословением, потому что он сооб! щает о причине неудачи API!функции. Так, если GetLastError возвратит 122, вы будете знать, что причиной ошибки стал недостаточный размер переданного в функцию
556 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
буфера. Все возвращаемые ОС коды ошибок указаны в файле WINERROR.H. Про! блема с функциями!ловушками в том, что во время своего выполнения они могут перезаписать значение последенй ошибки. Это может привести к катастрофе, если значение последней ошибки используется вашей программой.
Если при вызове CreateEvent вы хотите узнать, был ли возвращаемый описатель создан или просто открыт, вы также можете использовать код последней ошиб! ки: если CreateEvent просто открыла описатель, код будет иметь значение ERROR_AL READY_EXISTS. Одно из важнейших правил перехвата функций гласит, что вы не можете изменять ожидаемое поведение функции, поэтому сразу же после вызова действительной функции я должен был вызыать GetLastError, чтобы моя функция! ловушка могла правильно установить код последней ошибки, возвращаемый дей! ствительной функцией. Общее правило написания функций!ловушек таково: сразу после вызова действительной функции нужно вызывать GetLastError, а непосред! ственно перед выходом из ловушки устанавливать код ошибки при помощи Set LastError.
Стандартный вопрос отладки
Если теперь доступна ReturnAddress, почему вы не использовали ее, а пошли на все эти проблемы?
Когда пришло время обновить DeadlockDetection для второго издания этой книги, я думал немного упростить свою жизнь и изменить макрос HOOK FN_PROLOG, чтобы он использовал новую внутреннюю функцию _ReturnAddress. Это значио бы, что я могу избавиться от объявлений naked, привести функ! ции к более нормальному виду и не создавать собственные пролог и эпи! лог. Однако существующие макросы дают мне одно большое преимущество: я могу обращаться с параметрами, как с блоками памяти, и передавать их прямо в функцию вывода. Если б я применял стандартные функции, мне нужно было бы выполнять странное приведение типов для достижения того же результата. Кроме того, у меня был самый весомый аргумент: имевший! ся код работал очень хорошо, и мне не хотелось его переписывать. Поэто! му я оставил функции с соглашением naked прежними.
В этот момент я подумал, что все, кроме тестирования, сделано. Увы, во время первого теста я нашел ошибку: между вызовами ловушек я не сохранял регистры ESI и EDI, потому что в документации к встроенному ассемблеру сказано, что их сохранять не требуется. После решения этой проблемы казалось, что Deadlock! Detection работает прекрасно. Однако когда я начал сравнивать регистры до, во время и после вызовов функций, я заметил, что я не возвращаю значения, сохра! няемые действительными функциями в EBX, ECX и EDX и, что еще хуже, в регистре флагов. Хотя я не видел в этом никаких проблем и в документации говорилось, что эти регистры сохранять не требуется, я все же был озабочен тем, что мои функции!ловушки изменяли состояние приложения. Для сохранения значений регистров после вызовов действительных функций я объявил структуру REGSTATE, чтобы можно было восстанавливать регистры по возвращении из функции!ловуш! ки. Для сохранения и восстановления регистров я создал два дополнительных
558 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
Все функции ловушки объявляются с соглашением __declspec(naked), поэтому я должен сам написать пролог и эпилог. Я должен предоставить собственные пролог и эпилог по нескольким причинам.
1.Функции, написанные на C, не позволяют контролировать использование регистров и сохранение исходных регистров компилятором.
Отсутствие контроля над регистрами означает, что получить адрес возврата почти невозможно. Для проекта DeadlockDetection адрес возврата очень важен.
2.Я хотел передавать параметры в функцию обработки из DLL расширения, не копируя при каждом вызове функции большие объемы данных.
3.Так как почти все функции ловушки ведут себя похожим образом, я могу присвоить значения общим переменным, нужным во всех функциях.
4.Функции ловушки не должны изменять возвращаемые значения, в том числе значение, возвращаемое GetLastError. Собственные пролог
и эпилог позволяют мне значительно упростить возвращение правильного значения. Кроме того, я должен восстанавливать значения регистров в то состояние, в каком они были после вызова действительной функции.
Базовая функция ловушка требует только двух макросов: HOOKFN_STARTUP и HOOKFN_SHUTDOWN.
Как вы можете видеть, это здорово облегчает работу!
BOOL NAKEDDEF DD_InitializeCriticalSectionAndSpinCount (
LPCRITICAL_SECTION lpCriticalSection,
DWORD |
dwSpinCount |
) |
|
{ |
|
|
|
HOOKFN_STARTUP ( eInitializeCriticalSectionAndSpinCount |
, |
|
|
DDOPT_CRITSEC |
|
, |
|
0 |
|
) ; |
|
InitializeCriticalSectionAndSpinCount ( lpCriticalSection , |
|
||
|
dwSpinCount |
) ; |
|
HOOKFN_SHUTDOWN ( 2 , DDOPT_CRITSEC ) ;
}
Если надо выполнить специальную обработку и вы не хотите делать чего то, что нельзя выполнить, используя обычные макросы, вам помогут макросы:
HOOKFN_PROLOG
REAL_FUNC_PRE_CALL
REAL_FUNC_POST_CALL
HOOKFN_EPILOG
Пример функции, использующей указанные макросы:
HMODULE NAKEDDEF DD_LoadLibraryA ( LPCSTR lpLibFileName )
{
// Все локальные переменные должны быть объявлены
560ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
//вызовами функций в отладочных компоновках.
//////////////////////////////////////////////////////////////////////*/
#ifdef _DEBUG
#define SAVE_ESI() |
__asm PUSH |
ESI |
#define RESTORE_ESI() |
__asm POP |
ESI |
#else |
|
|
#define SAVE_ESI() |
|
|
#define RESTORE_ESI() |
|
|
#endif |
|
|
/*////////////////////////////////////////////////////////////////////// // Общий пролог для всех функций DD_*.
//////////////////////////////////////////////////////////////////////*/
#define HOOKFN_PROLOG() |
|
|
|
\ |
|
/* Все функции ловушки автоматически получают |
*/\ |
||||
/* три одинаковых локальных |
переменных. |
*/\ |
|||
DDEVENTINFO |
stEvtInfo |
; |
/* |
Информация о событии для функции. |
*/\ |
DWORD |
dwLastError |
; |
/* |
Значение последней ошибки. |
*/\ |
REGSTATE |
stRegState |
; |
/* |
Состояние регистров, нужное для их |
*/\ |
|
|
|
/* |
правильного восстановления. |
*/\ |
{ |
|
|
|
|
\ |
__asm PUSH |
EBP |
|
/* |
Всегда явно сохраняйте EBP. |
*/\ |
__asm MOV |
EBP , ESP |
|
/* |
Настройка кадра стека. |
*/\ |
__asm MOV |
EAX , ESP |
|
/* |
Получение указателя стека для подсчета*/\ |
|
|
|
|
/* |
адреса возврата и адреса параметров. |
*/\ |
SAVE_ESI ( ) |
|
|
/* |
Сохранение ESI в отлад. компоновках. |
*/\ |
__asm SUB |
ESP , __LOCAL_SIZE |
/* Место для локальных переменных. |
*/\ |
||
__asm ADD |
EAX , 04h + 04h |
/* |
Нужно учесть команду PUSH EBP |
*/\ |
|
|
|
|
/* |
и адрес возврата. |
*/\ |
|
|
|
/* |
Сохранение начала параметров в стеке. */\ |
|
__asm MOV |
[stEvtInfo.dwParams] , EAX |
\ |
|||
__asm SUB |
EAX , 04h |
|
/* |
Вернуться к адресу возврата. |
*/\ |
__asm MOV |
EAX , [EAX] |
|
/* |
Теперь EAX содержит адрес возврата. |
*/\ |
|
|
|
/* |
Сохранение адреса возврата. |
*/\ |
__asm MOV |
[stEvtInfo.dwAddr] |
, EAX |
\ |
||
__asm MOV |
dwLastError , 0 |
/* |
Инициализация dwLastError. |
*/\ |
|
|
|
|
/* |
Инициализация информации о событии. |
*/\ |
__asm MOV |
[stEvtInfo.eFunc] , eUNINITIALIZEDFE |
\ |
|||
__asm MOV |
[stRegState.dwEDI] |
, EDI /* Сохранение двух регистров, |
*/\ |
||
__asm MOV |
[stRegState.dwESI] |
, ESI /* которые нужно сохранять |
*/\ |
||
|
|
|
|
/* между вызовами функций. |
*/\ |
} |
|
|
|
|
|
/*//////////////////////////////////////////////////////////////////////
//Общий эпилог для всех функций DD_*. INumParams — это число
//параметров функции, используемое для восстановления
//правильного состояния стека после вызова ловушки.
//////////////////////////////////////////////////////////////////////*/
#define HOOKFN_EPILOG(iNumParams) |
\ |
{ |
\ |
562 |
ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода |
|||||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
\ |
dwLastError = GetLastError ( ) ; |
|
/* Сохр. кода последней ошибки.*/\ |
||||
{ |
|
|
|
|
|
\ |
__asm MOV |
EAX , [stRegState.dwEAX] |
|
/* Восстановление EAX |
*/\ |
||
|
|
|
|
|
/* Установка возвращаемого |
*/\ |
|
|
|
|
|
/* значения. |
*/\ |
__asm MOV |
[stEvtInfo.dwRetValue] , EAX |
\ |
||||
} |
|
|
|
|
|
|
/*////////////////////////////////////////////////////////////////////// |
||||||
// Удобный макрос для заполнения структуры информации о событии |
|
|||||
//////////////////////////////////////////////////////////////////////*/ |
||||||
#define FILL_EVENTINFO(eFn) |
|
\ |
|
|||
|
stEvtInfo.eFunc |
= eFn |
; |
\ |
|
|
|
stEvtInfo.ePrePost |
= ePostCall ; |
\ |
|
||
|
stEvtInfo.dwThreadId = GetCurrentThreadId ( ) |
|
||||
/*//////////////////////////////////////////////////////////////////////
//Макросы для второй версии программы, ЗНАЧИТЕЛЬНО
//облегчающие определение функций ловушек
//////////////////////////////////////////////////////////////////////*/
//Объявляйте его в начале каждой функции ловушки.
//eFunc значение перечисления, соответствующее функции.
//SynchClassType – значение флага DDOPT_*, указывающее на класс
// |
обрабатываемой вами функции. |
|
// bRecordPreCall – выполняет запись информации об этой функции. |
|
|
#define HOOKFN_STARTUP(eFunc,SynchClassType,bRecordPreCall) |
\ |
|
|
HOOKFN_PROLOG ( ) ; |
\ |
|
if ( TRUE == DoLogging ( SynchClassType ) ) |
\ |
|
{ |
\ |
|
FILL_EVENTINFO ( eFunc ) ; |
\ |
|
if ( TRUE == (int)bRecordPreCall ) |
\ |
|
{ |
\ |
|
stEvtInfo.ePrePost = ePreCall ; |
\ |
|
ProcessEvent ( &stEvtInfo ) ; |
\ |
|
} |
\ |
|
} |
\ |
|
REAL_FUNC_PRE_CALL ( ) ; |
|
/*//////////////////////////////////////////////////////////////////////
//Макрос завершения функции ловушки.
//iNuMParams число параметров, переданных функции.
//SynchClassType – класс функции синхронизации.
//////////////////////////////////////////////////////////////////////*/
#define HOOKFN_SHUTDOWN(iNumParams,SynchClass) |
\ |
REAL_FUNC_POST_CALL ( ) ; |
\ |
if ( TRUE == DoLogging ( SynchClass ) ) |
\ |
{ |
\ |
stEvtInfo.ePrePost = ePostCall ; |
\ |
ProcessEvent ( &stEvtInfo ) ; |
\ |
