第20章 DLL高级技术(2)

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
"),
                 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
"),
                 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]
"), 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("	(0x%08X)	%s
"), 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
"),iVal1,Func_A(iVal1));
    _tprintf(_T("Func_B(%d,%d)=%d
"), iVal1,iVal2,Func_B(iVal1,iVal2));
    _tprintf(_T("Func_C(%d,%d,%d)=%d
"), 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"
END

2 TEXTINCLUDE 
BEGIN
    "#include ""winres.h""
"
    ""
END

3 TEXTINCLUDE 
BEGIN
    "
"
    ""
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
原文地址:https://www.cnblogs.com/5iedu/p/5014798.html