
Роббинс Д. - Отладка приложений для Microsoft .NET и Microsoft Windows - 2004
.pdfГЛАВА 15 Блокировка в многопоточных приложениях |
543 |
|
|
[Initialization]
; Единственное обязательное значение, имя файла DeadDetExt,
;который будет обрабатывать вывод. ExtDll = "TextFileDDExt.dll"
;Если этот параметр равен 1, DeadlockDetection будет
;выполнять инициализацию в собственной функции DllMain,
;чтобы протоколирование могло быть начато как можно раньше. StartInDllMain = 0
;Если StartInDllMain равняется 1, этот ключ задает первоначальные
;параметры DeadlockDetection. В нем указываются значения флагов DDOPT_*.
;InitialOpts = 0
;Список модулей, игнорируемых при перехвате функций
;синхронизации. IMM32.DLL — это DLL Input Method Editor (редактор
;методов ввода), которую Windows XP загружает во все процессы.
;Создавайте список в последовательном порядке, начиная с номера 1. [IgnoreModules]
Ignore1=IMM32.DLL
Как вы можете увидеть по некоторым параметрам INI, DeadlockDetection мо жет выполнять инициализацию при простом вызове LoadLibrary. Для проактивной отладки было бы неплохо, чтобы во время инициализации ваше приложение проверяло конкретный раздел реестра или переменную среды и вызывало при их наличии LoadLibrary с указанным именем DLL. Благодаря этому вам не нужно было бы использовать условную компиляцию, и у вас были бы средства чистой загруз ки DLL в свое адресное пространство. Конечно, это подразумевает, что загружае мые вами таким образом DLL должны полностью инициализироваться в собствен ных функциях DllMain и не должны требовать вызова каких нибудь других экс портируемых функций.
Чтобы вы могли указывать параметры инициализации DeadlockDetection в своем коде, а не при помощи файла INI, вам нужно включить в свою программу файл DEADLOCKDETECTION.H и скомпоновать ее с библиотекой DEADLOCKDETEC TION.LIB. Если вы хотите инициализировать DeadlockDetection сами, вызовите в нужном месте функцию OpenDeadlockDetection, которая принимает единственный параметр — первоначальные флаги протоколирования. Все флаги DDOPT_* указа ны в табл. 15 2. Вызывать OpenDeadlockDetection следует до того, как ваша программа начнет создавать потоки, чтобы вы могли записать всю важную информацию об объектах синхронизации.
Изменять параметры протоколирования можно в любой момент при помощи функции SetDeadlockDetectionOptions. Она принимает тот же набор объединенных при помощи операции ИЛИ флагов, что и OpenDeadlockDetection. Чтобы увидеть текущие параметры, вызовите GetDeadlockDetectionOptions. Во время выполнения программы можете изменять параметры протоколирования сколько вашей душе угодно. Для приостановления и возобновления протоколирования служат функ ции SuspendDeadlockDetection и ResumeDeadlockDetection соответственно.

