Dynamic-Link库 (动态链接库)

一、动态链接库(DLL)的基本概念

二、动态链接库的优势

三、动态链接库的实现方法

四、动态链接库的版本冲突问题(DLL地狱)

五、动态链接库与静态链接库的区别


一、动态链接库(DLL)的基本概念

  1. 定义
    • 动态链接库(Dynamic Link Library,简称DLL)是微软Windows操作系统中实现共享函数库概念的一种方式。
    • 它是一种不可执行的二进制程序文件,允许程序共享执行特殊任务所必需的代码和其他资源。
  2. 文件扩展名
    • .dll(主要扩展名)
    • .ocx(包含ActiveX控制的库)
    • .drv(旧式的系统驱动程序)
    • 在Linux系统中,通常是.so的文件。
  3. 特性
    • DLL不是可执行文件,但包含了可由多个程序同时使用的代码和数据。
    • DLL提供了一种方法,使进程可以调用不属于其可执行代码的函数。

二、动态链接库的优势

  1. 共享代码和数据
    • 多个程序可以共享同一个DLL,避免重复开发,减少程序体积。
    • Windows提供的DLL文件中包含了基于Windows的程序在Windows环境下操作的许多函数和资源。
  2. 节省内存资源
    • 多个应用程序可同时访问内存中单个DLL副本的内容。
    • 通过模块化设计,DLL使得早期视窗能在紧张的内存条件下运行。
  3. 便于程序升级
    • 只需要更新DLL,无需重新编译或链接其他程序,即可完成程序升级。
  4. 插件接口
    • 提供了插件的通用接口使用,允许旧模块与新模块无缝集成。

