了解针对延迟加载的 helper 函数

链接器支持的延迟加载使用的帮助程序函数是在运行时实际加载 DLL 的函数。 可以修改帮助程序函数以自定义其行为。 如果不使用 delayimp.lib 中提供的帮助程序函数,可以编写自己的函数并将其链接到程序。 一个帮助器函数可为所有延迟加载的 DLL 提供服务。

如果你想要根据 DLL 或导入名称进行特定的处理,可以提供自己的帮助器函数版本。

帮助器函数执行以下操作:

  • 检查库的已存储句柄,确定该库是否已加载

  • 调用 LoadLibrary 以尝试加载 DLL

  • 调用 GetProcAddress 以尝试获取过程的地址

  • 返回到延迟导入加载形实转换以调用现已加载的入口点

在执行以下每个操作后,帮助器函数可以回调程序中的通知挂钩:

  • 当帮助器函数启动时

  • 在帮助器函数中调用 LoadLibrary 之前的那一刻

  • 在帮助器函数中调用 GetProcAddress 之前的那一刻

  • 如果在帮助器函数中对 LoadLibrary 的调用失败

  • 如果在帮助器函数中对 GetProcAddress 的调用失败

  • 在帮助器函数完成处理后

其中的每个挂钩点都可以返回一个值,该值以某种方式更改帮助器例程的正常处理,只不过它会返回到延迟导入加载形实转换。

默认帮助器代码可以在 MSVC include 目录中的 delayhlp.cppdelayimp.h 内找到。 它已根据目标体系结构编译到 MSVC lib 目录中的 delayimp.lib 内。 除非你编写自己的帮助器函数,否则需要在编译中包含此库。

延迟加载帮助器调用约定、参数和返回类型

延迟加载帮助器例程的原型是:

FARPROC WINAPI __delayLoadHelper2(
    PCImgDelayDescr pidd,
    FARPROC * ppfnIATEntry
);

参数

pidd
指向 constImgDelayDescr 指针,其中包含与导入相关的各种数据的偏移量、绑定信息的时间戳和提供有关描述符内容的进一步信息的特性集。 当前只有一个特性,即 dlattrRva,它指示描述符中的地址是相对虚拟地址。 有关详细信息,请参阅 delayimp.h 中的声明。

延迟描述符中的指针(delayimp.h 中的 ImgDelayDescr)使用相对虚拟地址 (RVA) 在 32 位和 64 位程序中按预期方式工作。 若要使用这些 RVA,请使用 delayhlp.cpp 中的函数 PFromRva 将其转换回指针。 可对描述符中的每个字段使用此函数将其转换回 32 位或 64 位指针。 默认的延迟加载帮助器函数是可用作示例的很好模板。

有关 PCImgDelayDescr 结构的定义,请参阅结构和常量定义

ppfnIATEntry
指向延迟加载导入地址表 (IAT) 中的槽的指针。 它是使用已导入函数的地址更新的槽。 帮助器例程需要存储它返回到此位置的同一值。

预期的返回值

如果帮助器函数成功,则返回已导入函数的地址。

如果该函数失败,则会引发结构化异常并返回 0。 引发的异常有三种类型:

  • 无效参数,发生在 pidd 中的特性未正确指定时。 请将此视为不可恢复的错误。

  • LoadLibrary 在指定的 DLL 上失败。

  • GetProcAddress 失败。

由你负责处理这些异常。 有关详细信息,请参阅错误处理和通知

注解

Helper 函数的调用约定是 __stdcall。 返回值的类型与本主题无关,因此使用了 FARPROC。 此函数具有 C 链接,这意味着,在 C++ 代码中声明它时需要用 extern "C" 包装它。 ExternC 宏会为你处理此包装器。

若要将帮助器例程用作通知挂钩,代码必须指定要返回的相应函数指针。 然后,链接器生成的 thunk 代码可将该返回值用作导入的实际目标并直接跳转到该目标。 如果你不想将帮助器例程用作通知挂钩,请将帮助器函数的返回值存储在 ppfnIATEntry 中,即传入的函数指针位置。

示例挂钩函数

以下代码演示如何实现一个基本的挂钩函数。