544 |
ЧАСТЬ IV |
Мощные средства и методы отладки неуправляемого кода |
|
||
Табл. 15-2. Параметры протоколирования DeadlockDetection |
||
|
|
|
Флаг |
|
Ограничивает протоколирование |
DDOPT_WAIT |
|
Функциями ожидания |
DDOPT_THREADS |
Функциями работы с потоками |
|
DDOPT_CRITSEC |
Функциями работы с критическими секциями |
|
DDOPT_MUTEX |
Функциями работы с мьютексами |
|
DDOPT_SEMAPHORE |
Функциями работы с семафорами |
|
DDOPT_EVENT |
Функциями работы с событиями |
|
DDOPT_ALL |
|
Регистрирует все перехваченные функции |
|
|
|
Вместе с исходным кодом DeadlockDetection вы можете найти на диске мою библиотеку DeadDetExt под названием TEXTFILEDDEXT.DLL. Это относительно простое расширение записывает всю информацию в текстовый файл. При запус ке DeadlockDetection вместе с TEXTFILEDDEXT.DLL расширение создает текстовый файл в том же каталоге, в котором находится выполняемая программа. Текстовый файл будет иметь имя выполняемой программы с расширением .DD. Например, при запуске программы DDSIMPTEST.EXE итоговый файл будет назван DDSIMP TEST.DD. Вот пример вывода, сгенерированного TEXTFILEDDEXT.DLL (листинг 15 1).
Листинг 15-1. Данные, выводимые утилитой DeadlockDetection при помощи TEXTFILEDDEXT.DLL
|
TID |
Ret Addr |
C/R Ret Value |
Function & Params |
|
|
|
0x00000DF8 |
[0x004011B2] (R) 0x00000000 |
InitializeCriticalSection 0x00404150 |
|
||
|
0x00000DF8 |
[0x004011CC] (R) 0x000007C0 |
CreateEventA 0x00000000, 1, 0, |
|
||
|
|
|
|
0x004040F0 [The Event Name] |
|
|
|
0x00000DF8 |
[0x004011EF] (R) 0x000007BC |
CreateThread 0x00000000, 0x00000000, |
|
||
|
|
|
|
0x00401000, |
0x00000000, |
|
|
|
|
|
0x00000000, 0x0012FF5C |
|
|
|
0x00000DF8 |
[0x00401212] (R) 0x000007B8 |
CreateThread 0x00000000, 0x00000000, |
|
||
|
|
|
|
0x004010BC, |
0x00000000, |
|
|
|
|
|
0x00000000, 0x0012FF5C |
|
|
|
0x00000DF8 |
[0x00401229] (C) |
EnterCriticalSection 0x00404150 |
|
||
|
0x000000A8 |
[0x00401030] (C) |
EnterCriticalSection 0x00404150 |
|
||
|
0x00000F04 |
[0x004010F3] (R) 0x000007B0 |
OpenEventA 0x001F0003, 0, 0x004040BC |
|
||
|
|
|
|
[The Event Name] |
|
|
|
0x00000DF8 |
[0x00401229] (R) 0x00000000 |
EnterCriticalSection 0x00404150 |
|
||
|
0x00000DF8 |
[0x0040123E] (C) |
WaitForSingleObject 0x000007C0, |
|
||
|
|
|
|
INFINITE |
|
|
|
0x00000F04 |
[0x00401121] (C) |
EnterCriticalSection 0x00404150 |
|
||
|
|
|
|
|
|
|
Заметьте: сведения об именах функций и их параметрах представлены в лис тинге 15 1 на нескольких строках, чтобы они помещались на странице. Инфор мация выводится в таком порядке.
1.Идентификатор выполняемого потока.
2.Адрес возврата, показывающий, какая из ваших функций вызвала функцию синхронизации. При помощи утилиты CrashFinder из главы 12 можно просмот реть адреса возврата и узнать, как вы оказались в ситуации блокировки.
ГЛАВА 15 Блокировка в многопоточных приложениях |
545 |
|
|
3.Индикатор вызова/возврата, который помогает определить действия, проис шедшие до или после конкретных функций.
4.Возвращаемое функцией значение, если ваша программа его сообщает.
5.Имя функции синхронизации.
6.Список параметров функции синхронизации. Значения в квадратных скобках описывают данные в понятной людям форме. Особое внимание я уделил вы воду строковых значений, но вы легко реализуете вывод более подробной ин формации, скажем, отдельных флагов.
Если при запуске вашей программы она заблокируется, завершите процесс и изучите файл вывода, чтобы узнать, какая функция синхронизации была вызвана последней. Для обновления информации TEXTFILEDDEXT.DLL сбрасывает файловые буферы в файл при каждом вызове функций WaitFor*, EnterCriticalSection и TryEnter CriticalSection.
Предупреждение: если вы включите полное протоколирование всех функций, почти мгновенно будут созданы очень большие файлы. Так, создав пару потоков при помощи приложения MTGDI из числа примеров к Visual C++, я за минуту или две сгенерировал 11 Мбайтный текстовый файл.
Реализация DeadlockDetection
Как видите, работать с DeadlockDetection довольно просто. Однако под просто той ее использования скрывается весьма сложная реализация. В первую очередь я хочу рассказать про перехват функций.
Перехват импортируемых функций
Способов перехвата вызываемых программой функций много. Можно выполнять поиск всех команд CALL и заменять их операнды собственным адресом, но этот подход сложен и подвержен ошибкам. К счастью, в случае DeadlockDetection мне нужно перехватывать импортируемые функции, поэтому их гораздо легче обра батывать, чем команды CALL.
Импортируемая функция — это функция, которая располагается в DLL. Напри мер, вызывая OutputDebugString, ваша программа вызывает функцию, находящую ся в KERNEL32.DLL. Кода я только начал писать программы для Win32, я думал, что вызов импортируемых функций аналогичен вызовам любых других функций: команда CALL или команда перехода передает управление по нужному адресу и начинает выполнение импортируемой функции. Единственное различие могло бы состоять в том, что в случае импортируемой функции загрузчик программ ОС должен был бы просмотреть исполняемый файл и исправить адреса, чтобы они соответствовали той области памяти, в которую будет загружена вызываемая DLL. Однако, взглянув на действительную реализацию вызовов импортируемых функ ций, я был поражен ее простотой и элегантностью.
Недостаток только что описанного мной подхода станет очевидным, если учесть наличие огромного числа API функций и возможность вызова одной и той же функции во многих местах. Если бы загрузчик должен был найти и исправить каждый вызов, скажем, функции OutputDebugString, загрузка программы могла бы продолжаться вечность. Даже если б компоновщик создавал таблицу, где указы