三、动态链接库的实现方法

  1. 创建DLL
    • 使用编程语言(如C++、C#等)编写代码,并编译为DLL文件。
    • 在DLL中明确声明导出函数。
  2. 引入DLL
    • 在程序中添加对DLL的引用,以便程序能够找到并调用库中的函数。
    • 通常需要添加.lib文件(包含了DLL中函数的入口地址)。
  3. 调用DLL中的函数
    • 通过程序中的函数调用语句,实现对DLL库中函数的调用。

四、动态链接库的版本冲突问题(DLL地狱)

  • 当多个应用程序使用同一个共享DLL库时,可能会因为版本不同而发生冲突。

五、动态链接库与静态链接库的区别

  • 动态链接库在程序运行时被加载到内存中,而静态链接库在编译时就被链接到目标程序中。
  • 动态链接库提供了更多的灵活性和模块化,但也可能引入版本冲突的问题。

动态 链接库 (DLL) 是一个模块,其中包含可由另一个模块 (应用程序或 DLL) 使用的函数和数据。

DLL 可以定义两种类型的函数:导出函数和内部函数。 导出的函数旨在由其他模块调用,以及从定义它们的 DLL 中调用。 内部函数通常只能从定义内部函数的 DLL 中调用。 尽管 DLL 可以导出数据,但其数据通常仅由其函数使用。 但是,没有什么可以阻止另一个模块读取或写入该地址。

DLL 提供了一种模块化应用程序的方法,以便可以更轻松地更新和重复使用其功能。 当多个应用程序同时使用相同的功能时,DLL 还有助于减少内存开销,因为尽管每个应用程序都接收自己的 DLL 数据副本,但应用程序会共享 DLL 代码。

windows 应用程序编程接口 (API) 作为一组 DLL 实现,因此使用 Windows API 的任何进程都使用动态链接。

Dynamic-Link库 (动态链接库) - Win32 apps | Microsoft Learn

动态链接允许模块仅包含加载时或运行时查找导出的 DLL 函数所需的信息。 动态链接不同于更熟悉的静态链接,其中链接器将库函数的代码复制到调用它的每个模块中。

动态链接的类型

在 DLL 中调用函数有两种方法:

  • 在 加载时动态链接中,模块显式调用导出的 DLL 函数,就像它们是本地函数一样。 这要求将模块与包含函数的 DLL 的导入库链接。 导入库为系统提供加载 DLL 所需的信息,并在加载应用程序时查找导出的 DLL 函数。
  • 在 运行时动态链接中,模块使用 LoadLibrary 或 LoadLibraryEx 函数在运行时加载 DLL。 加载 DLL 后,模块调用 GetProcAddress 函数以获取导出的 DLL 函数的地址。 该模块使用 GetProcAddress 返回的函数指针调用导出的 DLL 函数。 这样就不需要导入库了。

DLL 和内存管理

加载 DLL 的每个进程都会将其映射到其虚拟地址空间。 进程将 DLL 加载到其虚拟地址后,可以调用导出的 DLL 函数。

系统维护每个 DLL 的每个进程引用计数。 当线程加载 DLL 时,引用计数将增加 1。 当进程终止时,或者当引用计数变为零 (运行时动态链接仅) 时,将从进程的虚拟地址空间中卸载 DLL。

与任何其他函数一样,导出的 DLL 函数在调用它的线程的上下文中运行。 因此,以下条件适用:

  • 调用 DLL 的进程线程可以使用 DLL 函数打开的句柄。 同样,调用进程的任何线程打开的句柄都可以在 DLL 函数中使用。
  • DLL 使用调用线程的堆栈和调用进程的虚拟地址空间。
  • DLL 从调用进程的虚拟地址空间分配内存。

动态链接的优点

动态链接比静态链接具有以下优势:

  • 在同一基址加载同一 DLL 的多个进程在物理内存中共享该 DLL 的单个副本。 这样做可节省系统内存并减少交换。
  • DLL 中的函数发生更改时,只要函数参数、调用约定和返回值不更改,就不需要重新编译或重新链接使用它们的应用程序。 相比之下,静态链接对象代码要求在函数更改时重新链接应用程序。
  • DLL 可以提供市场后支持。 例如,可以修改显示驱动程序 DLL 以支持应用程序最初交付时不可用的显示器。
  • 以不同编程语言编写的程序可以调用同一 DLL 函数,只要这些程序遵循该函数使用的相同调用约定。 调用约定 ((如 C、Pascal 或标准调用) 控制调用函数必须将参数推送到堆栈的顺序、函数还是调用函数负责清理堆栈,以及是否在寄存器中传递任何参数。 有关详细信息,请参阅编译器附带的文档。

使用 DLL 的一个潜在缺点是应用程序不是自包含的;这取决于是否存在单独的 DLL 模块。 如果进程需要未在进程启动时找到的 DLL,系统会使用加载时动态链接终止进程,并向用户提供错误消息。 在这种情况下,系统不会使用运行时动态链接终止进程,但程序无法使用缺少的 DLL 导出的函数。

要创建动态链接库 (DLL),必须创建一个或多个源代码文件,可能还需要创建一个用于导出函数的链接器文件。 如果计划允许使用 DLL 的应用程序使用加载时动态链接,则还必须创建导入库。

创建源文件

DLL 的源文件包含导出的函数和数据、内部函数和数据,以及 DLL 的可选入口点函数。 可以使用支持创建基于 Windows 的 DLL 的任何开发工具。

如果 DLL 可由多线程应用程序使用,则应使 DLL“线程安全”。 若要避免数据损坏,必须同步对 DLL 的所有全局数据的访问。 还必须确保仅链接到线程也安全的库。 例如,Microsoft Visual C++ 包含多个版本的 C 运行时库,其中一个版本不是线程安全的,另外两个版本是线程安全的。

导出函数

如何指定应导出 DLL 中的哪些函数取决于用于开发的工具。 某些编译器使你可使用函数声明中的修饰符直接在源代码中导出函数。 其他情况下,必须在传递给链接器的文件中指定导出。

例如,使用 Visual C++ 时,可通过两种方法导出 DLL 函数:使用 __declspec(dllexport) 修饰符或使用模块定义 (.def) 文件。 如果使用 __declspec(dllexport) 修饰符,则无需使用 .def 文件。 有关详细信息,请参阅从 DLL 导出

创建导入库

导入库 (.lib) 文件包含链接器解析对导出 DLL 函数的外部引用所需的信息,以便系统可以在运行时找到指定的 DLL 和导出的 DLL 函数。 生成 DLL 时,可以为 DLL 创建导入库。

有关详细信息,请参阅生成导入库和导出文件

使用导入库

例如,要调用 CreateWindow 函数,必须将代码链接到导入库 User32.lib。 这是因为 CreateWindow 驻留在名为 User32.dll的系统 DLL 中,并且 User32.lib 是导入库,用于将代码中的调用解析为 User32.dll 中的导出函数。 链接器将创建一个表,其中包含每个函数调用的地址。 加载 DLL 时,将修复对 DLL 中的函数的调用。 当系统正在初始化进程时,由于该进程依赖于该 DLL 中的导出函数,因此它会加载 User32.dll,并会更新函数地址表中的条目。 对 CreateWindow 的所有调用都会调用从 User32.dll 中导出的函数。

Dynamic-Link库Entry-Point函数

DLL 可以选择指定入口点函数。 如果存在,则每当进程或线程加载或卸载 DLL 时,系统会调用入口点函数。 它可用于执行简单的初始化和清理任务。 例如,它可以在创建新线程时设置线程本地存储,并在线程终止时进行清理。

如果将 DLL 与 C 运行时库链接,它可能会提供入口点函数,并允许你提供单独的初始化函数。 有关详细信息,请查看运行时库的文档。

如果要提供自己的入口点,请参阅 DllMain 函数。 名称 DllMain 是用户定义的函数的占位符。 必须指定生成 DLL 时使用的实际名称。 有关详细信息,请参阅开发工具附带的文档。

调用 Entry-Point 函数

每当发生以下任一事件时,系统都调用入口点函数:

  • 进程加载 DLL。 对于使用加载时动态链接的进程,DLL 在进程初始化期间加载。 对于使用运行时链接的进程,DLL 在 LoadLibrary 或 LoadLibraryEx 返回之前加载。
  • 进程卸载 DLL。 当进程终止或调用 FreeLibrary 函数且引用计数变为零时,将卸载 DLL。 如果进程由于 TerminateProcess 或 TerminateThread 函数而终止,则系统不会调用 DLL 入口点函数。
  • 在已加载 DLL 的进程中创建一个新线程。 可以使用 DisableThreadLibraryCalls 函数在创建线程时禁用通知。
  • 加载 DLL 的进程线程正常终止,不使用 TerminateThread 或 TerminateProcess。 当进程卸载 DLL 时,入口点函数仅为整个进程调用一次,而不是为进程的每个现有线程调用一次。 可以使用 DisableThreadLibraryCalls 在线程终止时禁用通知。

一次只能有一个线程可以调用入口点函数。

系统在导致调用函数的进程或线程的上下文中调用入口点函数。 这允许 DLL 使用其入口点函数在调用进程的虚拟地址空间中分配内存,或打开进程可访问的句柄。 入口点函数还可以通过使用线程本地存储 (TLS) 为新线程分配专用的内存。 有关线程本地存储的详细信息,请参阅 线程本地存储

Entry-Point 函数定义

必须使用标准调用调用约定声明 DLL 入口点函数。 如果未正确声明 DLL 入口点,则不会加载 DLL,并且系统会显示一条消息,指示必须使用 WINAPI 声明 DLL 入口点。

在函数主体中,可以处理调用 DLL 入口点的以下方案的任意组合:

  • 进程DLL_PROCESS_ATTACH) 加载 DLL ( 。
  • 当前进程 (DLL_THREAD_ATTACH) 创建新的 线程。
  • 线程通常 (DLL_THREAD_DETACH) 退出。
  • 进程DLL_PROCESS_DETACH) 卸载 DLL ( 。

入口点函数应仅执行简单的初始化任务。 它不得 (调用 LoadLibrary 或 LoadLibraryEx 函数或调用这些函数的函数) ,因为这可能会在 DLL 加载顺序中创建依赖项循环。 这可能会导致在系统执行其初始化代码之前使用 DLL。 同样,入口点函数不得在进程终止期间 (调用 FreeLibrary 函数或调用 FreeLibrary) 函数,因为这可能会导致在系统执行其终止代码后使用 DLL。

由于Kernel32.dll保证在调用入口点函数时加载到进程地址空间中,因此调用 Kernel32.dll 中的函数不会导致在执行其初始化代码之前使用 DLL。 因此,入口点函数可以创建 同步对象 (如关键部分和互斥体),并使用 TLS,因为这些函数位于Kernel32.dll。 例如,调用注册表函数是不安全的,因为它们位于 Advapi32.dll。

调用其他函数可能会导致难以诊断的问题。 例如,调用 User、Shell 和 COM 函数可能会导致访问冲突错误,因为其 DLL 中的某些函数调用 LoadLibrary 来加载其他系统组件。 相反,在终止期间调用这些函数可能会导致访问冲突错误,因为相应的组件可能已卸载或未初始化。

以下示例演示如何构造 DLL 入口点函数。

syntax复制

BOOL WINAPI DllMain(
    HINSTANCE hinstDLL,  // handle to DLL module
    DWORD fdwReason,     // reason for calling function
    LPVOID lpReserved )  // reserved
{
    // Perform actions based on the reason for calling.
    switch( fdwReason ) 
    { 
        case DLL_PROCESS_ATTACH:
         // Initialize once for each new process.
         // Return FALSE to fail DLL load.
            break;

        case DLL_THREAD_ATTACH:
         // Do thread-specific initialization.
            break;

        case DLL_THREAD_DETACH:
         // Do thread-specific cleanup.
            break;

        case DLL_PROCESS_DETACH:
         // Perform any necessary cleanup.
            break;
    }
    return TRUE;  // Successful DLL_PROCESS_ATTACH.
}

Entry-Point函数返回值

当由于加载进程而调用 DLL 入口点函数时,该函数返回 TRUE 以指示成功。 对于使用加载时链接的进程,返回值 FALSE 会导致进程初始化失败,并且进程终止。 对于使用运行时链接的进程,返回值 FALSE 会导致 LoadLibrary 或 LoadLibraryEx 函数返回 NULL,表示失败。 (系统使用 DLL_PROCESS_DETACH 立即调用入口点函数,并卸载 DLL.) 当出于任何其他原因调用该函数时,将忽略入口点函数的返回值。

Load-Time动态链接

当系统启动使用加载时动态链接的程序时,它将使用链接器放置在文件中的信息来查找进程使用的 DLL 的名称。 然后,系统搜索 DLL。 有关详细信息,请参阅动态链接库搜索顺序

如果系统找不到所需的 DLL,它将终止进程并显示一个向用户报告错误的对话框。 否则,系统会将 DLL 映射到进程的虚拟地址空间,并递增 DLL 引用计数。

系统调用入口点函数。 函数接收一个代码,指示进程正在加载 DLL。 如果入口点函数不返回 TRUE,系统将终止进程并报告错误。 有关入口点函数的详细信息,请参阅 动态链接库Entry-Point函数

最后,系统使用导入的 DLL 函数的起始地址修改函数地址表。

DLL 在初始化期间映射到进程的虚拟地址空间,并且仅在需要时才加载到物理内存中。

Run-Time动态链接

当应用程序调用 LoadLibrary 或 LoadLibraryEx 函数时,系统会尝试查找 DLL (以了解详细信息,请参阅 动态链接库搜索顺序) 。 如果搜索成功,系统会将 DLL 模块映射到进程的虚拟地址空间,并递增引用计数。 如果对 LoadLibrary 或 LoadLibraryEx 的调用指定了一个 DLL,该 DLL 的代码已映射到调用进程的虚拟地址空间中,则函数仅返回 DLL 的句柄并递增 DLL 引用计数。 请注意,具有相同基本文件名和扩展名但位于不同目录中的两个 DLL 不被视为同一 DLL。

系统在调用 LoadLibrary 或 LoadLibraryEx 的线程上下文中调用入口点函数。 如果进程已通过对 LoadLibrary 或 LoadLibraryEx 的调用加载了 DLL,但没有对 FreeLibrary 函数的相应调用,则不会调用入口点函数。

如果系统找不到 DLL 或入口点函数返回 FALSE, 则 LoadLibrary 或 LoadLibraryEx 返回 NULL。 如果 LoadLibrary 或 LoadLibraryEx 成功,它将返回 DLL 模块的句柄。 进程可以使用此句柄在调用 GetProcAddress、 FreeLibrary 或 FreeLibraryAndExitThread 函数时标识 DLL。

GetModuleHandle 函数返回 GetProcAddressFreeLibrary 或 FreeLibraryAndExitThread 中使用的句柄。 仅当 DLL 模块已通过加载时链接或先前调用 LoadLibrary 或 LoadLibraryEx 映射到进程的地址空间时,GetModuleHandle 函数才会成功。 与 LoadLibrary 或 LoadLibraryEx 不同, GetModuleHandle 不会递增模块引用计数。 GetModuleFileName 函数检索与 GetModuleHandleLoadLibrary 或 LoadLibraryEx 返回的句柄关联的模块的完整路径。

进程可以使用 GetProcAddress 通过 LoadLibrary 或 LoadLibraryExGetModuleHandle 返回的 DLL 模块句柄获取 DLL 中导出函数的地址。

当不再需要 DLL 模块时,进程可以调用 FreeLibrary 或 FreeLibraryAndExitThread。 如果引用计数为零,这些函数会递减模块引用计数,并从进程的虚拟地址空间取消映射 DLL 代码。

运行时动态链接使进程能够继续运行,即使 DLL 不可用也是如此。 然后,该过程可以使用替代方法来实现其目标。 例如,如果一个进程找不到一个 DLL,它可以尝试使用另一个 DLL,或者它可能会通知用户出错。 如果用户可以提供缺少的 DLL 的完整路径,则进程可以使用此信息加载 DLL,即使它不在正常的搜索路径中。 这种情况与加载时链接形成鲜明对比,在该链接中,如果系统找不到 DLL,系统只会终止进程。

如果 DLL 使用 DllMain 函数对进程的每个线程执行初始化,则运行时动态链接可能会导致问题,因为不会对调用 LoadLibrary 或 LoadLibraryEx 之前存在的线程调用入口点。 有关如何处理此问题的示例,请参阅 在Dynamic-Link库中使用线程本地存储

同一动态链接库 (DLL) 的多个版本通常存在于操作系统 (OS) 内的不同文件系统位置。 可以通过指定完整路径来控制从中加载任何给定 DLL 的特定位置。 但是,如果不使用该方法,则系统会在加载时搜索 DLL,如本主题中所述。 DLL 加载程序是操作系统 (操作系统) 的一部分,用于加载 DLL 和/或解析对 DLL 的引用。

 提示

有关 打包 应用和 未打包 应用的定义,请参阅 打包应用的优缺点

影响搜索的因素

下面是本主题中讨论的一些特殊搜索因素-你可以将其视为 DLL 搜索顺序的一部分。 本主题的后续部分按特定应用类型的相应搜索顺序以及其他搜索位置列出了这些因素。 本部分只是为了介绍概念,并为其提供名称,我们将在本主题的后面部分引用这些概念。

  • DLL 重定向。 有关详细信息,请参阅 动态链接库重定向
  • API 集。 有关详细信息,请参阅 Windows API 集
  • 并行 (SxS) 清单重定向 - 桌面应用仅 (不) UWP 应用。 可以使用应用程序清单 (也称为并行应用程序清单或融合清单) 进行重定向。 有关详细信息,请参阅 清单
  • Loaded-module 列表。 系统可以检查是否已将具有相同模块名称的 DLL 加载到内存 (,无论该 DLL 是从) 加载的。
  • 已知 DLL。 如果 DLL 位于运行应用程序的 Windows 版本的已知 DLL 列表中,则系统会使用其已知 DLL (副本和已知 DLL 的依赖 DLL(如果有任何) )。 有关当前系统上的已知 DLL 的列表,请参阅注册表项 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs

如果 DLL 具有依赖项,则系统会搜索依赖 DLL,就像仅使用其模块名称加载一样。 即使通过指定完整路径加载了第一个 DLL,也是如此。

打包应用的搜索顺序

当打包的应用专门 (加载打包的模块时,库模块( .dll 通过调用 LoadPackagedLibrary 函数) 文件),DLL 必须位于进程的包依赖项关系图中。 有关详细信息,请参阅 LoadPackagedLibrary。 当打包的应用通过其他方式加载模块并且未指定完整路径时,系统会在加载时搜索 DLL 及其依赖项,如本部分所述。

当系统搜索模块或其依赖项时,它始终使用打包应用的搜索顺序;即使依赖项不是打包的应用代码。

打包应用的标准搜索顺序

系统按以下顺序搜索:

  1. DLL 重定向。
  2. API 集。
  3. 桌面应用仅 (UWP 应用) 。 SxS 清单重定向。
  4. Loaded-module 列表。
  5. 已知 DLL。
  6. 进程的包依赖项关系图。 这是应用程序的包,以及应用程序包清单的 节<Dependencies>中指定的任何依赖项<PackageDependency>。 依赖项按它们在清单中的出现顺序进行搜索。
  7. 调用进程从加载的文件夹 (可执行文件的文件夹) 。
  8. 系统文件夹 (%SystemRoot%\system32) 。

如果 DLL 具有依赖项,则系统会搜索依赖 DLL,就好像只加载了其模块名称 (即使第一个 DLL 是通过指定完整路径) 加载的。

打包应用的备用搜索顺序

如果模块通过使用 LOAD_WITH_ALTERED_SEARCH_PATH 调用 LoadLibraryEx 函数更改标准搜索顺序,则搜索顺序与标准搜索顺序相同,只是在步骤 7 中,系统搜索从加载指定模块的文件夹 (顶部加载模块的文件夹) ,而不是可执行文件的文件夹。

未打包应用的搜索顺序

当未打包的应用加载模块且未指定完整路径时,系统会在加载时搜索 DLL,如本节中所述。

 重要

如果攻击者控制了搜索的某个目录,则可以在该文件夹中放置 DLL 的恶意副本。 有关帮助防止此类攻击的方法,请参阅 动态链接库安全性

未打包应用的标准搜索顺序

系统使用的标准 DLL 搜索顺序取决于是否启用了 安全 DLL 搜索模式 。

默认情况下启用的安全 DLL 搜索模式 () 按搜索顺序移动用户的当前文件夹。 若要禁用安全 DLL 搜索模式,请 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode 创建注册表值并将其设置为 0。 当指定文件夹位于搜索路径 () 时,调用 SetDllDirectory 函数可有效地禁用安全 DLL 搜索模式) ,并更改本主题中所述的搜索顺序。

如果启用了安全 DLL 搜索模式,则搜索顺序如下所示:

  1. DLL 重定向。
  2. API sets.
  3. SxS manifest redirection.
  4. Loaded-module list.
  5. Known DLLs.
  6. Windows 11,版本 21H2 (10.0;内部版本 22000) 及更高版本。 The package dependency graph of the process. This is the application's package plus any dependencies specified as <PackageDependency> in the <Dependencies> section of the application's package manifest. Dependencies are searched in the order they appear in the manifest.
  7. 从中加载应用程序的文件夹。
  8. 系统文件夹。 使用 GetSystemDirectory 函数检索此文件夹的路径。
  9. 16 位系统文件夹。 没有获取此文件夹路径的函数,但会对其进行搜索。
  10. Windows 文件夹。 使用 GetWindowsDirectory 函数获取此文件夹的路径。
  11. 当前文件夹。
  12. 环境变量中列出的 PATH 目录。 这不包括由应用路径注册表项指定的每 应用程序路径 。 计算 DLL 搜索路径时,不使用 应用 路径键。

如果 禁用安全 DLL 搜索模式,则搜索顺序相同,只是 当前文件夹 在步骤 7 之后立即从序列 (从位置 11 移动到位置 8 。应用程序从中加载) 的文件夹 。

未打包应用的备用搜索顺序

若要更改系统使用的标准搜索顺序,可以使用 LOAD_WITH_ALTERED_SEARCH_PATH调用 LoadLibraryEx 函数。 还可以通过调用 SetDllDirectory 函数来更改标准搜索顺序。

 备注

在当前进程开始之前,在父进程中调用 SetDllDirectory 函数也会影响进程的标准搜索顺序。

如果指定备用搜索策略,则其行为会一直持续到找到所有关联的可执行模块。 在系统开始处理 DLL 初始化例程后,系统将恢复为标准搜索策略。

如果调用指定LOAD_WITH_ALTERED_SEARCH_PATH,并且 lpFileName 参数指定绝对路径,则 LoadLibraryEx 函数支持备用搜索顺序。

  • 在调用应用程序的文件夹中) 初始步骤后,标准搜索策略开始 (。
  • 在 LoadLibraryEx 正在加载的可执行模块的文件夹中的初始步骤) 后,使用 LOAD_WITH_ALTERED_SEARCH_PATH 的 LoadLibraryEx 指定的备用搜索策略开始 (。

这是他们区别的唯一方式。

如果启用了安全 DLL 搜索模式,则备用搜索顺序如下所示:

步骤 1-6 与标准搜索顺序相同。

  1. 由 lpFileName 指定的文件夹。
  2. The system folder. Use the GetSystemDirectory function to retrieve the path of this folder.
  3. The 16-bit system folder. There's no function that obtains the path of this folder, but it is searched.
  4. The Windows folder. 使用 GetWindowsDirectory 函数获取此文件夹的路径。
  5. 当前文件夹。
  6. 环境变量中列出的 PATH 目录。 这不包括应用路径注册表项指定的每 应用程序路径 。 计算 DLL 搜索路径时,不使用 应用 路径密钥。

如果 禁用安全 DLL 搜索模式,则备用搜索顺序是相同的,只是 当前文件夹 在步骤 7 后立即从顺序 (从位置 11 移动到位置 8 。由 lpFileName 指定的文件夹) 。

如果 lpPathName 参数指定路径,SetDllDirectory 函数支持备用搜索顺序。 备用搜索顺序如下:

步骤 1-6 与标准搜索顺序相同。

  1. 从中加载应用程序的文件夹。
  2. 由 SetDllDirectory 的 lpPathName 参数指定的文件夹。
  3. 系统文件夹。
  4. 16 位系统文件夹。
  5. Windows 文件夹。
  6. 环境变量中列出的 PATH 目录。

如果 lpPathName 参数为空字符串,则调用将从搜索顺序中删除当前文件夹。

当指定文件夹位于搜索路径中时,SetDllDirectory 可有效地禁用安全 DLL 搜索模式。 若要基于 SafeDllSearchMode 注册表值还原安全 DLL 搜索模式,并将当前文件夹还原到搜索顺序,请调用 SetDllDirectory , lpPathName 为 NULL。

使用LOAD_LIBRARY_SEARCH标志搜索顺序

可以通过将一个或多个 LOAD_LIBRARY_SEARCH 标志与 LoadLibraryEx 函数配合使用来指定搜索顺序。 还可以将 LOAD_LIBRARY_SEARCH 标志与 SetDefaultDllDirectories 函数一起使用,以建立进程的 DLL 搜索顺序。 可以使用 AddDllDirectory 或 SetDllDirectory 函数为进程 DLL 搜索顺序指定其他目录。

搜索的目录取决于使用 SetDefaultDllDirectories 或 LoadLibraryEx 指定的标志。 如果使用多个标志,则按以下顺序搜索相应的目录:

  1. LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR。 搜索包含 DLL 的文件夹。 此文件夹仅搜索要加载的 DLL 的依赖项。
  2. LOAD_LIBRARY_SEARCH_APPLICATION_DIR。 搜索应用程序文件夹。
  3. LOAD_LIBRARY_SEARCH_USER_DIRS。 搜索使用 AddDllDirectory 函数或 SetDllDirectory 函数显式添加的路径。 如果添加多个路径,则未指定搜索路径的顺序。
  4. LOAD_LIBRARY_SEARCH_SYSTEM32。 搜索“系统”文件夹。

如果调用 LoadLibraryEx 时没有 LOAD_LIBRARY_SEARCH 标志,或者为进程建立 DLL 搜索顺序,则系统将使用标准搜索顺序或备用搜索顺序搜索 DLL。

Dynamic-Link库 (DLL) 可以包含全局数据或本地数据。

变量范围

编译器和链接器将 DLL 源代码文件中声明为全局的变量视为全局变量,但加载给定 DLL 的每个进程都会获取该 DLL 的全局变量的自身副本。 静态变量的范围仅限于在其中声明静态变量的块。 因此,默认情况下,每个进程都有自己的 DLL 全局变量和静态变量实例。

 备注

开发工具可能允许重写默认行为。 例如,Visual C++ 编译器支持 #pragma 节 ,链接器支持 /SECTION 选项。 有关详细信息,请参阅开发工具附带的文档。

动态内存分配

当 DLL 使用 GlobalAlloc、 LocalAlloc、 HeapAlloc 和 VirtualAlloc) 的任何内存分配 (函数分配内存时,内存在调用进程的虚拟地址空间中分配,并且只能由该进程的线程访问。

DLL 可以使用文件映射来分配可在进程之间共享的内存。 有关如何使用文件映射创建命名共享内存的一般讨论,请参阅 文件映射。 有关使用 DllMain 函数通过文件映射设置共享内存的示例,请参阅 在Dynamic-Link库中使用共享内存

线程本地存储

线程本地存储 (TLS) 函数使 DLL 能够分配索引,以便为多线程进程的每个线程存储和检索不同的值。 例如,每次用户打开新电子表格时,电子表格应用程序都可以创建同一线程的新实例。 为各种电子表格操作提供函数的 DLL 可以使用 TLS 保存有关每个电子表格的当前状态的信息, (行、列等) 。 有关线程本地存储的一般讨论,请参阅 线程本地存储。 有关使用 DllMain 函数设置线程本地存储的示例,请参阅 在Dynamic-Link库中使用线程本地存储

Windows Server 2003 和 Windows XP: Visual C++ 编译器支持用于声明线程局部变量的语法: _declspec (线程) 。 如果在 DLL 中使用此语法,将无法在 Windows Vista 之前的 Windows 版本上使用 LoadLibrary 或 LoadLibraryEx 显式加载 DLL。 如果 DLL 将被显式加载,则必须使用线程本地存储函数,而不是 _declspec (线程) 

DLL 加载器是操作系统 (OS) 的一部分,用于解析对 DLL 的引用、加载和链接 DLL。 有很多技术可影响 DLL 加载器的行为,并控制它实际加载几个候选 DLL 中的哪一个,而动态链接库 (DLL) 重定向就是这种技术之一。

此功能还有其他名称,例如 .local、Dot Local、DotLocal 和 Dot Local Debugging。

DLL 版本控制问题

如果你的应用程序依赖于共享 DLL 的特定版本,而另一个应用程序随该 DLL 的更高版本或更低版本一起安装,这可能会导致兼容性问题和不稳定性,即它可能会导致应用启动失败。

DLL 加载器先查找从中加载调用进程的文件夹(可执行文件的文件夹),然后再查找其他文件系统位置。 因此,一种解决方法是在可执行文件的文件夹中安装应用所需的 DLL。 这有效地使 DLL 成为私有 DLL。

但是,这并不能解决 COM 的问题。 可安装并注册两个不兼容的 COM 服务器版本(即使在不同的文件系统位置),但只有一个位置用于注册 COM 服务器。 因此,只会激活最新注册的 COM 服务器。

可使用重定向来解决这些问题。

加载和测试专用二进制文件

DLL 加载器遵循的规则可确保从 Windows 系统位置(例如系统文件夹 %SystemRoot%\system32)加载系统 DLL。 这些规则可避免植入式攻击;在这种攻击中,攻击者将他们编写的代码放到他们可写入的位置,然后说服一些进程加载并执行它。 但是,加载器的规则也使得在 OS 组件上工作变得更加困难,因为运行它们需要更新系统;这是一个非常有影响力的变化。

但是,可以使用重定向来加载 DLL 的专用副本(例如为了测试或测量代码更改对性能的影响)。

如果想要对公共 WindowsAppSDK GitHub 存储库中的源代码做出贡献,需要对所做更改进行测试。 同样,在这种情况下,你可以使用重定向来加载 DLL 的专用副本,而不是 Windows 应用 SDK 附带的版本。

你的选项

事实上,有两种方法来确保你的应用使用你希望它实现以下操作的 DLL 版本:

 提示

如果你是开发人员或管理员,则应对现有应用程序使用 DLL 重定向。 这是因为它不需要对应用本身进行任何更改。 但是,如果要创建新应用或更新现有应用,并且想要将你的应用与潜在问题分隔开,请创建并行组件。

可选:配置注册表

若要在计算机范围启用 DLL 重定向,必须创建新的注册表值。 在 HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options 项下,使用 DevOverrideEnable 名称创建新的 DWORD 值。 将值设置为 1,然后重启计算机。 或者,使用以下命令(并重启计算机)。

控制台复制

reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options" /v DevOverrideEnable /t REG_DWORD /d 1

设置注册表值后,即使应用具有应用程序清单,也遵循 DotLocal DLL 重定向。

创建重定向文件或文件夹

若要使用 DLL 重定向,需要创建重定向文件或重定向文件夹(具体取决于你拥有的应用类型),如本主题后面的部分所示。

如何重定向打包应用的 DLL

对于 DLL 重定向,打包的应用需要特殊的文件夹结构。 如果启用了重定向,加载器将在以下路径进行查找:

<Drive>:\<path_to_package>\microsoft.system.package.metadata\application.local\

如果能够编辑 .vcxproj 文件,那么若要使用包创建和部署该特定文件夹,一种简便的方法是在 .vcxproj 中向生成项添加一些额外步骤:

XML复制

<ItemDefinitionGroup>
    <PreBuildEvent>
        <Command>
            del $(FinalAppxManifestName) 2&gt;nul
            <!-- [[Using_.local_(DotLocal)_with_a_packaged_app]] This makes the extra DLL deployed via F5 get loaded instead of the system one. -->
            if NOT EXIST $(IntDir)\microsoft.system.package.metadata\application.local MKDIR $(IntDir)\microsoft.system.package.metadata\application.local
            if EXIST "&lt;A.dll&gt;" copy /y "&lt;A.dll&gt;" $(IntDir)\microsoft.system.package.metadata\application.local 2&gt;nul
            if EXIST "&lt;B.dll&gt;" copy /y "&lt;B.dll&gt;" $(IntDir)\microsoft.system.package.metadata\application.local 2&gt;nul
        </Command>
    </PreBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
    <!-- Include any locally built system experience -->
    <Media Include="$(IntDir)\microsoft.system.package.metadata\application.local\**">
        <Link>microsoft.system.package.metadata\application.local</Link>
    </Media>
</ItemGroup>

让我们来看看该配置的一些作用。

  1. 为 Visual Studio 的“启动(不调试)”(或“启动调试”)体验设置 PreBuildEvent

    XML复制

    <ItemDefinitionGroup>
      <PreBuildEvent>
    
  2. 确保中间目录中具有正确的文件夹结构。

    XML复制

    <!-- [[Using_.local_(DotLocal)_with_modern_apps]] This makes the extra DLL deployed via Start get loaded instead of the system one. -->
    if NOT EXIST $(IntDir)\microsoft.system.package.metadata\application.local MKDIR $(IntDir)\microsoft.system.package.metadata\application.local
    
  3. 将本地生成的任何 DLL(并希望优先于系统部署的 DLL 使用)复制到 application.local 目录中。 可以几乎从任意位置选取 DLL(建议你对自己的 .vcxproj 使用可用宏)。 只需确保这些 DLL 在该项目之前生成;否则,它们会缺失。 此处显示了两个模板复制命令;请根据需要使用任意数量的模板复制命令,并编辑 <path-to-local-dll> 占位符。

    XML复制

      if EXIST "<path-to-local-dll>" copy /y "<path-to-local-dll>" $(IntDir)\microsoft.system.package.metadata\application.local 2&gt;nul
      if EXIST "<path-to-local-dll>" copy /y "<path-to-local-dll>" $(IntDir)\microsoft.system.package.metadata\application.local 2&gt;nul
      </Command>
    </PreBuildEvent>
    
  4. 最后,指示你想要在部署的包中包含特殊目录及其内容。

    XML复制

    <ItemGroup>
      <!-- Include any locally built system experience -->
      <Media Include="$(IntDir)\microsoft.system.package.metadata\application.local\**">
        <Link>microsoft.system.package.metadata\application.local</Link>
      </Media>
    </ItemGroup>
    

这里描述的方法(即使用中间目录)使源代码控制登记保持干净,并减少意外提交已编译的二进制文件的可能性。

接下来,只需(重新)部署项目即可。 为了获得干净、完整的(重新)部署,可能还需要卸载/清除目标设备上的现有部署。

手动复制二进制文件

如果无法按照上面所示的方式使用 .vcxproj,可在目标设备中通过简单几步操作实现相同的目的。

  1. 确定包的安装文件夹。 为此,可在 PowerShell 中发出 Get-AppxPackage 命令,并查找返回的 InstallLocation。

  2. 使用该 InstallLocation 来更改 ACL,以便可自行创建文件夹/复制文件。 编辑此脚本中的 <InstallLocation> 占位符并运行脚本:

    控制台复制

    cd <InstallLocation>\Microsoft.system.package.metadata
    takeown /F . /A
    icacls  . /grant Administrators:F
    md <InstallLocation>\Microsoft.system.package.metadata\application.local
    
  3. 最后,将本地生成的任何 DLL(并希望优先于系统部署的 DLL 使用)手动复制到 application.local 目录中,并(重新)启动应用。

验证所有内容是否正常工作

若要确认在运行时加载正确的 DLL,可以将 Visual Studio 与附加的调试程序一起使用。

  1. 打开“模块”窗口(“调试”>“Windows”>“模块”)。
  2. 找到 DLL,并确保路径指示重定向的副本,而不是系统部署的版本。
  3. 确认只加载给定 DLL 的一个副本。

如何重定向未打包的应用的 DLL

必须将重定向文件命名为 <your_app_name>.local。 因此,如果应用的名称为 Editor.exe,则将重定向文件命名为 Editor.exe.local。 必须在可执行文件的文件夹中安装重定向文件。 还必须在可执行文件的文件夹中安装 DLL。

重定向文件的内容将被忽略;只是存在该文件就会导致 DLL 加载器在每次加载 DLL 时都先检查可执行文件的文件夹。 为了缓解 COM 问题,该重定向同时应用于完整路径加载和部分名称加载。 因此,COM 情况下会发生重定向,而且无论路径指定为 LoadLibrary 还是 LoadLibraryEx 都会发生。 如果在可执行文件的文件夹中找不到 DLL,则加载遵循其通常的搜索顺序。 例如,如果应用 C:\myapp\myapp.exe 使用以下路径调用 LoadLibrary:

C:\Program Files\Common Files\System\mydll.dll

如果同时存在 C:\myapp\myapp.exe.local 和 C:\myapp\mydll.dllLoadLibrary 会加载 C:\myapp\mydll.dll。 否则,LoadLibrary 将加载 C:\Program Files\Common Files\System\mydll.dll

或者,如果存在名为 C:\myapp\myapp.exe.local 的文件夹,而且它包含 mydll.dll,则 LoadLibrary 将加载 C:\myapp\myapp.exe.local\mydll.dll

如果使用 DLL 重定向,并且应用无权按搜索顺序访问所有驱动器和目录,LoadLibrary 会在访问被拒绝后立即停止搜索。 如果不使用 DLL 重定向,LoadLibrary 会跳过它无法访问的目录,然后继续搜索。

最好在包含应用的同一文件夹中安装应用 DLL,即使未使用 DLL 重定向也是如此。 这可确保安装应用不会覆盖 DLL 的其他副本(覆盖会导致其他应用失败)。 此外,如果遵循此良好做法,其他应用不会覆盖你的 DLL 副本(而且不会导致应用失败)。

有时需要将 DLL 替换为较新版本。 在替换 DLL 之前,请执行版本检查,以确保将旧版本替换为较新版本。 可以替换正在使用的 DLL。 用于替换正在使用的 DLL 的方法取决于所使用的操作系统。 在 Windows XP 及更高版本上,应用程序应使用 独立应用程序和并行程序集

如果执行以下步骤,则无需重新启动计算机:

  1. 使用 MoveFileEx 函数重命名要替换的 DLL。 不要指定MOVEFILE_COPY_ALLOWED,并确保重命名的文件位于包含原始文件的同一卷上。 还可以通过为同一目录中的文件提供不同的扩展名来重命名该文件。
  2. 将新 DLL 复制到包含重命名 DLL 的目录。 现在,所有应用程序都将使用新的 DLL。
  3. 将 MoveFileEx 与 MOVEFILE_DELAY_UNTIL_REBOOT 配合使用可删除重命名的 DLL。

在进行此替换之前,应用程序将使用原始 DLL,直到卸载它。 进行替换后,应用程序将使用新的 DLL。 编写 DLL 时,必须小心确保它已准备好应对这种情况,尤其是在 DLL 维护全局状态信息或与其他服务通信时。 如果 DLL 未准备好更改全局状态信息或通信协议,更新 DLL 需要重新启动计算机,以确保所有应用程序都使用同一版本的 DLL。

当应用程序动态加载动态链接库而不指定完全限定的路径名称时,Windows 会尝试通过按特定顺序搜索一组定义完善的目录来查找 DLL,如 动态链接库搜索顺序中所述。 如果攻击者控制 DLL 搜索路径上的某个目录,则可以在该目录中放置 DLL 的恶意副本。 这有时称为 DLL 预加载攻击 或 二进制植入攻击。 如果系统在搜索受入侵的目录之前找不到 DLL 的合法副本,则会加载恶意 DLL。 如果应用程序以管理员权限运行,则攻击者可能会在本地特权提升中成功。

例如,假设应用程序旨在从用户的当前目录加载 DLL,如果找不到 DLL,则正常失败。 应用程序仅使用 DLL 的名称调用 LoadLibrary ,这会导致系统搜索 DLL。 假设启用了安全 DLL 搜索模式,并且应用程序未使用备用搜索顺序,则系统按以下顺序搜索目录:

  1. 从中加载应用程序的目录。
  2. 系统目录。
  3. 16 位系统目录。
  4. Windows 目录。
  5. 当前目录。
  6. PATH 环境变量中列出的目录。

继续本示例,了解应用程序的攻击者获取当前目录的控制,并将 DLL 的恶意副本置于该目录中。 当应用程序发出 LoadLibrary 调用时,系统会搜索 DLL,在当前目录中查找 DLL 的恶意副本,然后加载它。 然后,DLL 的恶意副本在应用程序中运行,并获取用户的权限。

开发人员可以遵循以下准则,帮助保护其应用程序免受 DLL 预加载攻击:

  • 尽可能在使用 LoadLibrary、LoadLibraryExCreateProcess 或 ShellExecute 函数时指定完全限定的路径。

  • 将 LOAD_LIBRARY_SEARCH 标志与 LoadLibraryEx 函数一起使用,或将这些标志与 SetDefaultDllDirectories 函数一起使用,以建立进程的 DLL 搜索顺序,然后使用 AddDllDirectory 或 SetDllDirectory 函数修改列表。 有关详细信息,请参阅动态链接库搜索顺序

    Windows 7、Windows Server 2008 R2、Windows Vista 和 Windows Server 2008: 这些标志和函数在安装了 KB2533623 的系统上可用。

  • 在安装了 KB2533623 的系统上,将 LOAD_LIBRARY_SEARCH 标志与 LoadLibraryEx 函数一起使用,或者将这些标志与 SetDefaultDllDirectories 函数一起使用,为进程建立 DLL 搜索顺序,然后使用 AddDllDirectory 或 SetDllDirectory 函数修改列表。 有关详细信息,请参阅动态链接库搜索顺序

  • 请考虑使用 DLL 重定向 或 清单 ,以确保应用程序使用正确的 DLL。

  • 使用标准搜索顺序时,请确保已启用安全 DLL 搜索模式。 这会将用户的当前目录置于搜索顺序的后面,从而增加了 Windows 在恶意复制之前找到 DLL 的合法副本的可能性。 默认情况下,安全 DLL 搜索模式从 Windows XP 开始启用,Service Pack 2 (SP2) ,并由 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode 注册表值控制。 有关详细信息,请参阅动态链接库搜索顺序

  • 请考虑使用空字符串 (“”) 调用 SetDllDirectory ,从标准搜索路径中删除当前目录。 这应该在进程初始化的早期完成一次,而不是在调用 LoadLibrary 之前和之后完成。 请注意 ,SetDllDirectory 会影响整个进程,并且具有不同值的多个调用 SetDllDirectory 的线程可能会导致未定义的行为。 如果应用程序加载第三方 DLL,请仔细测试以识别任何不兼容情况。

  • 除非启用了安全进程搜索模式,否则不要使用 SearchPath 函数检索 DLL 路径以便进行后续 LoadLibrary 调用。 如果未启用安全进程搜索模式, SearchPath 函数使用的搜索顺序与 LoadLibrary 不同,并且可能首先在用户的当前目录中搜索指定的 DLL。 若要为 SearchPath 函数启用安全进程搜索模式,请将 SetSearchPathMode 函数与 BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE一起使用。 这会在进程生命周期内将当前目录移动到 SearchPath 搜索列表的末尾。 请注意,当前目录不会从搜索路径中删除,因此,如果系统在到达当前目录之前找不到 DLL 的合法副本,则应用程序仍然易受攻击。 与 SetDllDirectory 一样,调用 SetSearchPathMode 应在进程初始化的早期完成,这会影响整个过程。 如果应用程序加载第三方 DLL,请仔细测试以识别任何不兼容情况。

  • 不要基于搜索 DLL 的 LoadLibrary 调用来假设操作系统版本。 如果应用程序在 DLL 合法不存在的环境中运行,但该 DLL 的恶意副本位于搜索路径中,则可能会加载 DLL 的恶意副本。 请改用 获取系统版本中所述的推荐技术。

进程监视器工具可用于帮助识别可能易受攻击的 DLL 加载操作。 可以从 下载 Process Monitor - Sysinternals | Microsoft Learn进程监视器工具。

以下过程介绍如何使用进程监视器检查应用程序中的 DLL 加载操作。

使用进程监视器检查应用程序中的 DLL 加载操作

  1. 启动进程监视器。
  2. 在进程监视器中,包括以下筛选器:
    • 操作为 CreateFile
    • 操作为 LoadImage
    • 路径包含.cpl
    • 路径包含.dll
    • 路径包含 .drv
    • 路径包含.exe
    • 路径包含 .ocx
    • 路径包含 .scr
    • 路径包含.sys
  3. 排除以下筛选器:
    • 进程名称procmon.exe
    • 进程名称Procmon64.exe
    • 进程名称为系统
    • 操作从IRP_MJ_开始
    • 操作从FASTIO_开始
    • 结果为 SUCCESS
    • 路径以pagefile.sys结尾
  4. 尝试将当前目录设置为特定目录来启动应用程序。 例如,双击扩展名为应用程序分配的文件处理程序的文件。
  5. 检查进程监视器输出中是否存在可疑的路径,例如调用当前目录以加载 DLL。 此类调用可能表示应用程序中存在漏洞。

AppInit DLL 和安全启动

从Windows 8开始,启用安全启动后,将禁用AppInit_DLLs基础结构。

关于AppInit_DLLs

AppInit_DLLs基础结构允许将自定义 DLL 加载到每个交互式应用程序的地址空间中,从而提供了一种简单的方法来挂钩系统 API。 应用程序和恶意软件都使用 AppInit DLL 的基本原因相同,即挂钩 API;加载自定义 DLL 后,它可以挂钩已知的系统 API 并实现备用功能。 只有一小部分新式合法应用程序使用此机制来加载 DLL,而大量恶意软件使用此机制来破坏系统。 即使是合法的AppInit_DLLs也会无意中导致系统死锁和性能问题,因此不建议使用AppInit_DLLs。

AppInit_DLLs和安全启动

Windows 8采用 UEFI 和安全启动来提高整体系统完整性,并针对复杂威胁提供强大的保护。 启用安全启动后,将禁用AppInit_DLLs机制,作为保护客户免受恶意软件和威胁的不妥协方法的一部分。

请注意,安全启动是 UEFI 协议,而不是Windows 8功能。 有关 UEFI 和安全启动协议规范的详细信息,请参阅 https://www.uefi.org

Windows 8桌面应用的AppInit_DLLs认证要求

Windows 8桌面应用的认证要求之一是,应用不得加载任意 DLL 以使用AppInit_DLLs机制截获 Win32 API 调用。 有关认证要求的详细信息,请参阅Windows 8桌面应用的认证要求的第 1.1 部分。

总结

  • 对于合法应用程序,不建议使用AppInit_DLLs机制,因为它可能导致系统死锁和性能问题。
  • 启用安全启动后,默认禁用AppInit_DLLs机制。
  • 在Windows 8桌面应用中使用AppInit_DLLs是 Windows 桌面应用认证失败。

若要下载包含 Windows 7 和 Windows Server 2008 R2 上AppInit_DLLs信息的白皮书,请访问 Windows 硬件开发人员中心存档,并在 Windows 7 和 Windows Server 2008 R2 中搜索 AppInit DLL

创建 DLL 为开发人员带来了许多挑战。 DLL 没有系统强制执行的版本控制。 当系统上存在多个版本的 DLL 时,由于很容易被覆盖,且缺少版本控制架构,会导致产生依赖项和 API 冲突。 开发环境中的复杂性、加载器实现和 DLL 依赖项造成了加载顺序和应用程序行为方面的脆弱性。 最后,许多应用程序依赖于 DLL,并且具有复杂的依赖项集,应用程序必须遵循它们才能正常运行。 本文档为 DLL 开发人员提供了指南,帮助构建更可靠、可移植和可扩展的 DLL。

DllMain 中的不当同步可能会导致应用程序在未初始化的 DLL 中死锁或访问数据或代码。 从 DllMain 中调用某些函数会导致此类问题。

what happens when a library is loaded

常规最佳做法

DllMain 在加载器锁被持有时调用。 因此,对可以在 DllMain 中调用的函数施加了重大限制。 因此,DllMain 旨在通过使用 Microsoft® Windows® API 的一小部分来执行最小的初始化任务。 不能调用 DllMain 中直接或间接尝试获取加载器锁的任何函数。 否则,将引入应用程序死锁或崩溃的可能性。 DllMain 实现中的错误可能会危及整个进程及其所有线程。

理想的 DllMain 只是一个空存根。 但是,鉴于许多应用程序的复杂性,这通常过于严格。 DllMain 的一个很好的经验法则是尽可能地推迟初始化。 延迟初始化会增加应用程序的稳定性,因为加载器锁被持有时不会执行此类初始化。 此外,延迟初始化使你能够安全地使用更多 Windows API 功能。

某些初始化任务无法推迟。 例如,如果文件格式不正确或包含垃圾,则依赖于配置文件的 DLL 应无法加载。 对于这种类型的初始化,DLL 应会尝试操作并快速失败,而不是通过完成其他工作来浪费资源。

不应从 DllMain 中执行以下任务:

  • 调用 LoadLibrary 或 LoadLibraryEx(直接或间接)。 这可能会导致死锁或崩溃。
  • 调用 GetStringTypeAGetStringTypeEx 或 GetStringTypeW(直接或间接)。 这可能会导致死锁或崩溃。
  • 与其他线程同步。 这可能会导致死锁。
  • 获取由等待获取加载器锁的代码拥有的同步对象。 这可能会导致死锁。
  • 使用 CoInitializeEx 初始化 COM 线程。 在某些情况下,此函数可以调用 LoadLibraryEx
  • 调用注册表函数。
  • 调用 CreateProcess。 创建进程时可能会加载另一个 DLL。
  • 调用 ExitThread。 在 DLL 分离期间退出线程可能会导致加载器锁再次被获取,从而导致死锁或崩溃。
  • 调用 CreateThread。 如果不与其他线程同步,则创建线程可以正常工作,但存在风险。
  • 调用 ShGetFolderPathW。 调用 shell/已知文件夹 API 可能会导致线程同步,因此可能会导致死锁。
  • 创建命名管道或其他命名对象(仅限 Windows 2000)。 在 Windows 2000 中,命名对象由终端服务 DLL 提供。 如果未初始化此 DLL,则对 DLL 的调用可能会导致进程崩溃。
  • 使用动态 C 运行时 (CRT) 中的内存管理功能。 如果未初始化 CRT DLL,则对这些函数的调用可能会导致进程崩溃。
  • 调用 User32.dll 或 Gdi32.dll 中的函数。 某些函数会加载另一个 DLL,该 DLL 可能无法初始化。
  • 使用托管代码。

可以在 DllMain 中安全地执行以下任务:

  • 在编译时初始化静态数据结构和成员。
  • 创建和初始化同步对象。
  • 分配内存并初始化动态数据结构(避免上面列出的函数)。
  • 设置线程本地存储 (TLS)。
  • 打开、读取和写入文件。
  • 调用 Kernel32.dll 中的函数(上面列出的函数除外)。
  • 将全局指针设置为 NULL,从而推迟动态成员的初始化。 在 Microsoft Windows Vista™ 中,可以使用一次性初始化函数来确保在多线程环境中只执行一次代码块。

锁顺序反转导致的死锁

实现使用多个同步对象(如锁)的代码时,必须遵循锁顺序。 如果需要一次获取多个锁,则必须定义一个称为锁层次结构或锁顺序的显式优先级。 例如,如果在代码中的某个位置在锁 B 之前获取了锁 A,并在代码中的其他位置在锁 C 之前获取了锁 B,则锁顺序为 A、B、C,并且应在整个代码中遵循此顺序。 锁顺序反转发生在未遵循锁顺序时,例如,如果在锁 A 之前获取了锁 B。锁顺序反转可能会导致难以调试的死锁。 为了避免此类问题,所有线程必须按相同的顺序获取锁。

请务必注意,加载器使用已获取的加载器锁调用 DllMain,因此加载器锁在锁层次结构中应具有最高优先级。 另请注意,代码只需要获取正确同步所需的锁,它不必获取层次结构中定义的每个锁。 例如,如果代码的某个部分只需锁 A 和 C 就能进行正确同步,则代码应在获取锁 C 之前获取锁 A,且代码不需要也获取锁 B。此外,DLL 代码无法显式获取加载器锁。 如果代码必须调用可以间接获取加载器锁的 API(例如 GetModuleFileName),并且代码还必须获取专用锁,则代码应在获取锁 P 之前调用 GetModuleFileName,从而确保遵守加载顺序。

图 2 是说明锁顺序反转的示例。 假设有一个主线程包含 DllMain 的 DLL。 库加载器会获取加载器锁 L,然后调用 DllMain。 主线程会创建同步对象 A、B 和 G 以序列化对其数据结构的访问,然后尝试获取锁 G。已成功获取锁 G 的工作线程随后调用尝试获取加载器锁 L 的函数,例如 GetModuleHandle。因此,工作线程在 L 上被阻止,主线程在 G 上被阻止,从而导致死锁。

deadlock caused by lock order inversion

要防止由锁顺序反转导致的死锁,所有线程都应始终尝试按定义的加载顺序获取同步对象。

同步最佳做法

假设有一个 DLL 会在初始化中创建工作线程。 DLL 清理后,必须与所有工作线程同步,以确保数据结构处于一致状态,然后终止工作线程。 目前,无法完全直接解决在多线程环境中干净地同步和关闭 DLL 的问题。 本部分介绍在 DLL 关闭期间线程同步的当前最佳做法。

进程退出期间 DllMain 中的线程同步

  • 在进程退出时调用 DllMain 时,所有进程的线程都已被强行清理,并且地址空间有可能不一致。 在这种情况下,不需要同步。 换句话说,理想的 DLL_PROCESS_DETACH 处理程序为空。
  • Windows Vista 会确保核心数据结构(环境变量、当前目录、进程堆等)处于一致状态。 但是,其他数据结构可能会损坏,因此清理内存并不安全。
  • 需要保存的持久状态必须刷新到永久存储。

DLL 卸载期间 DLL_THREAD_DETACH 的 DllMain 中的线程同步

  • 卸载 DLL 时,不会丢弃地址空间。 因此,DLL 应执行干净关闭。 这包括线程同步、打开的句柄、持久状态和分配的资源。
  • 线程同步很棘手,因为等待线程在 DllMain 中退出可能会导致死锁。 例如,DLL A 持有加载器锁。 它指示线程 T 退出,并等待线程退出。 线程 T 退出,加载器尝试获取加载器锁,以使用 DLL_THREAD_DETACH 调用 DLL A 的 DllMain。 这会导致死锁。 要将死锁的风险降到最低:
    • DLL A 在其 DllMain 中获取 DLL_THREAD_DETACH 消息,并为线程 T 设置事件,指示其退出。
    • 线程 T 完成其当前任务,使自身处于一致状态,向 DLL A 发出信号,并无限等待。 请注意,一致性检查例程应遵循与 DllMain 相同的限制,以避免死锁。
    • DLL A 终止 T,知道它处于一致状态。

如果 DLL 在其所有线程创建后且它们开始执行前被卸载,则这些线程可能会崩溃。 如果 DLL 在其 DllMain 中作为初始化的一部分创建了线程,则某些线程可能尚未完成初始化,并且其 DLL_THREAD_ATTACH 消息仍在等待被传递到 DLL。 在这种情况下,如果卸载 DLL,它将开始终止线程。 但是,某些线程可能会被挡在加载器锁后面。 它们的 DLL_THREAD_ATTACH 消息会在取消映射 DLL 后处理,从而导致进程崩溃。

建议

建议遵循以下准则:

  • 使用应用程序验证工具捕获 DllMain 中最常见的错误。
  • 如果在 DllMain 中使用专用锁,请定义锁层次结构并一致地使用它。 加载器锁必须位于此层次结构的底部。
  • 验证是否没有任何调用依赖于另一个可能尚未完全加载的 DLL。
  • 在编译时静态执行简单初始化,而不是在 DllMain 中执行。
  • 推迟 DllMain 中任何可以稍后再进行的调用。
  • 推迟可以稍后再进行的初始化任务。 必须尽早检测到某些错误条件,以便应用程序可以正常处理错误。 但是,这种早期检测与可靠性丢失

以下示例是创建简单 DLL 所需的源代码,Myputs.dll。 它定义一个名为 myPuts 的简单字符串打印函数。 Myputs DLL 不定义入口点函数,因为它与 C 运行时库链接,并且没有其自己的初始化或清理函数可执行。

若要生成 DLL,请按照开发工具随附的文档中的说明进行操作。

有关使用 myPuts 的示例,请参阅 使用Load-Time动态链接 或使用 Run-Time动态链接

C++复制

// The myPuts function writes a null-terminated string to
// the standard output device.
 
// The export mechanism used here is the __declspec(export)
// method supported by Microsoft Visual Studio, but any
// other export method supported by your development
// environment may be substituted.
 
 
#include <windows.h>
 
#define EOF (-1)
 
#ifdef __cplusplus    // If used by C++ code, 
extern "C" {          // we need to export the C interface
#endif
 
__declspec(dllexport) int __cdecl myPuts(LPCWSTR lpszMsg)
{
    DWORD cchWritten;
    HANDLE hConout;
    BOOL fRet;
 
    // Get a handle to the console output device.

    hConout = CreateFileW(L"CONOUT$",
                         GENERIC_WRITE,
                         FILE_SHARE_WRITE,
                         NULL,
                         OPEN_EXISTING,
                         FILE_ATTRIBUTE_NORMAL,
                         NULL);

    if (INVALID_HANDLE_VALUE == hConout)
        return EOF;
 
    // Write a null-terminated string to the console output device.
 
    while (*lpszMsg != L'\0')
    {
        fRet = WriteConsole(hConout, lpszMsg, 1, &cchWritten, NULL);
        if( (FALSE == fRet) || (1 != cchWritten) )
            return EOF;
        lpszMsg++;
    }
    return 1;
}
 
#ifdef __cplusplus
}
#endif

使用Load-Time动态链接 

创建 DLL 后,可以使用它在应用程序中定义的函数。 下面是一个简单的控制台应用程序,它使用从导出的 myPuts 函数Myputs.dll (请参阅 创建简单Dynamic-Link库) 。

由于此示例显式调用 DLL 函数,因此应用程序的模块必须与导入库 Myputs.lib 链接。 有关生成 DLL 的详细信息,请参阅开发工具附带的文档。

C++复制

#include <windows.h> 

extern "C" int __cdecl myPuts(LPCWSTR);   // a function from a DLL

int main(VOID) 
{ 
    int Ret = 1;

    Ret = myPuts(L"Message sent to the DLL function\n"); 
    return Ret;
}

使用Run-Time动态链接 

可以在加载时和运行时动态链接中使用相同的 DLL。 以下示例使用 LoadLibrary 函数获取 Myputs DLL 的句柄 (请参阅 创建简单Dynamic-Link库) 。 如果 LoadLibrary 成功,程序将使用 GetProcAddress 函数中返回的句柄来获取 DLL 的 myPuts 函数的地址。 调用 DLL 函数后,程序调用 FreeLibrary 函数来卸载 DLL。

由于程序使用运行时动态链接,因此无需将模块与 DLL 的导入库链接。

此示例说明了运行时和加载时动态链接之间的重要区别。 如果 DLL 不可用,则使用加载时动态链接的应用程序必须直接终止。 但是,运行时动态链接示例可以响应错误。

C++复制

// A simple program that uses LoadLibrary and 
// GetProcAddress to access myPuts from Myputs.dll. 
 
#include <windows.h> 
#include <stdio.h> 
 
typedef int (__cdecl *MYPROC)(LPCWSTR); 
 
int main( void ) 
{ 
    HINSTANCE hinstLib; 
    MYPROC ProcAdd; 
    BOOL fFreeResult, fRunTimeLinkSuccess = FALSE; 
 
    // Get a handle to the DLL module.
 
    hinstLib = LoadLibrary(TEXT("MyPuts.dll")); 
 
    // If the handle is valid, try to get the function address.
 
    if (hinstLib != NULL) 
    { 
        ProcAdd = (MYPROC) GetProcAddress(hinstLib, "myPuts"); 
 
        // If the function address is valid, call the function.
 
        if (NULL != ProcAdd) 
        {
            fRunTimeLinkSuccess = TRUE;
            (ProcAdd) (L"Message sent to the DLL function\n"); 
        }
        // Free the DLL module.
 
        fFreeResult = FreeLibrary(hinstLib); 
    } 

    // If unable to call the DLL function, use an alternative.
    if (! fRunTimeLinkSuccess) 
        printf("Message printed from executable\n"); 

    return 0;

}

在Dynamic-Link库中使用共享内存 

以下示例演示 DLL 入口点函数如何使用文件映射对象来设置可由加载 DLL 的进程共享的内存。 仅当加载 DLL 时,共享 DLL 内存才会保留。 应用程序可以使用 SetSharedMem 和 GetSharedMem 函数访问共享内存。

实现共享内存的 DLL

该示例使用文件映射将命名共享内存块映射到加载 DLL 的每个进程的虚拟地址空间中。 为此,入口点函数必须:

  1. 调用 CreateFileMapping 函数以获取文件映射对象的句柄。 加载 DLL 的第一个进程创建文件映射对象。 后续进程打开现有对象的句柄。 有关详细信息,请参阅 创建File-Mapping对象
  2. 调用 MapViewOfFile 函数将视图映射到虚拟地址空间。 这使进程能够访问共享内存。 有关详细信息,请参阅 创建文件视图

请注意,虽然可以通过为 CreateFileMapping 的 lpAttributes 参数传入 NULL 值来指定默认安全属性,但可以选择使用 SECURITY_ATTRIBUTES 结构来提供额外的安全性。

C++复制

// The DLL code

#include <windows.h> 
#include <memory.h> 
 
#define SHMEMSIZE 4096 
 
static LPVOID lpvMem = NULL;      // pointer to shared memory
static HANDLE hMapObject = NULL;  // handle to file mapping

// The DLL entry-point function sets up shared memory using a 
// named file-mapping object. 
 
BOOL WINAPI DllMain(HINSTANCE hinstDLL,  // DLL module handle
    DWORD fdwReason,              // reason called 
    LPVOID lpvReserved)           // reserved 
{ 
    BOOL fInit, fIgnore; 
 
    switch (fdwReason) 
    { 
        // DLL load due to process initialization or LoadLibrary
 
          case DLL_PROCESS_ATTACH: 
 
            // Create a named file mapping object
 
            hMapObject = CreateFileMapping( 
                INVALID_HANDLE_VALUE,   // use paging file
                NULL,                   // default security attributes
                PAGE_READWRITE,         // read/write access
                0,                      // size: high 32-bits
                SHMEMSIZE,              // size: low 32-bits
                TEXT("dllmemfilemap")); // name of map object
            if (hMapObject == NULL) 
                return FALSE; 
 
            // The first process to attach initializes memory
 
            fInit = (GetLastError() != ERROR_ALREADY_EXISTS); 
 
            // Get a pointer to the file-mapped shared memory
 
            lpvMem = MapViewOfFile( 
                hMapObject,     // object to map view of
                FILE_MAP_WRITE, // read/write access
                0,              // high offset:  map from
                0,              // low offset:   beginning
                0);             // default: map entire file
            if (lpvMem == NULL) 
                return FALSE; 
 
            // Initialize memory if this is the first process
 
            if (fInit) 
                memset(lpvMem, '\0', SHMEMSIZE); 
 
            break; 
 
        // The attached process creates a new thread
 
        case DLL_THREAD_ATTACH: 
            break; 
 
        // The thread of the attached process terminates
 
        case DLL_THREAD_DETACH: 
            break; 
 
        // DLL unload due to process termination or FreeLibrary
 
        case DLL_PROCESS_DETACH: 
 
            // Unmap shared memory from the process's address space
 
            fIgnore = UnmapViewOfFile(lpvMem); 
 
            // Close the process's handle to the file-mapping object
 
            fIgnore = CloseHandle(hMapObject); 
 
            break; 
 
        default: 
          break; 
     } 
 
    return TRUE; 
    UNREFERENCED_PARAMETER(hinstDLL); 
    UNREFERENCED_PARAMETER(lpvReserved); 
} 

// The export mechanism used here is the __declspec(export)
// method supported by Microsoft Visual Studio, but any
// other export method supported by your development
// environment may be substituted.

#ifdef __cplusplus    // If used by C++ code, 
extern "C" {          // we need to export the C interface
#endif
 
// SetSharedMem sets the contents of the shared memory 
 
__declspec(dllexport) VOID __cdecl SetSharedMem(LPWSTR lpszBuf) 
{ 
    LPWSTR lpszTmp; 
    DWORD dwCount=1;
 
    // Get the address of the shared memory block
 
    lpszTmp = (LPWSTR) lpvMem; 
 
    // Copy the null-terminated string into shared memory
 
    while (*lpszBuf && dwCount<SHMEMSIZE) 
    {
        *lpszTmp++ = *lpszBuf++; 
        dwCount++;
    }
    *lpszTmp = '\0'; 
} 
 
// GetSharedMem gets the contents of the shared memory
 
__declspec(dllexport) VOID __cdecl GetSharedMem(LPWSTR lpszBuf, DWORD cchSize) 
{ 
    LPWSTR lpszTmp; 
 
    // Get the address of the shared memory block
 
    lpszTmp = (LPWSTR) lpvMem; 
 
    // Copy from shared memory into the caller's buffer
 
    while (*lpszTmp && --cchSize) 
        *lpszBuf++ = *lpszTmp++; 
    *lpszBuf = '\0'; 
}
#ifdef __cplusplus
}
#endif

共享内存可以映射到每个进程中的不同地址。 因此,每个进程都有自己的 lpvMem 实例,lpvMem 声明为全局变量,以便它可用于所有 DLL 函数。 该示例假定 DLL 全局数据不共享,因此加载 DLL 的每个进程都有自己的 lpvMem 实例。

请注意,当文件映射对象的最后一个句柄关闭时,将释放共享内存。 若要创建永久性共享内存,需要确保某些进程始终具有文件映射对象的打开句柄。

使用共享内存的进程

以下进程使用上面定义的 DLL 提供的共享内存。 第一个进程调用 SetSharedMem 来编写字符串,而第二个进程调用 GetSharedMem 来检索此字符串。

此过程使用 DLL 实现的 SetSharedMem 函数将字符串“这是一个测试字符串”写入共享内存。 它还启动一个子进程,该进程将从共享内存中读取字符串。

C++复制

// Parent process

#include <windows.h>
#include <tchar.h>
#include <stdio.h>

extern "C" VOID __cdecl SetSharedMem(LPWSTR lpszBuf);

HANDLE CreateChildProcess(LPTSTR szCmdline) 
{ 
   PROCESS_INFORMATION piProcInfo; 
   STARTUPINFO siStartInfo;
   BOOL bFuncRetn = FALSE; 
 
// Set up members of the PROCESS_INFORMATION structure. 
 
   ZeroMemory( &piProcInfo, sizeof(PROCESS_INFORMATION) );
 
// Set up members of the STARTUPINFO structure. 
 
   ZeroMemory( &siStartInfo, sizeof(STARTUPINFO) );
   siStartInfo.cb = sizeof(STARTUPINFO); 
 
// Create the child process. 
    
   bFuncRetn = CreateProcess(NULL, 
      szCmdline,     // command line 
      NULL,          // process security attributes 
      NULL,          // primary thread security attributes 
      TRUE,          // handles are inherited 
      0,             // creation flags 
      NULL,          // use parent's environment 
      NULL,          // use parent's current directory 
      &siStartInfo,  // STARTUPINFO pointer 
      &piProcInfo);  // receives PROCESS_INFORMATION 
   
   if (bFuncRetn == 0) 
   {
      printf("CreateProcess failed (%)\n", GetLastError());
      return INVALID_HANDLE_VALUE;
   }
   else 
   {
      CloseHandle(piProcInfo.hThread);
      return piProcInfo.hProcess;
   }
}

int _tmain(int argc, TCHAR *argv[])
{
   HANDLE hProcess;

   if (argc == 1) 
   {
      printf("Please specify an input file");
      ExitProcess(0);
   }

   // Call the DLL function
   printf("\nProcess is writing to shared memory...\n\n");
   SetSharedMem(L"This is a test string");

   // Start the child process that will read the memory
   hProcess = CreateChildProcess(argv[1]);

   // Ensure this process is around until the child process terminates
   if (INVALID_HANDLE_VALUE != hProcess) 
   {
      WaitForSingleObject(hProcess, INFINITE);
      CloseHandle(hProcess);
   }
   return 0;
}

此过程使用 DLL 实现的 GetSharedMem 函数从共享内存中读取字符串。 它由上述父进程启动。

C++复制

// Child process

#include <windows.h>
#include <tchar.h>
#include <stdio.h>

extern "C" VOID __cdecl GetSharedMem(LPWSTR lpszBuf, DWORD cchSize);

int _tmain( void )
{
    WCHAR cBuf[MAX_PATH];

    GetSharedMem(cBuf, MAX_PATH);
 
    printf("Child process read from shared memory: %S\n", cBuf);
    
    return 0;
}

 在Dynamic-Link库中使用线程本地存储

本部分介绍如何使用 DLL 入口点函数设置线程本地存储 (TLS) 索引,以便为多线程进程的每个线程提供专用存储。

TLS 索引存储在全局变量中,使其可供所有 DLL 函数使用。 此示例假定不共享 DLL 的全局数据,因为加载 DLL 的每个进程的 TLS 索引不一定相同。

每当进程加载 DLL 时,入口点函数都使用 TlsAlloc 函数来分配 TLS 索引。 然后,每个线程都可以使用此索引来存储指向其自己的内存块的指针。

使用 DLL_PROCESS_ATTACH 值调用入口点函数时,代码将执行以下操作:

  1. 使用 TlsAlloc 函数分配 TLS 索引。
  2. 分配一个内存块,供进程的初始线程独占使用。
  3. 在调用 TlsSetValue 函数时使用 TLS 索引,将内存块的地址存储在与索引关联的 TLS 槽中。

每次进程创建新线程时,都会使用 DLL_THREAD_ATTACH 值调用入口点函数。 然后,入口点函数为新线程分配内存块,并使用 TLS 索引存储指向该线程的指针。

当函数需要访问与 TLS 索引关联的数据时,请在对 TlsGetValue 函数的调用中指定索引。 这会检索调用线程的 TLS 槽的内容,在本例中是指向数据的内存块的指针。 当进程使用此 DLL 的加载时链接时,入口点函数足以管理线程本地存储。 使用运行时链接的进程可能会出现问题,因为不会为 调用 LoadLibrary 函数之前存在的线程调用入口点函数,因此不会为这些线程分配 TLS 内存。 此示例通过检查 TlsGetValue 函数返回的值并分配内存(如果该值指示未设置此线程的 TLS 槽)来解决此问题。

当每个线程不再需要使用 TLS 索引时,它必须释放其指针存储在 TLS 槽中的内存。 当所有线程都已完成使用 TLS 索引时,请使用 TlsFree 函数释放索引。

当线程终止时,使用 DLL_THREAD_DETACH 值调用入口点函数,并释放该线程的内存。 进程终止时,使用 DLL_PROCESS_DETACH 值调用入口点函数,并释放 TLS 索引中指针引用的内存。

C++复制

// The DLL code

#include <windows.h>

static DWORD dwTlsIndex; // address of shared memory
 
// DllMain() is the entry-point function for this DLL. 
 
BOOL WINAPI DllMain(HINSTANCE hinstDLL, // DLL module handle
    DWORD fdwReason,                    // reason called
    LPVOID lpvReserved)                 // reserved
{ 
    LPVOID lpvData; 
    BOOL fIgnore; 
 
    switch (fdwReason) 
    { 
        // The DLL is loading due to process 
        // initialization or a call to LoadLibrary. 
 
        case DLL_PROCESS_ATTACH: 
 
            // Allocate a TLS index.
 
            if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES) 
                return FALSE; 
 
            // No break: Initialize the index for first thread.
 
        // The attached process creates a new thread. 
 
        case DLL_THREAD_ATTACH: 
 
            // Initialize the TLS index for this thread.
 
            lpvData = (LPVOID) LocalAlloc(LPTR, 256); 
            if (lpvData != NULL) 
                fIgnore = TlsSetValue(dwTlsIndex, lpvData); 
 
            break; 
 
        // The thread of the attached process terminates.
 
        case DLL_THREAD_DETACH: 
 
            // Release the allocated memory for this thread.
 
            lpvData = TlsGetValue(dwTlsIndex); 
            if (lpvData != NULL) 
                LocalFree((HLOCAL) lpvData); 
 
            break; 
 
        // DLL unload due to process termination or FreeLibrary. 
 
        case DLL_PROCESS_DETACH: 
 
            // Release the allocated memory for this thread.
 
            lpvData = TlsGetValue(dwTlsIndex); 
            if (lpvData != NULL) 
                LocalFree((HLOCAL) lpvData); 
 
            // Release the TLS index.
 
            TlsFree(dwTlsIndex); 
            break; 
 
        default: 
            break; 
    } 
 
    return TRUE; 
    UNREFERENCED_PARAMETER(hinstDLL); 
    UNREFERENCED_PARAMETER(lpvReserved); 
}

