20.3 延迟载入DLL
20.3.1延迟载入的目的
(1)如果应用程序使用了多个DLL,那么它的初始化可能比慢,因为加载程序要将所有必需的DLL映射到进程的地址空间。→利用延迟加载可将载入过程延伸到执行过程时
(2)如果我们的代码调用的操作系统的一个新函数,但程序又试图在老版本的操作系统运行。这时程序会被终止。这时可以有两种解决方法:一种是判断利用GetVersionEx操作系统,在老系统中使用旧函数而不使用新函数。另一种是通过延迟载入,通过SEH来捕获异
20.3.2延迟载入技术
(1)延迟载入是针对隐式链接DLL的
(2)一个导出了字段(如即全局变量)的DLL是无法延迟载入的
(3)Kernel32.dll模块是无法延迟载入的,因为必须载入该模块才能调用LoadLibrary和GetProcAddress。
(4)不应在DllMain入口函数中调用一个延迟载入的函数,这可能导致程序崩溃
20.3.3 使用方法及相关说明
(1)使用方法
①常规建立DLL和可执行模块
②链接可执行模块时,添加延迟加载开关
A.为了延迟加载Dll,还需要在解决方案的该项目“属性”->“配置属性”->“链接器”->“输入”->“延迟加载的Dll”中输入MyDll.dll(注意/DelayLoad:MyDll.dll这个开关不能用#pragma comment(linker, "/DelayLoad:MyDll.dll")来设置。
B.增加/Lib:DelayImp.lib开关:这可以用#include <delayimp.h>和#pragma comment(lib, "Delayimp.lib")。这个开关告诉链接器将delayimp中的__delayLoadHelper2函数嵌入到我们的可执行文件中。
C.如果需要手动卸载Dll,则需在可选“链接器”→“高级”中指定“卸载延迟加载的DLL”中输入“MyDll.dll”。但要注意两点:一是卸载时只能调用__FUnloadDelayLoadedDll2(PCSTR szDll)函数,而不能调用FreeLibrary。二是该卸载操作是可选的,不是必需的,只有在需要手动卸载Dll时才设置。
(2)/Lib:DelayImp.lib此时链接器将执行下列的事项
①将MyDll.dll从.exe的导入段去除,这样操作系统就不会隐式载入该DLL
②在.exe中嵌 入一个新的延迟载入段(Delay Import Section,称为.didata)表示要从MyDll.dll中导入哪些函数。
③对延迟载入函数的调用会跳转到__delayLoadHelper2函数,来完成对延迟载入函数的解析。
(3)其他说明
①应用程序对延迟载入函数的调用实际上会调用__delayLoadHelper2函数,该函数会引用那个特殊的延迟载入段,并用LoadLibrary和GetProcAddress得到延迟载入函数的地址,然后修改对该函数的调用,这样以后将直接调用该延迟载入函数。
②同一个DLL中的其他函数仍然必须在第一次被调用的时修复,即其他函数第1次调用时仍然会LoadLibrary+GetProcAddess并修复函数地址。
【Export/ImportDelay程序】演示延迟载入Dll
注意图中第1次列出模块中没有20_ExportDelay.dll,而第2次有且执行了其DllMain函数
//Dll源文件
/************************************************************************ Module: ExportDelay.h ************************************************************************/ #pragma once #ifdef DELAYLIB_EXPORT //MYLIB_EXPORT必须在Dll源文件包含该头件前被定义 #define DELAYAPI extern "C" __declspec(dllexport) //本例中所有的函数和变量都会被导出 #else #define DELAYAPI extern "C" __declspec(dllimport) #endif //定义要导出的函数的原型 DELAYAPI int Func_A(int iVal); DELAYAPI int Func_B(int iVal1, int iVal2); DELAYAPI int Func_C(int iVal1, int iVal2,int iVal3);
#include <windows.h> #include <tchar.h> #include <locale.h> //在这个DLL源文件定义要导出的函数和变量 #define DELAYLIB_EXPORT //这个源文件中须定义这个宏,以告诉编译器函数要 //__declspect(dllexport),这个宏须在包含MyLib.h //之前被定义 #include "ExportDelay.h" BOOL APIENTRY DllMain(HMODULE hDllHandle, DWORD dwReason, LPVOID lpreserved) { static TCHAR pModuleName[MAX_PATH] = {}; switch (dwReason) { case DLL_PROCESS_ATTACH: _tsetlocale(LC_ALL, _T("chs")); GetModuleFileName(hDllHandle, pModuleName, MAX_PATH); _tprintf(_T("进程[%u]调用线程[0x%X]加载DLL[0x%08X]:%s\n"), GetCurrentProcessId(),GetCurrentThreadId(),hDllHandle,pModuleName); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: //GetModuleFileName(hDllHandle, pModuleName, MAX_PATH); _tprintf(_T("进程[%u]调用线程[0x%X]卸载DLL[0x%08X]:%s\n"), GetCurrentProcessId(), GetCurrentThreadId(), hDllHandle, pModuleName); break; } return TRUE; } int Func_A(int iVal) { return iVal; } int Func_B(int iVal1, int iVal2) { return iVal1 + iVal2; } int Func_C(int iVal1, int iVal2, int iVal3) { return iVal1 + iVal2 + iVal3; }
//exe源文件
#include <windows.h> #include <tchar.h> #include <strsafe.h> #include <locale.h> #include <Psapi.h> //For EnumProcessModules函数 #include "../../Chap20/20_ExportDelay/ExportDelay.h" //#include <delayimp.h> //#pragma comment(lib, "Delayimp.lib") //Vs2013这两行可要可不要了。 #pragma comment(lib,"psapi") #pragma comment(lib,"../../Debug/20_ExportDelay.lib") //1、延迟加载是针对Dll的隐式链接的 //2、为了延迟加载Dll,还需要在解决方案的该项目“属性”->“配置属性”-> //“链接器”->“输入”->“延迟加载的Dll”中输入20_ExportDelay.dll void PrintModules(DWORD dwProcessID){ HMODULE* phMods = NULL; HANDLE hProcess = NULL; DWORD dwNeeded = 0; TCHAR szModName[MAX_PATH] = {}; hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwProcessID); if (NULL == hProcess){ _tprintf(_T("不能打开进程[ID:0x%X],错误码:[%u]\n"), dwProcessID, GetLastError()); return; } EnumProcessModules(hProcess, NULL, 0, &dwNeeded); phMods = (HMODULE*)malloc(dwNeeded); if (EnumProcessModules(hProcess,phMods,dwNeeded,&dwNeeded)){ for (DWORD i = 0; i < (dwNeeded / sizeof(HMODULE));i++){ ZeroMemory(szModName, MAX_PATH*sizeof(TCHAR)); if (GetModuleFileNameEx(hProcess,phMods[i],szModName,MAX_PATH)){ _tprintf(_T("\t(0x%08X)\t%s\n"), phMods[i], szModName); } } } free(phMods); CloseHandle(hProcess); } int _tmain(){ _tsetlocale(LC_ALL, _T("chs")); //显示进程中己加载的模块(此时不含(ExportDelay.dll) PrintModules(GetCurrentProcessId()); _tsystem(_T("PAUSE")); int iVal1 = 10; int iVal2 = 20; int iVal3 = 30; _tprintf(_T("Func_A(%d)=%d\n"),iVal1,Func_A(iVal1)); _tprintf(_T("Func_B(%d,%d)=%d\n"), iVal1,iVal2,Func_B(iVal1,iVal2)); _tprintf(_T("Func_C(%d,%d,%d)=%d\n"), iVal1,iVal2,iVal3, Func_C(iVal1,iVal2,iVal3)); PrintModules(GetCurrentProcessId()); _tsystem(_T("PAUSE")); return 0; }
(4)卸载延迟载入的DLL
①构建EXE时必须指定一个额外的链接器开关(/Delay:unload)。该开关告诉链接器在文件中嵌入另一个段。这个段包含用来记录己经调用过的函数。__FUnloadDelayLoadedDll2会引用这个段,将该DLL所有的函数地址重置,然后调用FreeLibrary来卸载该DLL。添加这个开关的方法:项目属性→配置属性→链接器→高级→“卸载延迟加载的DLL”中选择“是(/Delay:unload)”。
②基于以上原因,只能用__FUnloadDelayLoadedDLL2来卸载,而不能手工调用FreeLibrary。(注意,调用__FUnloadDelayLoadedDLL2时只需传入DLL的名称,而不必包含路径)。
③如果不打算卸载一个延迟载入的DLL,就不必指定/Delay:unload链接器开关,这样可减小EXE的大小(因为少了一个段)
20.3.4 延时载入时的异常处理
(1)异常码和异常过滤函数
//只处理后面的两个异常码,其余异常则将异常向外抛 LONG WINAPI DelayLoadDllExceptionFilter(PEXCEPTION_POINTERS pep) { //假定是认识的异常码,则后面会自行处理这些异常 LONG lDisposition = EXCEPTION_EXECUTE_HANDLER; //如果是延迟加载错误,则ExceptionInformation[0]指向一个DelayLoadInfo结构体,其中包含了额外的错误信息。 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): // 找不到指定的DLL模块 StringCchPrintfA(sz, _countof(sz), "Dll not found: %s", pdli->szDll); break; case VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND): // 在DLL模块中找不到指定的函数 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: //其余异常,则向外抛。 lDisposition = EXCEPTION_CONTINUE_SEARCH; break; } if (lDisposition == EXCEPTION_EXECUTE_HANDLER) { // 如果是我们认识的那两个异常,则弹出错误提示 chMB(sz); } return(lDisposition); }
(2)DelayLoadInfo结构体
字段 |
描述 |
DWORD cb |
结构体大小 |
PCImgDelayDescr pidd |
原始数据,指向嵌入在模块中的延迟载入段,其中包含了延迟载入DLL和延迟载入函数的列表 |
FARPROC* ppfn |
若函数查找成功,其地址保存在这个成员中 |
PTCSTR szDll |
试图载入的DLL的名字 |
DelayLoadProc dlp |
试图查打的函数的名字(可能通过序号或名称来)。DelayLoadProc是个联合体,用来表示序号或名称。 |
HMODULE hmodCur |
DLL被载入的内存地址 |
FARPROC pfnCur |
想要查找的函数的地址,为__delayLoadHelper2函数内部使用 |
DWORD dwLastError |
错误码,为__delayLoadHelper2函数内部使用 |
备注:①该结构体由__delayLoadHelper2函数分配和初始化,在函数动态载入DLL并取得被调用函数的地址过程中,它会填写结构中的成员。 ②在我们的SHE过滤器内部,可以获得该结构中体(见上面的代码) |
20.3.5 延迟载入的钩子函数
(1)作用:用于接收__delayLoadHelper2的进度通知和错误通知。
(2)使用方法
①编写钩子函数:FARPROC WINAPI DliHook(unsigned dliNotify, PDelayLoadInfo pdli) {…};
②将钩子函数的地址传给DelayImp.lib库中的两个全局变量(类型为PfnDliHook)。这两个变量被初始化为NULL,即默认__delayLoadHelper2不调用任何钩子函数。为了执行我们的钩子函数,要将钩子函数的地址赋值给这两个变量(分别为__pfnDliNotifyHook2和__pfnDliFailureHook2)。(如PfnDliHook __pfnDliNotifyHook2 = DliHook);
③__delayLoadHelper2实际上用到了两个回调函数,一个用来报告通知,另一个用来报告失败。两个函数原型完全相同。
【DelayLoadApp程序】演示延迟载入DLL、异常处理和钩子函数的使用
//Dll端文件
/************************************************************************ Module: DelayLoadLib.h Notices:Copyright(c) 2008 Jeffrey Richter & Christophe Nasarre ************************************************************************/ #ifndef DELAYLOADLIBAPI #define DELAYLOADLIBAPI extern "C" __declspec(dllimport) #endif // !DELAYLOADLIBAPI ////////////////////////////////////////////////////////////////////////// DELAYLOADLIBAPI int fnLib(); DELAYLOADLIBAPI int fnLib2(); ///////////////////////////////////文件结束////////////////////////////////////
//DelayLoadLib.cpp
/************************************************************************ Module: DelayLoadLib.cpp Notices:Copyright(c) 2008 Jeffrey Richter & Christophe Nasarre ************************************************************************/ #include <tchar.h> #define DELAYLOADLIBAPI extern "C" __declspec(dllexport) #include "DelayLoadLib.h" ////////////////////////////////////////////////////////////////////////// int fnLib(){ return (321); } ////////////////////////////////////////////////////////////////////////// int fnLib2(){ return (123); } //////////////////////////////文件结束/////////////////////////////////////
//exe端文件
/************************************************************************ Module: DelayLoadApp.cpp Notices:Copyright(c) 2008 Jeffrey Richter & Christophe Nasarre ************************************************************************/ #include "../../CommonFiles/CmnHdr.h" #include <tchar.h> #include <strsafe.h> ////////////////////////////////////////////////////////////////////////// #include <delayimp.h> //For error handling & advanced features #include "../20_DelayLoadLib/DelayLoadLib.h" #pragma comment(lib,"../../Debug/20_DelayLoadLib.lib") ////////////////////////////////////////////////////////////////////////// ////要延迟延载入的模块名称(20_DelayLoadLib.dll) TCHAR g_szDelayLoadModuleName[] = TEXT("20_DelayLoadLib"); ////////////////////////////////////////////////////////////////////////// //延迟载入异常过滤函数前向声明 LONG WINAPI DelayLoadDllExceptionFilter(PEXCEPTION_POINTERS pep); //////////////////////////////////////////////////////////////////////// //判断指定的DLL模块是否被加载到进载的地址空间 void IsModuleLoaded(PCTSTR pszModuleName){ HMODULE hmod = NULL;// GetModuleHandle(pszModuleName); char sz[100]; #ifdef UNICODE StringCchPrintfA(sz, _countof(sz), "模块 \"%S\" %s加载.", pszModuleName, (hmod == NULL) ? "未" : "己"); #else StringCchPrintfA(sz, _countof(sz), "模块 \"%s\" %s加载.", pszModuleName, (hmod == NULL) ? "未" : "己"); #endif //chMB(sz); } ////////////////////////////////////////////////////////////////////////// int APIENTRY _tWinMain(HINSTANCE hInstExe, HINSTANCE, PTSTR, int) { //捕获延迟加载时的异常 __try{ int x = 0; //调试模式下,可以通过菜单"调试"->"窗口"->"模块"来查看指定的DLL是否被加载 IsModuleLoaded(g_szDelayLoadModuleName); x = fnLib(); //尝试调用延迟加载函数 //调试下的模块窗口可查看到DLL是否被加载 IsModuleLoaded(g_szDelayLoadModuleName); x = fnLib2();//尝试调用延迟加载函数 //卸载延迟加载DLL //注意:动态库的名称必须正好匹配 /DelayLoad:(DllName) PCSTR pszDll = "20_DelayLoadLib.dll"; //要以下函数生效,必须在“链接器”->"高级"->"卸载延迟加载的DLL"中选择 //"/Delay:Unload" __FUnloadDelayLoadedDLL2(pszDll); //调试下的模块窗口可查看到DLL是否被加载 IsModuleLoaded(g_szDelayLoadModuleName); x = fnLib(); //调试下的模块窗口可查看到DLL是否被加载 IsModuleLoaded(g_szDelayLoadModuleName); } __except (DelayLoadDllExceptionFilter(GetExceptionInformation())){ //这里什么也不做,线程将正常的运行 } //这里可以做些其他事... return 0; } LONG WINAPI DelayLoadDllExceptionFilter(PEXCEPTION_POINTERS pep){ //假设异常为己知的 LONG lDisposition = EXCEPTION_EXECUTE_HANDLER; //如果出现延时加载错误,ExceptionInformation[0]指向一个DelayLoadInfo结构体 //其中包含出错的信息 PDelayLoadInfo pdli = (PDelayLoadInfo)pep->ExceptionRecord->ExceptionInformation[0]; //创建一个错误信息的缓冲区 char sz[100] = { 0 }; switch (pep->ExceptionRecord->ExceptionCode) { //#define VcppException(sev,err) ((sev) | (FACILITY_VISUALCPP<<16) | err) case VcppException(ERROR_SEVERITY_ERROR,ERROR_MOD_NOT_FOUND): //运行时无法找到Dll模块 StringCchPrintfA(sz, _countof(sz), "找不到Dll:%s", pdli->szDll); break; case VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND): //找到了Dll模块,但没有包含指定的函数 if (pdli->dlp.fImportByName){ StringCchPrintfA(sz, _countof(sz), "Dll(%s)中找不到指定的函数%s", pdli->szDll, pdli->dlp.szProcName); } else{ StringCchPrintfA(sz, _countof(sz), "Dll(%s)中找不到指定序号(%d)的函数", pdli->szDll, pdli->dlp.dwOrdinal); } break; default: //不认识的异常 lDisposition = EXCEPTION_CONTINUE_SEARCH; break; } if (lDisposition == EXCEPTION_EXECUTE_HANDLER){ chMB(sz); } return (lDisposition); } ////////////////////////////////////////////////////////////////////////// //DliHook函数处理的框架 FARPROC WINAPI DliHook(unsigned dliNotify, PDelayLoadInfo pdli){ FARPROC fp = NULL; //默认的返回值 //注意:pdli指向到目前为止处理的结果 switch (dliNotify) { case dliFailLoadLib: //调用LoadLibrary函数失败 //这里可以自己再调用LoadLibrary并返回HMODULE //假如返回NULL,__delayLoadHelper2会抛出一个ERROR_MOD_NOT_FOUND异常 fp = (FARPROC)(HMODULE)NULL; break; case dliNotePreGetProcAddress: //会在GetProcAddress函数前被调用 //返回NULL可以使__delayLoadHelper2调用GetProcAddress或可以 //自己调用GetProcAddress并返回其地址 fp = (FARPROC)NULL; break; case dliFailGetProc: //当GetProcAddress调用失败时,被执行 //这里可以自己调用GetProcAddress并返回地址,如果返回NULL //__delayLoadHelper2将抛出ERROR_PROC_NOT_FOUND异常 fp = (FARPROC)NULL; break; case dliNoteEndProcessing: //一个简单的__delayLoadHelper2处理己完成 //这里可以检查DelayLoadInfo结构体,如果有需要,也可以抛出一个异常 break; } return (fp); } //告诉__delayLoadHelper2来调用我们的DliHook函数 PfnDliHook __pfnDliNotifyHook2 = DliHook; PfnDliHook __pfnDliFailureHook2 = DliHook;
//resource.h
//{{NO_DEPENDENCIES}} // Microsoft Visual C++ 生成的包含文件。 // 供 20_DelayLoadApp.rc 使用 // #define IDI_DELAYLOADAPP 101 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NEXT_RESOURCE_VALUE 102 #define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_SYMED_VALUE 101 #endif #endif
//20_DelayLoadApp.rc
// Microsoft Visual C++ generated resource script. // #include "resource.h" #define APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 2 resource. // #include "winres.h" ///////////////////////////////////////////////////////////////////////////// #undef APSTUDIO_READONLY_SYMBOLS ///////////////////////////////////////////////////////////////////////////// // 中文(简体,中国) resources #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED #ifdef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // TEXTINCLUDE // 1 TEXTINCLUDE BEGIN "resource.h\0" END 2 TEXTINCLUDE BEGIN "#include ""winres.h""\r\n" "\0" END 3 TEXTINCLUDE BEGIN "\r\n" "\0" END #endif // APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Icon // // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. IDI_DELAYLOADAPP ICON "DelayLoadApp.ico" #endif // 中文(简体,中国) resources ///////////////////////////////////////////////////////////////////////////// #ifndef APSTUDIO_INVOKED ///////////////////////////////////////////////////////////////////////////// // // Generated from the TEXTINCLUDE 3 resource. // ///////////////////////////////////////////////////////////////////////////// #endif // not APSTUDIO_INVOKED