链接器的延迟加载 DLL 支持

MSVC 链接器支持延迟加载 DLL。 此功能可减轻你使用 Windows SDK 函数 LoadLibraryGetProcAddress 实现 DLL 延迟加载的需要。

在不延迟加载的情况下,运行时加载 DLL 的唯一方法是使用 LoadLibraryGetProcAddress,操作系统会在加载可执行文件或 DLL 时加载 DLL。

使用延迟加载时,当隐式链接 DLL 时,链接器会提供用于延迟 DLL 加载的选项,直到程序在该 DLL 中调用函数。

应用程序可以使用带有帮助程序函数的 /DELAYLOAD(延迟加载导入)链接器选项来延迟加载 DLL。 (Microsoft 提供了一个默认帮助程序函数实现。)帮助程序通过调用 LoadLibraryGetProcAddress 为你在运行时按需加载 DLL。

如果存在以下情况,请考虑延迟加载 DLL:

  • 程序可能不会在 DLL 中调用函数。

  • DLL 中的函数可能直到程序执行的后期才被调用。

可以在 EXE 或 DLL 项目的生成期间指定延迟加载 DLL。 延迟加载一个或多个 DLL 的 DLL 项目本身不应在 DllMain 中调用延迟加载入口点。

指定要延迟加载的 DLL

可以使用 /delayload:dllname 链接器选项指定延迟加载哪个 Dll。 如果你不打算使用自己的帮助程序函数版本,则还必须将你的程序与 delayimp.lib(对于桌面应用程序)或 dloadhelper.lib(对于 UWP 应用)链接在一起。

以下是延迟加载 DLL 的一个简单示例:

// cl t.cpp user32.lib delayimp.lib  /link /DELAYLOAD:user32.dll
#include <windows.h>
// uncomment these lines to remove .libs from command line
// #pragma comment(lib, "delayimp")
// #pragma comment(lib, "user32")

int main() {
   // user32.dll will load at this point
   MessageBox(NULL, "Hello", "Hello", MB_OK);
}

生成项目的调试版本。 使用调试器单步调试该代码,你会注意到只有在你调用 MessageBox 时才加载 user32.dll

显式卸载延迟加载的 DLL

/delay:unload 链接器选项允许代码显式卸载延迟加载的 DLL。 默认情况下,延迟加载的导入保留在导入地址表 (IAT) 中。 但是,如果在链接器命令行上使用 /delay:unload,则帮助程序函数支持通过 __FUnloadDelayLoadedDLL2 调用显式卸载 DLL,并将 IAT 重置为其原始形式。 现在将覆盖无效的指针。 IAT 是 ImgDelayDescr 结构中的一个字段,其中包含原始 IAT 的副本的地址(如果存在)。

卸载延迟加载的 DLL 示例

此示例演示了如何显式卸载包含函数 fnMyDll 的 DLL MyDll.dll

// link with /link /DELAYLOAD:MyDLL.dll /DELAY:UNLOAD
#include <windows.h>
#include <delayimp.h>
#include "MyDll.h"
#include <stdio.h>

#pragma comment(lib, "delayimp")
#pragma comment(lib, "MyDll")
int main()
{
    BOOL TestReturn;
    // MyDLL.DLL will load at this point
    fnMyDll();

    //MyDLL.dll will unload at this point
    TestReturn = __FUnloadDelayLoadedDLL2("MyDll.dll");

    if (TestReturn)
        printf_s("\nDLL was unloaded");
    else
        printf_s("\nDLL was not unloaded");
}

有关卸载延迟加载的 DLL 的重要说明:

  • 可以在 MSVC include 目录中的文件 delayhlp.cpp 中找到 __FUnloadDelayLoadedDLL2 函数的实现。 有关详细信息,请参阅了解延迟加载帮助程序函数

  • __FUnloadDelayLoadedDLL2 函数的 name 参数必须与导入库包含的内容完全匹配(包括大小写)。 (该字符串也位于映像中的导入表中。)可以使用 DUMPBIN /DEPENDENTS 查看导入库的内容。 如果首选不区分大小写的字符串匹配,可以更新 __FUnloadDelayLoadedDLL2 以使用其中一个不区分大小写的 CRT 字符串函数或使用 Windows API 调用。

绑定延迟加载的导入

默认链接器行为是为延迟加载的 DLL 创建可绑定的导入地址表 (IAT)。 如果绑定了 DLL,则 helper 函数会尝试使用绑定信息,而不是对每个引用的导入调用 GetProcAddress。 如果时间戳或首选地址与加载的 DLL 中的时间戳或首选地址不匹配,则帮助程序函数会假定绑定的导入地址表已经过期。 它会继续,就像 IAT 不存在一样。