// The export mechanism used here is the __declspec(export)
// method supported by Microsoft Visual Studio, but any
// other export method supported by your development
// environment may be substituted.

#ifdef __cplusplus    // If used by C++ code, 
extern "C" {          // we need to export the C interface
#endif

__declspec(dllexport)
BOOL WINAPI StoreData(DWORD dw)
{
   LPVOID lpvData; 
   DWORD * pData;  // The stored memory pointer 

   lpvData = TlsGetValue(dwTlsIndex); 
   if (lpvData == NULL)
   {
      lpvData = (LPVOID) LocalAlloc(LPTR, 256); 
      if (lpvData == NULL) 
         return FALSE;
      if (!TlsSetValue(dwTlsIndex, lpvData))
         return FALSE;
   }

   pData = (DWORD *) lpvData; // Cast to my data type.
   // In this example, it is only a pointer to a DWORD
   // but it can be a structure pointer to contain more complicated data.

   (*pData) = dw;
   return TRUE;
}

__declspec(dllexport)
BOOL WINAPI GetData(DWORD *pdw)
{
   LPVOID lpvData; 
   DWORD * pData;  // The stored memory pointer 

   lpvData = TlsGetValue(dwTlsIndex); 
   if (lpvData == NULL)
      return FALSE;

   pData = (DWORD *) lpvData;
   (*pdw) = (*pData);
   return TRUE;
}
#ifdef __cplusplus
}
#endif

