Основные моменты реализации
Одной из первичных целей реализации DeadlockDetection была организация управления этой утилитой как с помощью данных, так и с помощью таблиц. Если немного отстраниться и посмотреть на то, чем занимается DLL при обработке подключения, то нетрудно заметить, что для каждой функции из табл. 12.1 обработка почти идентична. Подключаемая функция вызывается, определяет, ее ли класс функций контролируется, вызывает реальную функцию и (если регистрация для этого класса включена) регистрирует информацию, после чего выполняет возврат в вызывающую программу. Надо было написать набор аналогичных функций подключения, причем сделать их как можно более простыми. Сложные функции подключения — идеальная питательная почва для размножения ошибок.
Чтобы показать эту простоту, поговорим о том, как была написана DeadDetExt DLL. DeadDetExt DLL должна иметь три экспортируемых функций. Две первых — DeadDetExtOpen И DeadDetExtClose — самоочевидны. Интересна функция DeadDet, которую вызывает каждая функция подключения, когда имеется информация для записи. DeadDetProcessEvent принимает единственный параметр — указатель на структуру DDEVENTINFO:
typedef struct tagDDEVENTINFO
{
// Идентификатор, который указывает, что содержит остальная часть
// этой структуры
eFuncEnum eFunc ;
// Индикатор pre- или post-вызова
ePrePostEnum ePrePost ;
// Адрес возврата. Этот адрес помогает в нахождении вызова
// функции.
DWORD dwAddr;
// ID вызывающего потока
DWORD dwThreadld;
// Возвращаемое значение для post-вызовов.
DWORD dwRetValue ;
// Информация параметра. Преобразовать тип этой информации
//в подходящую структуру для функции (как описано ниже).
// При доступе к параметрам трактовать их как read-only
// (только-для-чтения).
DWORD dwParams
} DDEVENTINFO-, * LPDDEVENTINFO;
Полный вывод для единственной функции, которая появляется в листинге 12-1, формируется информацией структуры DDEVENTINFO. Большинство полей в ней самоочевидны, но поле dwParams нуждается в специальном упоминании.
Листинг 12-3. DD_FUNCS.H
/*- - - - - - - - - - - - - - - - - - - - - - - - - - - -
"Debugging Applications" (Microsoft Press)
Copyright (c) 1997-2000 John Robbins — All rights reserved.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Прототипы для всех функций подключения и код пролога/эпилога
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#ifndef J3D_FUNCS_H
#define _DD_FUNCS__H
/*//////////////////////////////////////////////////////////////
Все функции подключения имеют спецификатор _declspec (naked), так что я должен обеспечить пролог и эпилог. Заказной пролог и эпилог необходим по нескольким причинам:
1. Функции, написанные на С, не имеют никакого контроля над тем, какие регистры используются или когда компилятор сохраняет исходные значения регистров. Отсутствие контроля над регистрами означает, что получение адреса возврата почти невозможно. Для проекта DeadlockDetection адрес возврата имеет решающее значение.
2. Я также хотел передавать параметры в обрабатывающую функцию расширяющей DLL без необходимости копировать большие объемы данных при каждом вызове функции.
3. Поскольку почти все функции подключения ведут себя одинаково, я установил общие переменные, необходимые во всех функциях.
4. Функции подключения не могут изменять возвращаемых значений
включая значения, полученные от GetLastError. Организуя собственный пролог и эпилог, я могу значительно облегчить возврат корректных значений. Также необходимо восстанавливать значения регистров к тому состоянию, в котором они были после вызова реальной функции.
Основная функция подключения напоминает следующую функцию:
HANDLE NAKEDDEF DD_OpenEventA ( DWORD dwDesiredAccess,
BOOL blnheritHandle ,
LPCSTR IpNarne )
{
// Любые локальные переменные функции должны быть специфицированы
// перед рабочим кодом.
// HOOKFN_PROLOG должен быть специфицирован сразу же после
// локальных переменных.
HOOKFN_PROLOG ();
// Включена ли регистрация типа функции?
if ( TRUE == DoLogging ( DDOPT_EVENT))
{
// Используйте макрос FILL_EVENTINFO, чтобы заполнить переменную
// stEvtlnfo, которая объявлена в макросе HOOKFN_PROLOG. Все
// функции подключения автоматически имеют некоторые локальные
// переменные, помогающие стандартизировать их код.
FILL_EVENTINFO ( eOpenEventA);
// НЕОБХОДИМО вызвать макрос REAL_FUNC_PRE_CALL перед вызовом
// реальной функции, или если регистры ESI и EDI не будут
// сохраняться во время вызова.
REAL_FUNC_PRE_CALL ();
// Вызвать реальную функцию. Возвращаемое значение, сохраняемое
//в ЕАХ, сохраняется как часть обработки REAL_FUNC_POST_CALL.
OpenEventA ( dwDesiredAccess, blnheritHandle , IpName );
//Вы должны назвать макрос REAL_FUNC_POST_CALL после вызова
// реальной функции. Значения регистров и последней ошибки
// сохраняются как часть REAL_FUNC_POST_CALL.
REAL_FUNC_POST_CALL ();
// Вызвать код регистрации для регистрации события.
ProcessEvent ( sstEvtlnfo);
}
else
{
// См. комментарии выше. Предложение else обрабатывает случай,
// когда функция не зарегистрирована.
REAL_FUNC_PRE_CALL ();
OpenEventA ( dwDesiredAccess, blnheritHandle , IpName );
REAL_FUNC_POST_CALL ();
}
// HOOKFN_EPILOG — последний макрос в функции. Его параметр — это
// число параметров функции, так что стек будет очищен правильно.
// Макрос HOOKFN_EPILOG заботится также об установке во всех
// регистрах тех же значений, которые возвратила реальная
// функция.
HOOKFN_EPILOG ( 3) ;
}
//////////////////////////////////////////////////////////////////*/
/*//////////////////////////////////////////////////////////////
Структура состояния регистра. Я использую эту структуру, чтобы гарантировать, что ВСЕ регистры возвращаются точно в том состоянии, в каком они прибыли из реальной функции.
Заметьте, что регистры ЕВР и ESP обрабатываются как часть пролога.
////////////////////////////////////////////////*/
typedef struct tag__REGSTATE
{
DWORD dwEAX;
DWORD dwEBX;
DWORD dwECX;
DWORD dwEDX;
DWORD dwEDI;
DWORD dwESI;
DWORD dwEFL;
} REGSTATE, * PREGSTATE;
/*//////////////////////////////////////////////////////////
Общий код пролога для всех DD_*-функций
//////////////////////////////////////////////////////////*/
#define HOOKFN_PROLOG() \
/* Все функции подключения автоматически получают три одинаковые */\
/* переменные. */\
DDEVENTINFO stEvtlnfo ; /* Информация о событиях для функции */\
DWORD dwLastError; /* Значение последней ошибки */\
REGSTATE stRegState ; /* Состояние регистра */\
{ \
_asm PUSH EBP /* ЕВР всегда сохраняйте явно. */\
_asm MOV EBP, ESP /* Переместить стек */\
_asm MOV EAX, ESP /* Получить указатель стека для вычисления /\
/* адреса возврата и параметров. */\
_asm SUB ESP, _LOCAL_SIZE /* Сохранить пространство для локальных*/\
/ * переменных * / \
_asm ADD EAX, 04h + 04h /* Счет для PUSH EBP и адреса возврата. */\
/* Сохранить начало параметров в стеке. */\
_asm MOV [stEvtlnfo.dwParams], EAX \
_asm SUB EAX, 04h /* Вернуться к адресу возврата. */\
_asm MOV EAX, [EAX] /* EAX теперь содержит адрес возврата. */\
/* Сохранить адрес возврата. */\
_asm MOV [stEvtlnfo.dwAddr], EAX. \
_asm MOV dwLastError, 0 /* Инициализировать dwLastError. */\
/* Инициализировать информацию событий. */\
_asm MOV [stEvtlnfo.eFunc], eUNINITIALIZEDFE \
_asm MOV [stRegState.dwEDI], EDI /* Сохранить два регистра /\
_asm MOV [stRegState.dwESI], ESI /* на время вызовов функций. */\
}
/*///////////////////////////////////////////////////////////
Общий код эпилога для всех 00_*-функций. INumParams -число параметров функции, которая используется для восстановления стека после вызова подключения.
//////////////////////////////////////////////////////////*/
#define HOOKFN_EPILOG(iNumParams) \
{ \
SetLastError ( dwLastError); /* Установить значение последней */\
/* ошибки реальной функции. */\
_asm ADD ESP, _LOCAL_SIZE /* Добавить размер локальных */\
/* переменных. */\
_asm MOV EBX, [StRegState.dwEBX]/* Восстановить все регистры так, */\
_asm MOV ECX, [stRegState.dwECX]/* чтобы данный вызов выглядел */\
_asm MOV EDX, [stRegState.dwEDX]/* идентично перехваченной */\
_asm MOV EDI, [stRegState.dwEDI]/*функции. */\
_asm MOV ESI, [StRegState.dwESI] \
_asm MOV EAX, [StRegState.dwEFL] \
_asm SAHF . \
_asm MOV EAX, [stRegState.dwEAX] \
_asm MOV ESP, EBP /* Передвинуть назад ESP. */\
_asm POP EBP /* Восстановить сохраненный EBP. */\
_asm RET iNumParams * 4 /* stdcall восстановление стека */\
}
/*///////////////////////////////////////////////////////
Необходимо, чтобы макрос REAL_FUNC_PRE_CALL размещался непосредственно перед любым вызовом реальной функции, которую обрабатывает функция подключения. Макрос гарантирует, что EDI и ESI возвращаются в том же состоянии, в котором они были при пересылке в функцию подключения.
//////////////////////////////////////////////////////////////////*/
#define REAL_FUNC_PRE_CALL() \
{ \
_asm MOV EDI, [stRegState.dwEDI] /* Восстановить реальный EDI. /\
_asm MOV ESI, [stRegState.dwESI] /* Восстановить реальный ESI. */\
}
/*//////////////////////////////////////////////////////////////
Необходимо, чтобы макрос REAL_FUNC_POST_CALL размещался сразу же после любого вызова реальной функции, который обрабатывает функция подключения. Все значения регистров после вызова реальной функции сохраняются, так что эпилог функции подключения может возвратить те же значения.
//////////////////////////////////////////////////////////////////*/
#define REAL_FUNC_POST_CALL(} \
{ \
_asm MOV [stRegState.dwEAX], EAX /* Сохранить значение EAX. */\
_asm MOV [stRegState.dwEBX], EBX /* Сохранить значение ЕВХ. */\
_asm MOV [stRegState.dwECX], ECX /* Сохранить значение ЕСХ. */\
_asm MOV [stRegState.dwEDX], EDX /* Сохранить значение EDX. */\
_asm MOV [stRegState.dwEDI], EDI /* Сохранить значение EDI. */\
_asm MOV [stRegState.dwESI], ESI /* Сохранить значение ESI. */\
_asm XOR . EAX, EAX /* Обнулить ЕАХ. */\
_asm LAHF /* в АН.*/\
_asm MOV [stRegState.dwEFL], EAX /* Загрузить значение флажков. */\
} \
dwLastError = GetLastError (); /* Сохранить значение последней */\
/* ошибки. */\
{ \
_asm MOV EAX, [stRegState.dwEAX] /* Восстановить ЕАХ */\
/* к его исходному значению. */\
/* Установить возвращаемое */\
/* значение для информации. */\
_asm MOV [stEvtlnfo.dwRetValue] , ЕАХ \
}
/*////////////////////////////////////////////////////////////
Удобный макрос для заполнения информационной структуры события
/////////////////////////////////////////////////////////////////*/
#define FILL__EVENTINFO(eFn) \
stEvtlnfo.eFunc = eFn ; \
stEvtlnfo.ePrePost = ePostCall; \
stEvtlnfo.dwThreadld = GetCurrentThreadld ()
/*///////////////////////////////////////////////////////////////
Объявление для всех DD_*-определений
////////////////////////////////////////////////////////////////*/
#define NAKEDDEF _declspec(naked)
/*///////////////////////////////////////////////////////////////
БОЛЬШОЕ ПРИМЕЧАНИЕ
Все следующие прототипы похожи на cdecl-функции. Но это не так — все они
stdcall-функции! Заказной пролог и эпилог гарантируют, что используется
правильное соглашение о вызовах!
////////////////////////////////////////////////////////////////*/
////////////////////////////////////////////////////////////////*/
// Обязательные функции, которые должны быть перехвачены, чтобы выполнить
// системную работу
HMODULE DD_LoadLibraryA ( LPCSTR IpLibFileName);
HMODULE DD_LoadLibraryW ( LPCWSTR IpLibFileName);
HMODULE DD_LoadLibraryExA ( LPCSTR IpLibFileName,
HANDLE hFile ,
DWORD dwFlags );
HMODULE DD_LoadLibraryExW ( LPCWSTR IpLibFileName,
HANDLE hFile , DWORD dwFlags );
VOID DD_ExitPr
FARPROC DD_GetProcAddress ( HMODULE hModule, LPCSTR IpProcName);
//////////////////////////////////////////////////////////
Функции, специфические для потоков
HANDLE DD_CreateThread (LPSECURITY_ATTRIBUTES IpThreadAttributes ,
DWORD dwStackSize ,
LPTHREAD_START_ROUTINE IpStartAddress ,
LPVOID IpParameter ,
DWORD dwCreationFlags ,
LPDWORD IpThreadld );
VOID DD_ExitThread ( DWORD dwExitCode);
DWORD DD_SuspendThread ( HANDLE hThread);
DWORD DD_ResumeThread ( HANDLE hThread);
BOOL DDjrerminateThread ( HANDLE hThread, DWORD dwExitCode);
//////////////////////////////////////////////////////////
Ожидание и специальные функции
DWORD DD_WaitForSingleObject ( HANDLE hHandle ,
DWORD dwMilliseconds );
DWORD DD_WaitForSingleObjectEx ( HANDLE hHandle ,
DWORD dwMilliseconds,
BOOL bAlertable );
DWORD DD_WaitForMultipleObjects( DWORD nCount ,
CONST HANDLE * IpHandles ,
BOOL bWaitAll ,
DWORD dwMilliseconds );
DWORD DD_WaitForMultipleObjectsEx( DWORD nCount ,
CONST HANDLE * IpHandles ,
BOOL bWaitAll ,
DWORD dwMilliseconds,
BOOL bAlertable };
DWORD DD_MsgWaitForMultipleObjects ( DWORD nCount ,
LPHANDLE pHandles ,
BOOL fWaitAll ,
DWORD dwMilliseconds,
DWORD dwWakeMask );
DWORD DD_MsgWaitForMultipleObjectsEx ( DWORD nCount ,
LPHANDLE pHandles ,
DWORD dwMilliseconds ,
DWORD dwWakeMask ,
DWORD dwFlags );
DWORD DD_SignalObjectAndWait ( HANDLE hObjectToSignal,
HANDLE hObjectToWaitOn,
DWORD dwMilliseconds ,
BOOL bAlertable );
BOOL DD_CloseHandle ( HANDLE hObject);
///////////////////////////////////////////////////////
// Функции критической секции
VOID DD_InitializeCriticalSection(LPCRITICAL_SECTION IpCriticalSection);
BOOL DD_InitializeCriticalSectionAndSpinCount (
LPCRITICAL_SECTION IpCriticalSection,
DWORD dwSpinCount );
VOID DD_DeleteCriticalSection(LPCRITICAL_SECTION IpCriticalSection);
VOID DD_EnterCriticalSection ( LPCRITICAL_SECTION IpCriticalSection);
VOID DD_LeaveCriticalSection ( LPCRITICAL_SECTION IpCriticalSection);
DWORD DD__SetCriticalSectionSpinCount (
LPCRITICAL_SECTION IpCriticalSection,
DWORD dwSpinCount );
BOOL DD_TryEnterCriticalSection ( LPCRITICAL_SECTION IpCriticalSection);
//////////////////////////////////////////////////////
// Функции мьютекса
HANDLE DD_CreateMutexA ( LPSECURITY_ATTRIBUTES IpMutexAttributes,
BOOL blnitialOwner ,
LPCSTR IpName ) ;
HANDLE DD_CreateMutexW ( LPSECURITY_ATTRIBUTES IpMutexAttributes,
BOOL blnitialOwner ,
LPCWSTR IpName );
HANDLE DD_OpenMutexA ( DWORD dwDesiredAccess,
BOOL blnheritHandle ,
LPCSTR IpName ) ;
HANDLE DD_OpenMutexW ( DWORD dwDesiredAccess,
BOOL blnheritHandle ,
LPCWSTR IpName );
BOOL DD_ReleaseMutex ( HANDLE hMutex);
////////////////////////////////////////////////////////////
// Функции семафора
DD_CreateSemaphoreA ( LPSECURITY_ATTRIBUTES IpSemaphoreAttributes,
LONG UnitialCount ,
LONG IMaximumCount ,
LPCSTR IpName );
HANDLE
DD_CreateSemaphoreW ( LPSECURITY_ATTRIBUTES IpSemaphoreAttributes,
LONG UnitialCount ,
LONG IMaximumCount ,
LPCWSTR IpName );
HANDLE DD_OpenSemaphoreA ( DWORD dwDesiredAccess,
BOOL blnheritHandle ,
LPCSTR IpNaroe );
HANDLE DD_OpenSemaphoreW ( DWORD dwDesiredAccess,
BOOL blnheritHandle ,
LPCWSTR IpNarae );
BOOL DD_ReleaseSemaphore ( HANDLE hSemaphore ,
LONG IReleaseCount ,
LPLONG IpPreviousCount );
//////////////////////////////////////////////////////////////////
// Функции событий
HANDLE DD__CreateEventA ( LPSECURITY_ATTRIBUTES IpEventAttributes,
BOOL bManualReset ,
BOOL blnitialState ,
LPCSTR IpName );
HANDLE DD_CreateEventW ( LPSECURITY_ATTRIBUTES IpEventAttributes,
BOOL bManualReset ,
BOOL blnitialState ,
LPCWSTR IpName );
HANDLE DD_OpenEventA ( DWORD dwDesiredAccess,
BOOL blnheritHandle ,
LPCSTR IpName );
HANDLE DDjOpenEventW ( DWORD dwDesiredAccess,
BOOL blnheritHandle , LPCWSTR IpName );
BOOL DD_PulseEvent ( HANDLE hEvent);
BOOL DD_ResetEvent ( HANDLE hEvent);
BOOL DD_SetEvent ( HANDLE hEvent);
#endif // _DD_FUNCS_H
Сделаю несколько коротких замечаний относительно утилиты DeadlockDetection. Во-первых, DeadlpckDetection всегда активна в приложении, даже если вы откладываете ее регистрацию. Вместо динамических подключений и отключений, я оставляю функции подключенными, а для того чтобы определить, как подключение должно себя вести, просматриваю некоторые внутренние флажки. Сохранение всех функций подключенными облегчает переключение регистрации различных функций во время выполнения, но это добавляет и некоторые издержки. Было интуитивно ясно, что организация подключений и отключений "на лету" привела бы к увеличению количества ошибок в коде DeadlockDetection.
Во-вторых, DeadlockDetection подключает функции вне DLL, когда подключается к программе через функцию LoadLibrary. Однако она может получить управление только после того, как выполнилась функция DllMain этой DLL, поэтому если какие-нибудь объекты синхронизации создаются или используются во время выполнения DllMain, то DeadlockDetection может пропустить их.
В-третьих, DeadlockDetection подключает также функции GetProcAddress и ExitProcess. Подключение GetProcAddress происходит в том случае, если ваша программа вызывает GetProcAddress, чтобы найти (во время выполнения) метод синхронизации.
Я подключаю ExitProcess, потому что, когда приложение заканчивается, необходимо отключить и завершить DeadlockDetection так, чтобы она не завершилась аварийно или не подвесила вашу программу. Поскольку не существует никакого способа управлять порядком разгрузки DLL во время завершения программы, можно легко попасть в ситуации, в которых DLL, связанная с DeadlockDetection (например, DeadDetExt), была бы разгружена раньше самой DeadlockDetection.К счастью, очень немногие разработчики организуют основное многопоточное управление после того, как приложение вызывает функцию ExitProcess.
Наконец, на сопровождающем компакт-диске вместе с DeadlockDetection находятся некоторые тестовые программы. Все они включены в главное рабочее пространство утилиты DeadlockDetection и скомпонованы с библиотекой DEADLOCKDETECTION.DLL так, что их можно использовать для посмотра работы DeadlockDetection.