FARPROC WINAPI delayHook(unsigned dliNotify, PDelayLoadInfo pdli)
{
    switch (dliNotify) {
        case dliStartProcessing :

            // If you want to return control to the helper, return 0.
            // Otherwise, return a pointer to a FARPROC helper function
            // that will be used instead, thereby bypassing the rest
            // of the helper.

            break;

        case dliNotePreLoadLibrary :

            // If you want to return control to the helper, return 0.
            // Otherwise, return your own HMODULE to be used by the
            // helper instead of having it call LoadLibrary itself.

            break;

        case dliNotePreGetProcAddress :

            // If you want to return control to the helper, return 0.
            // If you choose you may supply your own FARPROC function
            // address and bypass the helper's call to GetProcAddress.

            break;

        case dliFailLoadLib :

            // LoadLibrary failed.
            // If you don't want to handle this failure yourself, return 0.
            // In this case the helper will raise an exception
            // (ERROR_MOD_NOT_FOUND) and exit.
            // If you want to handle the failure by loading an alternate
            // DLL (for example), then return the HMODULE for
            // the alternate DLL. The helper will continue execution with
            // this alternate DLL and attempt to find the
            // requested entrypoint via GetProcAddress.

            break;

        case dliFailGetProc :

            // GetProcAddress failed.
            // If you don't want to handle this failure yourself, return 0.
            // In this case the helper will raise an exception
            // (ERROR_PROC_NOT_FOUND) and exit.
            // If you choose, you may handle the failure by returning
            // an alternate FARPROC function address.

            break;

        case dliNoteEndProcessing :

            // This notification is called after all processing is done.
            // There is no opportunity for modifying the helper's behavior
            // at this point except by longjmp()/throw()/RaiseException.
            // No return value is processed.

            break;

        default :

            return NULL;
    }

    return NULL;
}

/*
and then at global scope somewhere:

ExternC const PfnDliHook __pfnDliNotifyHook2 = delayHook;
ExternC const PfnDliHook __pfnDliFailureHook2 = delayHook;
*/

延迟加载结构和常量定义

默认的延迟加载帮助器例程使用多个结构来与挂钩函数通信,在发生任何异常期间也会使用多个结构。 这些结构在 delayimp.h 中定义。 下面是传递给挂钩的宏、typedef、通知和失败值、信息结构以及指向挂钩函数的指针类型:

#define _DELAY_IMP_VER  2

#if defined(__cplusplus)
#define ExternC extern "C"
#else
#define ExternC extern
#endif

typedef IMAGE_THUNK_DATA *          PImgThunkData;
typedef const IMAGE_THUNK_DATA *    PCImgThunkData;
typedef DWORD                       RVA;

typedef struct ImgDelayDescr {
    DWORD           grAttrs;        // attributes
    RVA             rvaDLLName;     // RVA to dll name
    RVA             rvaHmod;        // RVA of module handle
    RVA             rvaIAT;         // RVA of the IAT
    RVA             rvaINT;         // RVA of the INT
    RVA             rvaBoundIAT;    // RVA of the optional bound IAT
    RVA             rvaUnloadIAT;   // RVA of optional copy of original IAT
    DWORD           dwTimeStamp;    // 0 if not bound,
                                    // O.W. date/time stamp of DLL bound to (Old BIND)
    } ImgDelayDescr, * PImgDelayDescr;

typedef const ImgDelayDescr *   PCImgDelayDescr;

enum DLAttr {                   // Delay Load Attributes
    dlattrRva = 0x1,                // RVAs are used instead of pointers
                                    // Having this set indicates a VC7.0
                                    // and above delay load descriptor.
    };

//
// Delay load import hook notifications
//
enum {
    dliStartProcessing,             // used to bypass or note helper only
    dliNoteStartProcessing = dliStartProcessing,

    dliNotePreLoadLibrary,          // called just before LoadLibrary, can
                                    //  override w/ new HMODULE return val
    dliNotePreGetProcAddress,       // called just before GetProcAddress, can
                                    //  override w/ new FARPROC return value
    dliFailLoadLib,                 // failed to load library, fix it by
                                    //  returning a valid HMODULE
    dliFailGetProc,                 // failed to get proc address, fix it by
                                    //  returning a valid FARPROC
    dliNoteEndProcessing,           // called after all processing is done, no
                                    //  bypass possible at this point except
                                    //  by longjmp()/throw()/RaiseException.
    };

typedef struct DelayLoadProc {
    BOOL                fImportByName;
    union {
        LPCSTR          szProcName;
        DWORD           dwOrdinal;
        };
    } DelayLoadProc;

typedef struct DelayLoadInfo {
    DWORD               cb;         // size of structure
    PCImgDelayDescr     pidd;       // raw form of data (everything is there)
    FARPROC *           ppfn;       // points to address of function to load
    LPCSTR              szDll;      // name of dll
    DelayLoadProc       dlp;        // name or ordinal of procedure
    HMODULE             hmodCur;    // the hInstance of the library we have loaded
    FARPROC             pfnCur;     // the actual function that will be called
    DWORD               dwLastError;// error received (if an error notification)
    } DelayLoadInfo, * PDelayLoadInfo;