如果从未打算绑定 DLL 的延迟加载导入,请在链接器命令行上指定 /delay:nobind。 链接器不会生成绑定的导入地址表,这会节省映像文件中的空间。

加载被延迟加载的 DLL 的所有导入

delayhlp.cpp 中定义的 __HrLoadAllImportsForDll 函数指示链接器从使用 /delayload 链接器选项指定的 DLL 加载所有导入。

一次加载所有导入时,可以在一个位置集中处理错误。 可以避免围绕对导入的所有实际调用进行结构化异常处理。 这还避免了应用程序在某个过程期间失败的情况:例如,如果帮助程序代码在成功加载其他导入后无法加载某个导入。

调用 __HrLoadAllImportsForDll 不会更改挂钩和错误处理的行为。 有关详细信息,请参阅错误处理和通知

__HrLoadAllImportsForDll 对 DLL 本身中存储的名称进行区分大小写的比较。

下面是在称为 TryDelayLoadAllImports 的函数中使用 __HrLoadAllImportsForDll 尝试加载命名 DLL 的示例。 它使用函数 CheckDelayException 来确定异常行为。

int CheckDelayException(int exception_value)
{
    if (exception_value == VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND) ||
        exception_value == VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND))
    {
        // This example just executes the handler.
        return EXCEPTION_EXECUTE_HANDLER;
    }
    // Don't attempt to handle other errors
    return EXCEPTION_CONTINUE_SEARCH;
}

bool TryDelayLoadAllImports(LPCSTR szDll)
{
    __try
    {
        HRESULT hr = __HrLoadAllImportsForDll(szDll);
        if (FAILED(hr))
        {
            // printf_s("Failed to delay load functions from %s\n", szDll);
            return false;
        }
    }
    __except (CheckDelayException(GetExceptionCode()))
    {
        // printf_s("Delay load exception for %s\n", szDll);
        return false;
    }
    // printf_s("Delay load completed for %s\n", szDll);
    return true;
}

可以使用 TryDelayLoadAllImports 的结果来控制是否调用导入函数。

错误处理和通知

如果程序使用延迟加载的 DLL,则必须可靠地处理错误。 程序运行时发生的失败将导致未经处理的异常。 有关 DLL 延迟加载错误处理和通知的详细信息,请参阅错误处理和通知

转储延迟加载的导入

可使用 DUMPBIN /IMPORTS 转储延迟加载的导入。 这些导入显示的信息与标准导入略有不同。 它们被隔离到 /imports 列表中自己的部分,并被显式标记为延迟加载的导入。 如果映像中存在卸载信息,则会说明这一点。 如果存在绑定信息,则会记下目标 DLL 的日期和时间戳以及导入的绑定地址。

延迟加载 DLL 的限制

延迟加载 DLL 导入有几个限制。

  • 不支持数据导入。 解决方法是使用 LoadLibrary(或者在知道延迟加载帮助程序已加载 DLL 后使用 GetModuleHandle)和 GetProcAddress 自行显式处理数据导入。

  • 不支持延迟加载 Kernel32.dll。 必须加载此 DLL 才能使延迟加载帮助程序例程正常工作。

  • 不支持绑定转发的入口点。

  • 如果 DLL 加载延迟,而不是在启动时加载,则进程可能会有不同的行为。 你可以看到在延迟加载 DLL 的入口点中是否存在按进程的初始化。 其他情况包括静态 TLS(线程本地存储),它使用通过 LoadLibrary 加载 DLL 时不处理的 __declspec(thread) 来声明。 使用 TlsAllocTlsFreeTlsGetValueTlsSetValue 的动态 TLS 仍可在静态或者延迟加载的 DLL 中使用。

  • 初次调用每个函数后,应将静态全局函数指针重新初始化为导入的函数。 这是必需的,因为第一次使用函数指针会指向 thunk,而不是加载的函数。

  • 目前还没有办法在使用正常导入机制时,只延迟加载 DLL 中的特定过程。

  • 不支持自定义调用约定(例如在 x86 体系结构上使用条件代码)。 此外,任何平台上都不保存浮点寄存器。 如果自定义帮助程序例程或挂钩例程使用浮点类型,请注意:这些例程必须在具有浮点参数的寄存器调用约定的计算机上保存和恢复完整浮点状态。 延迟加载 CRT DLL 时请小心谨慎,尤其是在帮助程序函数中调用 CRT 函数的情况,这些 CRT 函数采用数值数据处理器 (NDP) 堆栈上的浮点参数。

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

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

另请参阅

在 Visual Studio 中创建 C/C++ DLL
MSVC 链接器参考