以下代码演示如何使用在上一个示例中定义的 DLL 函数。

C++复制

#include <windows.h> 
#include <stdio.h> 
 
#define THREADCOUNT 4 
#define DLL_NAME TEXT("testdll")

VOID ErrorExit(LPSTR); 

extern "C" BOOL WINAPI StoreData(DWORD dw);
extern "C" BOOL WINAPI GetData(DWORD *pdw);
 
DWORD WINAPI ThreadFunc(VOID) 
{   
   int i;

   if(!StoreData(GetCurrentThreadId()))
      ErrorExit("StoreData error");

   for(i=0; i<THREADCOUNT; i++)
   {
      DWORD dwOut;
      if(!GetData(&dwOut))
         ErrorExit("GetData error");
      if( dwOut != GetCurrentThreadId())
         printf("thread %d: data is incorrect (%d)\n", GetCurrentThreadId(), dwOut);
      else printf("thread %d: data is correct\n", GetCurrentThreadId());
      Sleep(0);
   }
   return 0; 
} 
 
int main(VOID) 
{ 
   DWORD IDThread; 
   HANDLE hThread[THREADCOUNT]; 
   int i; 
   HMODULE hm;
 
// Load the DLL

   hm = LoadLibrary(DLL_NAME);
   if(!hm)
   {
      ErrorExit("DLL failed to load");
   }

// Create multiple threads. 
 
   for (i = 0; i < THREADCOUNT; i++) 
   { 
      hThread[i] = CreateThread(NULL, // default security attributes 
         0,                           // use default stack size 
         (LPTHREAD_START_ROUTINE) ThreadFunc, // thread function 
         NULL,                    // no thread function argument 
         0,                       // use default creation flags 
         &IDThread);              // returns thread identifier 
 
   // Check the return value for success. 
      if (hThread[i] == NULL) 
         ErrorExit("CreateThread error\n"); 
   } 
 
   WaitForMultipleObjects(THREADCOUNT, hThread, TRUE, INFINITE); 

   FreeLibrary(hm);
 
   return 0; 
} 
 
VOID ErrorExit (LPSTR lpszMessage) 
{ 
   fprintf(stderr, "%s\n", lpszMessage); 
   ExitProcess(0); 
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值