Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009
.pdf658 Часть IV. Динамически подключаемые библиотеки
манду Print, приложение обращается к соответствующей функции вашей DLL, и та автоматически загружается. Все отлично, но, напечатав документ, пользователь вряд ли станет сразу же печатать что-то еще, а значит, вы можете выгрузить свою DLL и освободить системные ресурсы. Потом, когда пользователь решит напечатать другой документ, DLL вновь будет загружена в адресное пространство вашего процесса.
Чтобы DLL отложенной загрузки можно было выгружать, вы должны сделать две вещи. Во-первых, при сборке исполняемого файла задать ключ /Delay: unload компоновщика. А во-вторых, немного изменить исходный код и поместить в точке выгрузки DLL вызов функции __FUnloadDelayLoadedDLL2:
BOOL __FUnloadDelayLoadedDLL2(PCSTR szDll);
Ключ /Delay:unload заставляет компоновщик создать в файле дополнительный раздел. В нем хранится информация, необходимая для сброса уже вызывавшихся DLL-функций, чтобы к ним снова можно было обратиться через
__delayLoadHelper. Вызывая __FUnloadDelayLoadedDLL2, вы передаете имя вы-
гружаемой DLL. После этого она просматривает раздел выгрузки (unload section) и сбрасывает адреса всех DLL-функций. И, наконец, __FUnloadDelayLoadedDLL2 вызывает FreeLibrary, чтобы выгрузить эту DLL.
Обратите внимание на несколько важных моментов. Во-первых, ни при каких условиях не вызывайте сами FreeLibrary для выгрузки DLL, иначе сброса адреса DLL-функции не произойдет, и впоследствии любое обращение к ней приведет к нарушению доступа. Во-вторых, при вызове __FUnloadDelayLoadedDLL2 в имени DLL нельзя указывать путь, а регистры всех букв должны быть точно такими же, как и при передаче компоновщику в ключе /DelayLoad; в ином случае вызов __FUnloadDelayLoadedDLL2 закончится неудачно. В-третьих, если вы вообще не собираетесь выгружать DLL отложенной загрузки, не задавайте ключ /Delay:unload — тогда вы уменьшите размер своего исполняемого файла. И, наконец, если вы вызовете __FUnloadDelayLoadedDLL2 из модуля, собранного без ключа /Delay:unload, ничего страшного не случится: __FUnloadDelayLoadedDLL2
проигнорирует вызов и просто вернет FALSE.
Другая особенность DLL отложенной загрузки в том, что вызываемые вами функции по умолчанию связываются с адресами памяти, по которым они, как считает система, будут находиться в адресном пространстве процесса. (О связывании мы поговорим чуть позже.) Поскольку связываемые разделы DLL отложенной загрузки увеличивают размер исполняемого файла, вы можете запретить их создание, указав ключ /Delay:nobind компоновщика. Однако связывание, как правило, предпочтительно, поэтому при сборке большинства приложений этот ключ использовать не следует.
И последняя особенность DLL отложенной загрузки. Она, кстати, наглядно демонстрирует характерное для Майкрософт внимание к деталям. Функция __delayLoadHelper2 может вызывать предоставленные вами функ- ции-ловушки (hook functions), и они будут получать уведомления о том, как
Глава 20. DLL - более сложные методы программирования.docx 659
идет выполнение __delayLoadHelper2, а также уведомления o6 ошибках. Кроме того, они позволяют изменять порядок загрузки DLL и формирования виртуального адреса DLL-функций.
Чтобы получать уведомления или изменить поведение __delauLoadHelper2, нужно внести два изменения в спой исходный код. Во-первых, вы должны написать функцию-ловушку по образу и подобию DliHook, код которой показан на рис. 20-6. Моя функция DliHook не влияет на характер работы
__delayLoadHelper2. Если вы хотите изменить поведение __delayLoadHelper2,
начните с DliHook и модифицируйте ее код так, как вам требуется. Потом передайте ее адрес функции __delayLoadHelper2.
В статически подключаемой библиотеке DelayImp.lib определены две гло-
бальные переменные типа PfnDliHook: __pfnDliNotifyHook2 и __pfnDliFailureHook2:
typedef FARPR0C (WINAPI *PfnDliHook)( unsigned dliNotify, PDelayLoadInfo pdli);
Как видите, это тип данных, соответствующий функции, и он совпадает с прототипом моей DliHook. В DelayImp.lib эти две переменные инициализируются значением NULL, которое сообщает __delayLoadHelper2, что никаких функцийловушек вызывать не требуется. Чтобы ваша функция-ловушка все же вызывалась, вы должны присвоить ее адрес одной из этих переменных. В своей программе я просто добавил на глобальном уровне две строки:
PfnDliHook _pfnDliNotifyHook2 = DliHook;
PfnDliHook _pfnDliFailureHook2 = DliHook;
Так что __delayLoadHelper фактически работает с двумя функциями обратного вызова: одна вызывается для уведомлений, другая—для сообщений об ошибках. Поскольку их прототипы идентичны, а первый параметр, dliNotify, сообщает о причине вызова функции, я всегда упрощаю себе жизнь, создавая одну функцию и настраивая на нее обе переменные.
Совет. Утилита Dependency Walker (www.DependencyWalker.com) генери-
рует список зависимостей периода компоновки (статические, а также связанные с отложенной загрузкой DKK, а также отслеживает вызовы LoadLibrary/GetProcAddress в период выполнения.
Программа-пример DelayLoadApp
Эта программа (20-DelayLoadApp.exe) показывает, как использовать все преимущества DLL отложенной загрузки. Для демонстрации нам понадобится небольшой DLL-файл; он находится в каталоге 20-DelayLoadLib внутри архива, доступного на Web-сайте поддержки этой книги.
660 Часть IV. Динамически подключаемые библиотеки
Так как программа загружает модуль «20 DelayLoadLib» с задержкой, загрузчик не проецирует его на адресное пространство процесса при запуске. Периодически вызывая функцию IsModuleLoaded, программа выводит окно, которое информирует, загружен ли модуль в адресное пространство процесса. При первом запуске модуль «20 DelayLoadLib» не загружается, о чем и сообщается в окне
(рис. 20-4).
Рис. 20-4. DelayLoadApp сообщает, что модуль «20 DelayLoadLib» незагружен
Далее программа вызывает функцию, импортируемую из DLL, и это заставляет __delayLoadHelper автоматически загрузить нужную DLL. Когда функция вернет управление, программа выведет окно, показанное на рис. 20-5.
Рис. 20-5. DelayLoadApp сообщает, что модуль «20 DelayLoadLib» загружен
Когда пользователь закроет это окно, будет вызвана другая функция из той же DLL. В этом случае DLL не перезагружается в адресное пространство, но перед вызовом новой функции придется определять ее адрес.
Далее вызывается __FUnloadDelayLoadedDLL2, и модуль «20 DelayLoadLib» выгружается из памяти. После очередного вызова IsModuleLoaded на экране появляется окно, показанное на рис. 20-4. Наконец, вновь вызывается импортируемая функция, что приводит к повторной загрузке модуля «20 DelayLoadLib», а IsModuleLoaded открывает окно, как на рис. 20-5.
Если все нормально, то программа будет работать, как я только что рассказал. Однако, если перед запуском программы вы удалите модуль «20 DelayLoadLib» или если в этом модуле не окажется одной из импортируемых функций, будет возбуждено исключение. Из моего кода видно, как корректно выйти из такой ситуации.
Наконец, эта программа демонстрирует, как настроить функцию-ловушку из DLL отложенной загрузки. Моя схематическая функция DliHook не делает ничего интересного. Тем не менее она перехватывает различные уведомления и показывает их вам.
Глава 20. DLL - более сложные методы программирования.docx 661
DelayLoadApp.cpp
/****************************************************************************** Module: DelayLoadApp.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h" /* See Appendix A. */ #include <Windowsx.h>
#include <tchar.h> #include <StrSafe.h>
///////////////////////////////////////////////////////////////////////////////
#include |
<Delayimp.h> // For error handling & advanced features |
|
#include |
"..\20-DelayLoadLib\DelayLoadLib.h" |
// My DLL function prototypes |
|
|
|
///////////////////////////////////////////////////////////////////////////////
//Statically link __delayLoadHelper2/__FUnloadDelayLoadedDLL2 #pragma comment(lib, "Delayimp.lib")
//Note: it is not possible to use #pragma comment(linker, "")
//for /DELAYLOAD and /DELAY
//The name of the Delay-Load module (only used by this sample app) TCHAR g_szDelayLoadModuleName[] = TEXT("20-DelayLoadLib");
///////////////////////////////////////////////////////////////////////////////
// Forward function prototype
LONG WINAPI DelayLoadDllExceptionFilter(PEXCEPTION_POINTERS pep);
///////////////////////////////////////////////////////////////////////////////
void IsModuleLoaded(PCTSTR pszModuleName) {
HMODULE hmod = GetModuleHandle(pszModuleName); char sz[100];
#ifdef UNICODE
StringCchPrintfA(sz, _countof(sz), "Module \"%S\" is %Sloaded.", pszModuleName, (hmod == NULL) ? L"not " : L"");
#else
662 Часть IV. Динамически подключаемые библиотеки
StringCchPrintfA(sz, _countof(sz), "Module \"%s\" is %sloaded.", pszModuleName, (hmod == NULL) ? "not " : "");
#endif
chMB(sz);
}
///////////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR pszCmdLine, int) {
// Wrap all calls to delay-load DLL functions inside SEH __try {
int x = 0;
//If you're in the debugger, try the new Debug.Modules menu item to
//see that the DLL is not loaded prior to executing the line below IsModuleLoaded(g_szDelayLoadModuleName);
x = fnLib(); // Attempt to call delay-load function
// Use Debug.Modules to see that the DLL is now loaded IsModuleLoaded(g_szDelayLoadModuleName);
x = fnLib2(); // Attempt to call delay-load function
//Unload the delay-loaded DLL
//NOTE: Name must exactly match /DelayLoad:(DllName)
PCSTR pszDll = "20-DelayLoadLib.dll";
__FUnloadDelayLoadedDLL2(pszDll);
// Use Debug.Modules to see that the DLL is now unloaded IsModuleLoaded(g_szDelayLoadModuleName);
x = fnLib(); // Attempt to call delay-load function
// Use Debug.Modules to see that the DLL is loaded again IsModuleLoaded(g_szDelayLoadModuleName);
}
__except (DelayLoadDllExceptionFilter(GetExceptionInformation())) { // Nothing to do in here, thread continues to run normally
}
Глава 20. DLL - более сложные методы программирования.docx 663
// More code can go here...
return(0);
}
///////////////////////////////////////////////////////////////////////////////
LONG WINAPI DelayLoadDllExceptionFilter(PEXCEPTION_POINTERS pep) {
// Assume we recognize this exception
LONG lDisposition = EXCEPTION_EXECUTE_HANDLER;
//If this is a Delay-load problem, ExceptionInformation[0] points
//to a DelayLoadInfo structure that has detailed error info PDelayLoadInfo pdli =
PDelayLoadInfo(pep->ExceptionRecord->ExceptionInformation[0]);
//Create a buffer where we construct error messages
char sz[500] = { 0 };
switch (pep->ExceptionRecord->ExceptionCode) {
case VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND): // The DLL module was not found at runtime
StringCchPrintfA(sz, _countof(sz), "Dll not found: %s", pdli->szDll); break;
case VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND):
// The DLL module was found, but it doesn't contain the function if (pdli->dlp.fImportByName) {
StringCchPrintfA(sz, _countof(sz), "Function %s was not found in %s", pdli->dlp.szProcName, pdli->szDll);
} else {
StringCchPrintfA(sz, _countof(sz), "Function ordinal %d was not found
in %s",
pdli->dlp.dwOrdinal, pdli->szDll);
}
break;
default:
// We don't recognize this exception lDisposition = EXCEPTION_CONTINUE_SEARCH; break;
}
664 Часть IV. Динамически подключаемые библиотеки
if (lDisposition == EXCEPTION_EXECUTE_HANDLER) {
// We recognized this error and constructed a message, show it chMB(sz);
}
return(lDisposition);
}
///////////////////////////////////////////////////////////////////////////////
// Skeleton DliHook function that does nothing interesting FARPROC WINAPI DliHook(unsigned dliNotify, PDelayLoadInfo pdli) {
FARPROC fp = NULL; // Default return value
//NOTE: The members of the DelayLoadInfo structure pointed
//to by pdli shows the results of progress made so far.
switch (dliNotify) { case dliStartProcessing:
//Called when __delayLoadHelper2 attempts to find a DLL/function
//Return 0 to have normal behavior or nonzero to override
//everything (you will still get dliNoteEndProcessing)
break;
case dliNotePreLoadLibrary:
//Called just before LoadLibrary
//Return NULL to have __delayLoadHelper2 call LoadLibary
//or you can call LoadLibrary yourself and return the HMODULE fp = (FARPROC) (HMODULE) NULL;
break;
case dliFailLoadLib:
//Called if LoadLibrary fails
//Again, you can call LoadLibary yourself here and return an HMODULE
//If you return NULL, __delayLoadHelper2 raises the
//ERROR_MOD_NOT_FOUND exception
fp = (FARPROC) (HMODULE) NULL; break;
case dliNotePreGetProcAddress:
//Called just before GetProcAddress
//Return NULL to have __delayLoadHelper2 call GetProcAddress,
//or you can call GetProcAddress yourself and return the address fp = (FARPROC) NULL;
break;
Глава 20. DLL - более сложные методы программирования.docx 665
case dliFailGetProc:
//Called if GetProcAddress fails
//You can call GetProcAddress yourself here and return an address
//If you return NULL, __delayLoadHelper2 raises the
//ERROR_PROC_NOT_FOUND exception
fp = (FARPROC) NULL; break;
case dliNoteEndProcessing:
//A simple notification that __delayLoadHelper2 is done
//You can examine the members of the DelayLoadInfo structure
//pointed to by pdli and raise an exception if you desire break;
}
return(fp);
}
///////////////////////////////////////////////////////////////////////////////
// Tell __delayLoadHelper2 to call my hook function PfnDliHook __pfnDliNotifyHook2 = DliHook; PfnDliHook __pfnDliFailureHook2 = DliHook;
//////////////////////////////// End of File //////////////////////////////////
DelayLoadLib.cpp
/****************************************************************************** Module: DelayLoadLib.cpp
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#include "..\CommonFiles\CmnHdr.h" /* See Appendix A. */ #include <Windowsx.h>
#include <tchar.h>
///////////////////////////////////////////////////////////////////////////////
#define DELAYLOADLIBAPI extern "C" __declspec(dllexport) #include "DelayLoadLib.h"
///////////////////////////////////////////////////////////////////////////////
666 Часть IV. Динамически подключаемые библиотеки
int fnLib() {
return(321);
}
///////////////////////////////////////////////////////////////////////////////
int fnLib2() {
return(123);
}
//////////////////////////////// End of File //////////////////////////////////
DelayLoadLib.h
/****************************************************************************** Module: DelayLoadLib.h
Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre
******************************************************************************/
#ifndef DELAYLOADLIBAPI
#define DELAYLOADLIBAPI extern "C" __declspec(dllimport) #endif
///////////////////////////////////////////////////////////////////////////////
DELAYLOADLIBAPI int fnLib();
DELAYLOADLIBAPI int fnLib2();
//////////////////////////////// End of File //////////////////////////////////
Переадресация вызовов функций
Запись о переадресации вызова функции (function forwarder) — это строка в разделе экспорта DLL, которая перенаправляет вызов к другой функции, находящейся в другой DLL. Например, запустив утилиту DumpBin из Visual C++ для Kernel32.dll в Windows Vista, вы среди прочей информации увидите и следующее.
C:\Windows\System32>DumpBin - Exports Kernel32.dll |
(часть вывода опущена) |
7549 CloseThreadpoolIo (forwarded to NTDLL.TpReleaseIoCoropletion)
764A CloseThreadpoolTimer (forwarded to NTDLL.TpReleaseTimer)
774B CloseThreadpoolWait (forwarded to NTDLL.TpReleaseWait)
784C CloseThreadpoolWork (forwarded to NTDLL.TpReleaseWork)
(остальное тоже опущено)
Глава 20. DLL - более сложные методы программирования.docx 667
Здесь есть четыре переадресованные функции. Всякий раз, когда ваше прило-
жение вызывает CloseThreadpoolIo, CloseThreadpoolTimer, CloseThreadpoolWait, or CloseThreadpoolWork, его EXE-модуль динамически связывается с Kernel32.dll. При запуске EXE-модуля загрузчик загружает Kernel32.dll и, обнаружив, что переадресуемые функции на самом деле находятся в NTDLL. dll, загружает и эту DLL. Обращаясь к CloseThreadpoolIo, программа фактически вызывает функцию
TpReleaseIoCompletion из NTDLL.dll. А функции CloseThreadpoolIo вообще нет!
При вызове CloseThreadpoolIo (см. ниже) функция GetProcAddress просмотрит раздел экспорта Kernel32.dll и, выяснив, что CloseThreadpoolIo — переадресуемая функция, рекурсивно вызовет сама себя для поиска RtlAllocateHeap в разделе экс-
порта NTDLL.dll.
Вы тоже можете применять переадресацию вызовов функций в своих DLL. Самый простой способ — воспользоваться директивой pragma:
// переадресация к функции из DllWork
#pragma comment(linker, “/export:SomeFunc=DllWork.SomeOtherFunc”)
Эта директива сообщает компоновщику, что DLL должна экспортировать функцию SomeFunc, которая на самом деле реализована как функция SomeOtherFunc в модуле DllWork.dll. Такая запись нужна для каждой переадресуемой функции.
Известные DLL
Некоторые DLL, поставляемые с операционной системой, обрабатываются поособому. Они называются известными DLL (known DLLs) и ведут себя точно так же, как и любые другие DLL с тем исключением, что система всегда ищет их в одном и том же каталоге. В реестре есть раздел:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\
Session Manager\KnownDLLs
Содержимое этого раздела может выглядеть примерно так, как показано ниже (при просмотре реестра с помощью утилиты RegEdit.exe). Как видите, здесь содержится набор параметров, имена которых совпадают с именами известных DLL Значения этих параметров представляют собой строки, идентичные именам параметров, но дополненные расширением .dll. (Впрочем, это не всегда так, и вы сами убедитесь в этом на следующем примере.) Когда вы вызываете LoadLibrary или LoadLibraryEx, каждая из них сначала проверяет, указано ли имя DLL вместе с расширением .dll. Если нет, поиск DLL ведется по обычным правилам.
