第20章 DLL高级技术(2)

时间:2023-04-13 22:34:50

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

第20章 DLL高级技术(2)

注意图中第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 = ;
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, , &dwNeeded);
phMods = (HMODULE*)malloc(dwNeeded); if (EnumProcessModules(hProcess,phMods,dwNeeded,&dwNeeded)){
for (DWORD i = ; 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 = ;
int iVal2 = ;
int iVal3 = ;
_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 ;
}

(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[]); // Create a buffer where we construct error messages
char sz[] = { }; 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、异常处理和钩子函数的使用

第20章 DLL高级技术(2)

//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 ();
} //////////////////////////////////////////////////////////////////////////
int fnLib2(){
return ();
} //////////////////////////////文件结束/////////////////////////////////////

//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[];
#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 = ; //调试模式下,可以通过菜单"调试"->"窗口"->"模块"来查看指定的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 ;
} LONG WINAPI DelayLoadDllExceptionFilter(PEXCEPTION_POINTERS pep){
//假设异常为己知的
LONG lDisposition = EXCEPTION_EXECUTE_HANDLER; //如果出现延时加载错误,ExceptionInformation[0]指向一个DelayLoadInfo结构体
//其中包含出错的信息
PDelayLoadInfo pdli = (PDelayLoadInfo)pep->ExceptionRecord->ExceptionInformation[]; //创建一个错误信息的缓冲区
char sz[] = { }; 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
// TEXTINCLUDE
BEGIN
"resource.h\0"
END TEXTINCLUDE
BEGIN
"#include ""winres.h""\r\n"
"\0"
END 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