typedef FARPROC (WINAPI *PfnDliHook)(
    unsigned        dliNotify,
    PDelayLoadInfo  pdli
    );

计算延迟加载的必要值

延迟加载帮助器例程需要计算两项关键信息。 为提供帮助,delayhlp.cpp 中有两个内联函数可以计算此信息。

  • 第一个函数 IndexFromPImgThunkData 计算当前导入到三个不同表(导入地址表 (IAT)、绑定导入地址表 (BIAT) 和未绑定导入地址表 (UIAT))的项的索引。

  • 第二个函数 CountOfImports 计算有效 IAT 中的导入项数。

// utility function for calculating the index of the current import
// for all the tables (INT, BIAT, UIAT, and IAT).
__inline unsigned
IndexFromPImgThunkData(PCImgThunkData pitdCur, PCImgThunkData pitdBase) {
    return pitdCur - pitdBase;
    }

// utility function for calculating the count of imports given the base
// of the IAT. NB: this only works on a valid IAT!
__inline unsigned
CountOfImports(PCImgThunkData pitdBase) {
    unsigned        cRet = 0;
    PCImgThunkData  pitd = pitdBase;
    while (pitd->u1.Function) {
        pitd++;
        cRet++;
        }
    return cRet;
    }

支持卸载延迟加载的 DLL

加载延迟加载的 DLL 时,默认的延迟加载帮助器会检查延迟加载描述符是否在 pUnloadIAT 字段中包含指针和原始导入地址表 (IAT) 的副本。 如果包含,则帮助器会在列表中保存一个指向导入延迟描述符的指针。 帮助器函数可使用此条目按名称查找 DLL,以支持显式卸载该 DLL。

下面是用于显式卸载延迟加载 DLL 的关联结构和函数:

//
// Unload support from delayimp.h
//

// routine definition; takes a pointer to a name to unload

ExternC
BOOL WINAPI
__FUnloadDelayLoadedDLL2(LPCSTR szDll);

// structure definitions for the list of unload records
typedef struct UnloadInfo * PUnloadInfo;
typedef struct UnloadInfo {
    PUnloadInfo     puiNext;
    PCImgDelayDescr pidd;
    } UnloadInfo;

// from delayhlp.cpp
// the default delay load helper places the unloadinfo records in the
// list headed by the following pointer.
ExternC
PUnloadInfo __puiHead;

UnloadInfo 结构是使用一个 C++ 类实现的,该类分别将 LocalAllocLocalFree 实现用作其 operator newoperator delete。 这些选项保存在使用 __puiHead 作为列表头的标准链接列表中。

当你调用 __FUnloadDelayLoadedDLL 时,它会尝试在已加载 DLL 列表中查找你提供的名称。 (必须完全匹配。)如果找到,则将 pUnloadIAT 中的 IAT 副本复制到正在运行的 IAT 的顶部,以还原形实转换指针。 然后使用 FreeLibrary 释放库,将匹配的 UnloadInfo 记录从列表中取消链接并删除,然后返回 TRUE

函数 __FUnloadDelayLoadedDLL2 的参数区分大小写。 例如,需要指定:

__FUnloadDelayLoadedDLL2("user32.dll");

而不是:

__FUnloadDelayLoadedDLL2("User32.DLL");

有关卸载延迟加载的 DLL 的示例,请参阅显式卸载延迟加载的 DLL

开发你自己的延迟加载帮助器函数

你可能想要提供自己的延迟加载帮助器例程版本。 在你自己的例程中,可以根据 DLL 或导入名称进行特定的处理。 可通过两种方式插入你自己的代码:编写自己的帮助器函数(可能基于提供的代码)。 或者,使用通知挂钩来挂接提供的帮助器以调用你自己的函数。

编写你自己的帮助器

你可以直接创建自己的帮助器例程。 可以参照现有的代码来创建新函数。 创建的函数必须使用与现有帮助器相同的调用约定。 此外,如果它返回到链接器生成的形实转换,则它必须返回正确的函数指针。 创建代码后,可以根据需要完善调用或放弃调用。

使用开始处理通知挂钩

最简单的方法可能是提供一个指向用户提供的通知挂钩函数的新指针,该函数采用的值与 dliStartProcessing 通知的默认帮助器相同。 此时,挂钩函数本质上可以成为新的帮助器函数,因为成功返回到默认帮助器会绕过默认帮助器中的所有其他处理。

另请参阅

链接器对延迟加载 DLL 的支持