546 ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
вал бы место каждого вызова OutputDebugString, загрузка программы была бы му чительно медленной из за огромного объема работы, связанной с циклами и за писью в память.
Так как же загрузчик сообщает программе о том, где находится импортируе мая функция? Решение чертовски умно. Представив, куда направляются вызовы OutputDebugString, вы вскоре поймете, что каждый вызов должен обращаться к одному и тому же адресу памяти, по которому OutputDebugString была загружена. Конеч но, ваша программа не может знать этот адрес заранее, поэтому все вызовы Output DebugString выполняются посредством единственного косвенного адреса. При за грузке вашего исполняемого файла и нужных ему DLL загрузчик корректирует этот единственный косвенный адрес, чтобы он соответствовал итоговому адресу за грузки OutputDebugString. Чтобы косвенная адресация работала, компилятор гене рирует при каждом вызове импортируемой функции переход к этому косвенно му адресу. Косвенный адрес хранится в исполняемом файле в разделе .idata (или import). Если вы импортируете функцию, объявляя ее как __declspec(dllimport), то вместо косвенного перехода будет косвенный вызов, что экономит несколько команд на каждом вызове функции.
Чтобы установить ловушку для импортируемой функции, нужно отыскать в исполняемом файле раздел импорта, найти адрес нужной функции и заменить его адресом функции ловушки. Вам может показаться, что это потребует большого объема работы, однако все не так уж плохо, так как формат файлов Win32 Portable Executable (PE) организован очень разумно.
Метод установки ловушки для импортируемых функций описан в главе 10 ве ликолепной книги Мэтта Питрека (Matt Pietrek) «Windows 95 System Programming Secrets» (IDG Books, 1995). Мэтт просто ищет для модуля раздел импорта и про сматривает в цикле импортируемые функции, используя значение, возвращаемое функцией GetProcAddress. Обнаружив нужную функцию, он перезаписывает ее первоначальный адрес адресом функции ловушки.
С момента издания книги Мэтта в 1995 г. в мире программирования произош ли два небольших изменения. Во первых, когда Мэтт писал свою книгу, большин ство программистов не объединяло раздел импорта с другими разделами PE файла. Поэтому, если раздел импорта располагается в памяти, доступной только для чте ния, попытка перезаписи адреса функции приведет к нарушению доступа. Чтобы избежать этой ошибки, я перед записью адреса функции ловушки устанавливаю защиту виртуальной памяти в состояние разрешения чтения и записи. Вторая, чуть более сложная проблема связана с невозможностью перехвата в некоторых слу чаях импортируемых функций в Microsoft Windows Me. Очень многие спрашива ют меня о перехвате функций, поэтому я решил реализовать его и для Windows Me и рассказать, что происходит в этой ОС.
Работая с DeadlockDetection, вам хотелось бы иметь возможность перенаправ ления функций работы с потоками при любом запуске своей программы, даже когда она выполняется под управлением отладчика. Однако установка ловушек под управ лением отладчика может представлять проблему, хотя на первый взгляд так не кажется. Получив адрес функции при помощи GetProcAddress в Windows XP или при выполнении программы в Windows Me вне отладчика, вы всегда сможете найти этот адрес в разделе импорта. Но в Windows Me адрес, возвращаемый функцией
ГЛАВА 15 Блокировка в многопоточных приложениях |
547 |
|
|
GetProcAddress в программе, выполняемой под управлением отладчика, отличает ся от адреса, получаемого при выполнении вне отладчика. В первом случае GetProc Address на самом деле возвращает отладочный шлюз (debug thunk) — специаль ную оболочку для действительного вызова.
Отладочный шлюз нужен потому, что Windows Me не выполняет копирование при записи (copy on write) для адресов, расположенных выше 2 Гб. Копирование при записи предполагает, что при записи в страницу разделяемой памяти ОС делает копию страницы и предоставляет ее процессу, выполняющему запись. Обычно Windows Me и Windows XP следуют одинаковым правилам, и все работает отлич но. Однако для разделяемой памяти, находящейся выше 2 Гб, где в Windows Me загружаются все DLL системы, Windows Me не выполняет копирования при запи си. Это значит, что при изменении памяти в DLL системы в результате установки точки прерывания или исправления функции изменение произойдет для всех процессов ОС, что вызовет ее крах, если измененная область будет использоваться другим процессом. Поэтому Windows Me прилагает серьезные усилия, чтобы по мешать вам исказить эту память.
Отладочный шлюз, возвращаемый GetProcAddress при выполнении под отлад чиком, — это средство, при помощи которого Windows Me предотвращает попытки отладки системных функций, расположенных выше 2 Гб. В целом отсутствие ко пирования при записи большинство программистов волновать не должно; оно представляет проблему только для тех, кто разрабатывает отладчики или желает корректно перехватывать функции независимо от того, выполняется программа под отладчиком или нет.
К счастью, получить действительный адрес импортируемой функции не так сложно — просто для этого требуется чуть поработать, избегая при этом GetProc Address. Структура IMAGE_IMPORT_DESCRIPTOR в PE файле, которая содержит всю ин формацию о функциях, импортируемых из конкретной DLL, имеет указатели на два массива в исполняемом файле — таблицы адресов импортируемых функций (import address table, IAT) (иногда их называют массивами данных шлюзов — thunk data array). Первый указатель указывает на действительную IAT, который загруз чик программ корректирует при загрузке исполняемого файла, второй — на ис ходную IAT, содержащую адреса импортируемых функций и не изменяемую заг рузчиком. Итак, для обнаружения действительного адреса импортируемой функ ции нужно просто найти ее в исходной IAT; после этого надо записать адрес ло вушки в соответствующий элемент действительной IAT, используемой програм мой. Благодаря этому ловушка будет работать всегда независимо от того, где она вызывается.
Всю работу, связанную с установкой ловушек, выполняет моя функция HookIm portedFunctionsByName (табл. 15 3). Так как я хотел сделать перехват функций как можно более общим, я реализовал возможность одновременного перехвата не скольких функций, импортируемых из одной DLL. Как можно догадаться по име ни, HookImportedFunctionsByName перехватывает только те функции, которые импор тируются по имени. Для перехвата функций, экспортируемых по ординалу, я на писал функцию HookOrdinalExport, но я не буду рассматривать ее в этой книге.

548 |
ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода |
|
|
Табл. 15-3. Описание параметров функции HookImportedFunctionsByName |
|
|
|
Параметр |
Описание |
hModule |
Модуль, в котором находятся перехватываемые импортируемые |
|
функции. |
szImportMod Имя модуля импортируемых функций.
uiCount Число перехватываемых функций. Этот параметр равен числу элементов массивов paHookArray и paOrigFuncs.
paHookArray Массив указателей на структуры дескрипторов функций, в котором указываются перехватываемые функции. Он не обязан быть сортиро ванным по порядку имен szFunc (хотя это было бы мудрым решением, потому что в будущем я могу реализовать лучший алгоритм поиска). Если конкретный указатель pProc имеет значение NULL, HookImportedFunctionsByName пропускает этот элемент. Структура каждого элемента в массиве paHookArray содержит имя перехватываемой функ ции и указатель на новую функцию ловушку. Чтобы вы в любой мо мент могли установить/удалить ловушку, HookImportedFunctionsByName возвращает все исходные адреса импортируемых функций.
paOrigFuncs Массив первоначальных адресов функций, перехватываемых при по мощи HookImportedFunctionsByName. Если функция не была перехвачена, соответствующий ей элемент будет иметь значение NULL.
pdwHooked Возвращает число перехваченных функций из массива paHookArray.
В листинге 15 2 показана функция HookImportedFunctionsByNameA, но в своем коде вы будете вызывать HookImportedFunctionsByName: этот макрос выполняет отображение между форматами ANSI и Unicode. Однако, поскольку все имена в разделе IAT хранятся в формате ANSI, я реализовал и функцию HookImportedFunctionsByNameW, которая просто преобразует соответствующие параметры в формат ANSI и вызы вает HookImportedFunctionsByNameA.
Листинг 15-2. Функция HookImportedFunctionsByNameA из файла HOOKIMPORTEDFUNCTIONBYNAME.CPP
BOOL BUGSUTIL_DLLINTERFACE __stdcall
HookImportedFunctionsByNameA ( HMODULE |
hModule |
, |
LPCSTR |
szImportMod |
, |
UINT |
uiCount |
, |
LPHOOKFUNCDESC |
paHookArray , |
|
PROC * |
paOrigFuncs , |
|
LPDWORD |
pdwHooked |
) |
{ |
|
|
// Проверка параметров. |
|
|
ASSERT ( FALSE == IsBadReadPtr ( hModule |
, |
|
sizeof ( IMAGE_DOS_HEADER ) |
) ) ; |
ASSERT ( FALSE == IsBadStringPtrA ( szImportMod , MAX_PATH ) ) ; ASSERT ( 0 != uiCount ) ;
ASSERT ( NULL != paHookArray ) ;
ASSERT ( FALSE == IsBadReadPtr ( paHookArray ,
sizeof (HOOKFUNCDESC) * uiCount ));

550ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода
//Здесь я проверяю, расположена ли данная системная DLL выше 2 Гб,
//в случае чего Windows 98 не позволит ее скорректировать.
if ( ( FALSE == IsNT ( ) ) && ( (DWORD_PTR)hModule >= 0x80000000 ) )
{
SetLastErrorEx ( ERROR_INVALID_HANDLE , SLE_ERROR ) ; return ( FALSE ) ;
}
//СООБРАЖЕНИЯ ПО ПОВОДУ УЛУЧШЕНИЯ ПРОГРАММЫ
//Следует ли проверять каждый элемент массива
//перехватываемых фукнций в заключительных компоновках?
if ( NULL != paOrigFuncs )
{
// Присвоение всем элементам массива paOrigFuncs значения NULL. memset ( paOrigFuncs , NULL , sizeof ( PROC ) * uiCount ) ;
}
if ( NULL != pdwHooked )
{
// Присвоение числу перехваченных функций значения 0. *pdwHooked = 0 ;
}
// Получение специфического дескриптора импорта. PIMAGE_IMPORT_DESCRIPTOR pImportDesc =
GetNamedImportDescriptor ( hModule , szImportMod ); if ( NULL == pImportDesc )
{
// Запрошенный модуль не был импортирован. Не возвращать ошибку. return ( TRUE ) ;
}
//ИСПРАВЛЕННАЯ ОШИБКА. Спасибо Аттиле Шепезвари (Attila Szepesvary)!
//Проверка того, что первый шлюз и исходный первый шлюз
//не равны NULL. Исходный первый шлюз может быть нулевым
//дескриптором импорта, что вызвало бы крах этой функции.
if ( ( NULL == pImportDesc >OriginalFirstThunk |
) |
|| |
( NULL == pImportDesc >FirstThunk |
) |
) |
{ |
|
|
//Я возвращаю TRUE, потому что это аналогично случаю,
//в котором запрошенный модуль не был импортирован.
//Все в порядке!
SetLastError ( ERROR_SUCCESS ) ; return ( TRUE ) ;
}
//Получение информации об исходном шлюзе для этой DLL.
//Я не могу использовать информацию, хранимую
//в pImportDesc >FirstThunk, так как загрузчик уже изменил
//этот массив во время коррекции импортируемых функций.

552 |
ЧАСТЬ IV Мощные средства и методы отладки неуправляемого кода |
|||
|
|
|
|
|
|
|
|
|
|
|
for ( UINT i = 0 ; i < uiCount ; i++ ) |
|
|
|
|
{ |
|
|
|
|
if ( ( paHookArray[i].szFunc[0] == |
|
|
|
|
pByName >Name[0] ) && |
|||
|
( 0 == strcmpi ( paHookArray[i].szFunc , |
|
|
|
|
(char*)pByName >Name |
) |
) |
) |
|
{ |
|
|
|
|
// Если адрес функции равен NULL, выполняется выход; |
|||
|
// в противном случае функция перехватывается. |
|
||
|
if ( NULL != paHookArray[ i ].pProc ) |
|
|
|
|
{ |
|
|
|
|
bDoHook = TRUE ; |
|
|
|
|
} |
|
|
|
|
break ; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
if ( TRUE == bDoHook ) |
|
|
|
|
{ |
|
|
|
|
// Я обнаружил функцию, которую нужно перехватить. Теперь, |
|||
|
// прежде чем перезаписать указатель на функцию, я должен |
|||
|
// изменить защиту памяти, разрешив запись в нее. Заметьте, |
|||
|
// что я выполняю запись в область действительного шлюза! |
|||
|
MEMORY_BASIC_INFORMATION mbi_thunk ; |
|
|
|
|
VirtualQuery ( pRealThunk |
|
|
, |
|
&mbi_thunk |
|
|
, |
|
sizeof ( MEMORY_BASIC_INFORMATION ) ) ; |
|||
|
if ( FALSE == VirtualProtect ( mbi_thunk.BaseAddress , |
|||
|
mbi_thunk.RegionSize |
, |
||
|
PAGE_READWRITE |
|
|
, |
|
&mbi_thunk.Protect |
|
)) |
|
|
{ |
|
|
|
|
ASSERT ( !"VirtualProtect failed!" ) ; |
|
|
|
|
SetLastErrorEx ( ERROR_INVALID_HANDLE , SLE_ERROR ); |
|||
|
return ( FALSE ) ; |
|
|
|
|
} |
|
|
|
|
// Сохранение исходного адреса в случае надобности. |
|
||
|
if ( NULL != paOrigFuncs ) |
|
|
|
|
{ |
|
|
|
|
paOrigFuncs[i] = |
|
|
|
|
(PROC)((INT_PTR)pRealThunk >u1.Function) ; |
|
||
|
} |
|
|
|
|
// Перехват функции. |
|
|
|
|
DWORD_PTR * pTemp = (DWORD_PTR*)&pRealThunk >u1.Function ; |
|||
|
*pTemp = (DWORD_PTR)(paHookArray[i].pProc); |
|
|
|
|
|
|
|
|