• 动态链接库(DLL)是作为函数和资源的共享库的一种可执行文件。动态链接是操作系统功能。
  • DLL可以使执行文件调用函数或者使用存储在单独文件中的资源。可从使用的这些函数和资源的可执行文件中对其分别进行编译和部署
  • DLL不是独立的可执行文件。它在调用它们的应用程序的上下文中运行
  • 操作系统将DLL 在加载应用程序时(隐式链接) 或在运行时按需(显式链接) 加载到应用程序的内存空间中
  • DLL可以在可执行文件之间轻松共享函数和资源。多个应用程序可同时访问内存中单个DLL副本的内容

1.动态链接和静态链接

  • 编译预处理(对源文件 .h .cpp等进行预处理,主要处理一些伪指令,即#定义的命令或语句(宏定义#define、头文件包括指令#include、条件编译指令#ifdef等)和特殊符号__LINE__(被解释为当前行号)、FILE(被解释为当前被编译的C源程序的名称)等,生成.i 文件)

    #include<iostream>
    using namespace std;
    
    #line 100
    
    int main(){
    
        cout << "__LINE__: " << __LINE__ << endl;  //#line 使用之前是当前行数 8    #line 100 之后是108
        cout << "__FILE__: " << __FILE__ << endl; //当前文件名
    
        return 0;
    }
    
  • 编译(对预处理后的.i文件进行编译,主要进行词法分析、语法分析、语义分析等,生成.s的汇编文件)

  • 汇编(将对应的汇编指令翻译成机器指令,生成可重定位的二进制目标文件 .o)

  • 链接(.a /.lib .so/.dll)

    • 静态库:在链接时,链接器将从库文件取得所需的代码,复制到生成的可执行文件中(.exe、.com、./a.out…),这种库就是静态库,可执行文件中包含了库代码的一份完整的拷贝,移植方便。缺点——被多次使用就会有多分冗余拷贝,即静态库中的所有指令都全部被直接包含在最终生成的exe文件中了,浪费空间和资源。vs中新建生成静态库的工程,编译生成成功后,只产生一个.lib文件
      • .lib包含函数代码本身(函数索引,实现等),编译时直接将代码加入程序
    • 动态库:包含可由多个程序同时使用的代码和数据的库,DLL不是可执行文件。动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个DLL中,该DLL包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。程序运行时由系统动态加载动态库到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。vs中新建生成动态库的工程,编译成功后,产生一个.lib和一个.dll
      • .lib包含函数所在的DLL文件和文件中函数索引,函数实现代码由运行时加载在进程空间中的DLL来提供
      • 动态库把对一些库函数的链接载入推迟到程序运行时期
      • 可以实现进程之间的资源共享(共享库)
      • 程序升级不需要所有程序都重新编译,变得更简单
      • 能做到链接载入完全由程序代码控制(显式调用)
      • 隐式链接动态库:
        1. 需要 .h .dll .lib 文件
        2. 将 .dll放到工程的工作目录
        3. 设置项目属性 – vc++目录 – 库目录为lib所在的路径
        4. 将lib添加到项目属性 – 链接器 – 输入 – 附加依赖项(或在源代码中加入#pragma comment(lib, “*.lib”))
        5. 在源文件中添加.h头文件
      • 显式链接动态库:
        1. 显式链接只需要.dll文件。但这种方式很难区调用dll中的类,不能调用dll中的变量
        2. Windows下显式调用主要使用WIN32 API中的函数LoadLibrary、GetProcAddress
    • 如果要完成源代码的编译,只需.lib文件即可
    • 如果使动态链接的程序运行,只需要.dll文件

1.1 静态链接

  • 将静态库中的所有对象代码复制到生成时使用它的可执行文件中

1.2 动态链接

  • 仅包括Windows在运行时用于查找和加载含有数据项或函数的DLL所需的信息
  • 创建DLL时,还将创建包含此信息的导入库
  • 生成调用DLL的可执行文件时,链接器会使用导入库中的导出符号来为Windows加载程序存储此信息
  • 当加载程序加载DLL时,该DLL会映射到应用程序的内存空间中。如果存在,则调用DLL中的特殊函数DllMain,以执行DLL所需的任何初始化

2.应用程序和DLL的区别

  • 应用程序可以在系统中同时运行其自身的多个实例。DLL只能有一个实例
  • 应用程序可以作为进程进行加载,可以管理如堆栈、执行线程、全局内存、文件句柄和消息队列之类的资源。DLL不能管理这些资源

3.DLL的优点

  • 动态链接节省内存,较少交换。
    • 许多进程可以同时使用DLL,并在内存中共享DLL只读部分的单个副本。
    • 使用静态链接库构建的每个应用程序都有一个完整的库代码副本,Windows必须将其加载到内存
  • 动态链接节省了磁盘空间和带宽。许多应用程序可以在磁盘上共享DLL的单个副本。
    • 使用静态链接库构建的每个应用程序都是讲库代码链接到可执行映像,这会占用更多的磁盘空间,并需要更多带宽来传输
  • 更易维护、安全修复和升级。
  • 使用DLL提供售后支持。
    • 可以修改显示驱动程序DLL以支持应用程序发布时不可用的显示
  • 可以使用显式链接在运行时发现和加载DLL。
    • 无需重新生成或重新部署就可以将新功能添加到你的应用的应用程序扩展
  • 对于用不同编程语言编写的应用程序,使用动态链接可以更轻松地对其提供支持。
    • 用不同编程语言编写的程序只要遵循函数调用约定,就可以调用相同的DLL函数
    • 程序和函数必须在【函数期望将其参数推送到堆栈上的顺序、函数或应用程序是否负责清理堆栈、是否有参数在寄存器中传递】方面兼容
  • 动态链接提供了一种扩展Microsoft基础类库(MFC)类的机制。
    • 可以从现有的MFC类派生类,并将它们放在MFC扩展DLL中提供MFC应用程序使用
  • 动态链接使创建应用程序的国际版本更容易。
    • 可以将每种语言的字符串和映像放在一个单独的资源DLL中,而不是发布应用程序的许多本地化版本。然后应用程序可以在运行时加载该区域设置的适当资源

潜在缺点:应用程序不是自包含的。它依赖于一个独立的DLL模块的存在——在安装过程中必须亲自部署或验证的模块

4.创建和使用动态链接库

  • 创建DLL项目,在项目下分别新建MathLibrary.h和MathLibrary.cpp,然后编译即可生成DLL文件

  • #pragma once
    /*
    * __declspec(dllexport) 和 __declspec(dllimport) 是特定于C 和 C++语言的扩展。可以使用它们从DLL中导出或向其中导入函数、数据和对象
    * grammar:__declspec(dllimport) declarator;		__declspec(dllexport) declarator;
    */
    #ifdef MATHLIBRARY_EXPORTS
    #define MATHLIBRARY_API __declspec(dllexport) 
    #else
    #define MATHLIBRARY_API __declspec(dllimport)
    #endif
    
    extern "C" MATHLIBRARY_API void fibonacci_init(const unsigned long long a, const unsigned long long b);
    
    extern "C" MATHLIBRARY_API bool fibonacci_next();
    
    extern "C" MATHLIBRARY_API unsigned long long fibonacci_current();
    
    extern "C" MATHLIBRARY_API unsigned fibonacci_index();
    
  • //MathLibrary.cpp
    #include "pch.h"
    #include <utility>
    #include <limits.h>
    #include "MathLibrary.h"
    
    //DLL内部状态变量
    static unsigned long long previous_;
    static unsigned long long current_;
    static unsigned index_;
    
    //这个函数必须比其他函数先被调用
    void fibonacci_init(const unsigned long long a, const unsigned long long b) {
    	index_ = 0;
    	current_ = a;
    	previous_ = b;
    }
    
    //生成序列中的下一个值。成功返回true,溢出返回false
    bool fibonacci_next() {
    	if ((ULLONG_MAX - previous_ < current_) || (UINT_MAX == index_)) return false;
    	if (index_ > 0) previous_ += current_;
    	std::swap(current_, previous_);
    	++index_;
    	return true;
    }
    
    //获取序列中的当前值
    unsigned long long fibonacci_current() {
    	return current_;
    }
    
    //获取序列中当前索引位置
    unsigned fibonacci_index() {
    	return index_;
    }
    
  • 创建新的C++控制台项目MathClient充当客户端。新建MathClient.cpp。设置属性:

    • C/C++ – 附加包含目录 – 指向MathLibrary.h文件位置的路径
    • 链接器 – 输入 – 附加依赖项 – MathLibrary.lib
    • 链接器 – 常规 – 附加库目录 – 指向MathLibrary.lib文件位置的路径
  • //MathClient.cpp
    #include <iostream>
    #include "MathLibrary.h"
    
    int main()
    {
        fibonacci_init(1, 1);
        do {
            std::cout << fibonacci_index() << ": " << fibonacci_current() << std::endl;
        } while (fibonacci_next());
    
        std::cout << fibonacci_index() + 1 << " Fibonacci sequence values fit in an unsigned 64-bit integer." << std::endl;
    }
    
  • 编译,运行可以看到结果

5.将可执行文件链接到DLL

大多数应用程序会使用隐式链接,因为这是可使用的最简单的连接方法。

5.1 隐式链接

  • 操作系统会与使用DLL的可执行文件同时加载它。
  • 客户端可执行文件调用DLL的导出函数的方式与函数进行静态链接并包含在可执行文件中时的方式相同。
  • 隐式链接又被称为 静态加载 或 加载时动态链接。
  1. 当应用程序的代码导出到处DLL函数时,会进行隐式链接。当编译或汇编调用可执行文件的源代码时,DLL函数调用会在对象代码中生成外部函数引用。若要解析此外部引用,应用程序必须与DLL创建者提供的导入库(.lib)链接
  2. 导入库包含的代码仅用于加载DLL和实现对DLL中函数的调用。在导入库中查找外部函数会告知链接器该函数的代码处于DLL中。若要解析对DLL的外部引用,链接器只需将信息添加到可执行文件,告知系统在进程启动时查找DLL代码的位置
  3. 当系统启动包含动态链接引用的程序时,它将使用该程序可执行文件中的信息查找所需DLL。如果找不到DLL,则系统将终止进程,并显示报告错误的对话框(上面的示例)。否则,系统会将DLL模块映射到进程地址空间中。
  4. 如果任何DLL的初始化和终止代码(如DllMain)又入口点函数,则操作系统将调用该函数。传递给入口点函数的一个参数指定用于指示DLL附加到进程的代码。如果入口点函数不返回TRUE,则系统将终止进程并报告错误
  5. 最后,系统会修改进程的可执行代码以提供DLL函数的起始地址。
  6. 与程序代码的其余部分一样,在程序启动时,加载程序会将DLL代码映射到进程的地址空间中。操作系统仅在需要时才将它加载到内存中。因此,.def文件在以前Windows版本中用于控制加载的PRELOAD和LOADONCALL代码特性不再有意义。
5.1.1 隐式链接的使用
  • 客户端可执行文件必须从DLL的提供程序获取一下文件:
    1. 一个或多个头文件(.h),其中包含DLL中的导出数据、函数和C++类的声明。DLL导出的类、函数和数据全部必须在头文件中标记为 __declspec(dllimport)。__declspec(dllexprot) 和 __declspec(dllimport)该存储类属性是特定于C和C++的语言扩展。可以使用他们从DLL中到处或向其中导入函数、数据和对象
    2. 要链接到可执行文件中的库。生成DLL时,链接器会创建导入库(具体操作参考第四点的demo引入lib库的操作)。
    3. 实际DLL文件
  • 要通过隐式链接使用DLL中的数据、函数和类,任何客户端源文件都必须包含声明他们的头文件。从编码的角度来看,对导出函数的调用与任何其他函数调用一样。
  • 要生成客户端可执行文件,必须与DLL的导入库链接。如果使用外部生成文件或生成系统,请将导入库与链接的其他对象文件或库一起指定
  • 当操作系统加载调用可执行文件时,必须要能找到DLL文件。即必须在安装应用程序时部署DLL或验证DLL是否存在

5.2 显式链接

  • 操作系统会在运行时按需加载DLL。
  • 通过显式链接使用DLL的可执行文件必须显式加载和卸载DLL。
  • 必须设置函数指针,用于访问它从DLL使用的每个函数。
  • 客户端可执行文件必须通过函数指针调用显式DLL中的导出函数。
  • 显式链接又被称为 动态加载 或 运行时动态链接
5.2.1 使用显式链接的一些常见原因:
  1. 应用程序知道运行时才知道它所加载的DLL的名称。例如,应用程序可能会在启动时从配置文件获取DLL的名称和导出函数
  2. 如果在使用隐式链接的进程启动时找不到DLL,则操作系统会终止进程。使用显式链接的进程在这种情况下不会终止,可以尝试从错误中恢复。例如,进程可以向用户通知错误,并让用户指定DLL的其他路径
  3. 如果使用隐式链接的进程所链接到的任何DLL的DllMain函数失败,则进程也会终止。使用显式链接的进程在这种情况下不会终止
  4. 隐式链接到许多DLL的应用程序可能会速度较慢,因为Windows会在应用程序加载时加载所有DLL。若要提高启动性能,应用程序可以只对在加载之后立即需要的DLL使用隐式链接。它可以仅在需要时才使用显式链接加载其他DLL
  5. 显式链接无需使用导入库链接应用程序。如果DLL中的更改导致导出序号发生更改,则在使用函数名称而不是序号值调用GetProcAddress时,应用程序无需重新链接。使用隐式链接的应用程序仍必须重新链接到更改的导入库
5.2.2 使用显式链接的风险
  1. 如果DLL具有DllMain入口点函数,则操作系统会在调用LoadLibrary的线程的上下文中调用该函数。如果DLL已附加到进程【LoadLibrary实际上是将DLL加载到当前进程空间,如果是第一次加载就会调用DllMain,如果第二次调用LoadLibrary加载同一个文件且没有释放,就是已经附加到进程】,则不会调用入口点函数,因为以前调用的LoadLibrary没有对FreeLibrary函数进行相应调用。如果DLL使用DllMain函数初始化进程的每个线程,则显式链接可能会导致问题,因为调用LoadLibrary(或AfxLoadLibrary)时已存在的任何线程都不会进行初始化
  2. 如果DLL将静态范围数据声明为__declspec(thread),则可能会在进行显式链接时导致保护错误。在通过调用LoadLibrary加载DLL之后,每当代码引用此数据,便会导致保护错误。(静态范围数据包含全局和局部静态项)这就是为什么在创建DLL时,应避免使用线程本地存储。如果不能这样做,请向DLL用户告知动态加载DLL的潜在陷阱。
5.2.3 显式链接的使用
  • 应用程序必须在运行时进行函数调用以显式加载DLL,若要显式链接到DLL,应用程序必须:

    1. 调用LoadLibraryEx或类似函数以加载DLL并获取模块句柄
    2. 调用GetProcAddress以获取应用程序调用的每个导出函数的函数指针。由于应用程序通过指针调用DLL函数,因此编译器不生成外部引用,从而不需要与导入库进行链接。但是必须有一个 typedef 或 using 语句来定义所嗲用的导出函数的调用签名
    3. 处理完DLL时,需要调用FreeLibrary
  • 例,以下示例调用 LoadLibrary 以加载名为 “MyDLL” 的DLL,调用 GetProcAddress 以获取指向名唯 “DLLFunc1”的函数的指针,调用该函数并保存结果,然后调用 FreeLibrary 以卸载DLL

    #include <iostream>
    #include <Windows.h>
    using namespace std;
    
    typedef HRESULT(CALLBACK* LPFNDLLFUNC1)(DWORD, UINT*);
    
    HRESULT LoadAndCallSomFunction(DWORD dwParam1, UINT* puParam2) {
    	HINSTANCE hDLL; //Handle to DLL
    	LPFNDLLFUNC1 lpfnDLLFunc1; //Function Pointer
    	HRESULT hrReturnVal;
    	//利用TEXT宏使其自动选择正确的字符集。或使用LoadLibraryA
    	//因为LoadLibrary在定义了UNICODE的情况下使用的是LoadLibraryW,需要的是UNICODE字符串也就是宽字符
    	hDLL = LoadLibrary(TEXT("D:\_fyyFolder\_fyy\work\cppProject\MathLibrary\Debug\MathLibrary.dll")); 
    	if (NULL != hDLL) {
    		lpfnDLLFunc1 = (LPFNDLLFUNC1)GetProcAddress(hDLL, "DLLFunc1");
    		if (NULL != lpfnDLLFunc1) hrReturnVal = lpfnDLLFunc1(dwParam1, puParam2);
    		else hrReturnVal = ERROR_DELAY_LOAD_FAILED;
    		FreeLibrary(hDLL);
    	}
    	else {
    		hrReturnVal = ERROR_DELAY_LOAD_FAILED;
    	}
    	return hrReturnVal;
    }
    

6.DLL运行时库行为

6.1 默认DLL入口点 _DllMainCRTStartup

  • Windows中,所有DLL都可以包含一个可选的入口点函数DllMain,该函数用于初始化和终止操作,可以根据需要分配或释放额外的资源。Windows会在四种情况下调用入口点函数:进程附加、进程分离、线程附加、线程分离
    • 进程附加:将DLL加载到进程地址空间中时,或在加载使用它的应用程序时,或在应用程序在运行时请求DLL时,操作系统会创建DLL数据的单独副本。
    • 线程附加:加载DLL的进程创建新线程时
    • 线程分离:线程终止时
    • 进程分离:不再需要DLL并由应用程序释放DLL
  • 操作系统为每个事件单独调用DLL入口点,并为每个事件类型传递“原因”参数。eg:OS将DLL_PROCESS_ATTACT作为原因参数发送给信号进程附加
  • VCRuntime库提供一个名为_DllMainCRTStartip的入口点函数来处理默认的初始化和终止操作。在进程附加、进程分离、线程附加、线程分离的情况下,VCRuntime代码本身不进行额外的初始化或终止,只调用DllMain来传递消息。如果DllMain从进程附加返回FALSE并出现信号故障,、_DllMainCRTStartip将再次调用DllMain,并将DLL_PROCESS_DETACH作为“原因”参数传递,然后继续完成终止进程的其余部分
  • 在Visual Studio中生成dll时,VCRuntime提供的默认入口点_DllMainCRTStartup将自动链接,不需要使用/ENTRY(入口点符号)链接器作为DLL指定入口点函数

6.2 初始化DLL

  • DllMian必须具有DLL入口点所需的签名。默认入口点函数_DllMainCRTStartup使用Windows传递的相同参数调用DllMain。默认如果未提供DllMain函数,VisualStudio将为你提供一个函数并链接它,以便_DllmainCRTStartup始终可以调用某些内容。

  • ​ 用于DllMain的签名:

  • #include <windows.h>
    
    extern "C" BOOL WINAPI DllMain(
        HINSTANCE const instance; //32bit unsigned long.Handle. struct HINSTANCE__{int unused;}
        DWORD const reason; // 32bit unsigned long
        LPVOID const reserved; // void *
    );
    
  • DLL源代码必须包含名唯DllMain的函数,以下代码提供基本框架:

  • #incldue <windows.h>
    
    extern "C" BOOL WINAPI DllMain(HINSTANCE const instance, DWORD const reason, LPVOID const reserved){
        switch(reason){
            case DLL_PROCESS_ATTACH:
                //对每个新的线程进行初始化。如果加载DLL失败返回FALSE
                break;
            case DLL_THREAD_ATTACH:
                //执行特定于线程的初始化
                break;
            case D__THREAD_DETACH:
                //执行特定于线程的清理
                break;
            case DLL_PROCESS_DETACH:
                //执行任何必要的清理
                break;
        }
        return TRUE; //DLL_PROCESS_ATTACH成功
    }
    

7.LoadLibrary和AfxLoadLibrary

  • 进程调用LoadLibrary或LoadLibraryEx显式链接DLL
  • 函数执行成功会将指定的DLL映射到调用进程的地址空间中并返回该DLL的句柄,并递增模块的引用数
  • LoadLibrary将尝试使用用于隐式链接的相同搜索顺序来查找DLL。如果找不到DLL或入口点函数返回FLASE,则返回NULL。
  • LoadLibraryEx可以更好地控制搜索路径顺序

8.GetProcAddress

  • 显式链接DLL的进程会调用GetProcAddress以获取DLL中导出函数的地址

  • 可使用返回的函数指针调用DLL函数

  • 采用句柄作为参数(LoadLibrary、AfxLoadLibrary、GetModuleHandle的返回值),并采用要调用的函数的名称或函数的导出序号

  • 因为通过指针调用DLL函数,没有编译时类型检查,所以要确保函数的参数正确

  • 通常使用 查看导出函数的函数原型并为函数指针创建匹配的typedef 来提供类型安全

  • typedef UINT (CALLBACK* LPFNDLLFUNC1)(DWORD, UINT);
    ...
    HINSTANCE hDLL;
    LPFNDLLFUNC1 lpfnDllFunc1;
    DWORD dParam1;
    UINT uParam2, uReturnVal;
    
    hDLL = LoadLibrary("MathLibrary.dll");
    if(NULL != hDLL){
        lpfnDllFunc1 = (LPFNDLLFUNC1) GetProcAddress(hDLL, "DLLFunc1");
        if(!lpfnDllFunc1){
            FreeLibrary(hDLL);
            return SOME_ERROR_CODE;
        } else {
            uReturnVal = lpfnDllFunc1(dwParam1, uParam2);
        }
    }
    
  • 仅当要链接到DLL使用模块定义(.def)文件生成,并且序号随函数在DLL.def文件的EXPORTS节中列出时,才能获取导出序号。

  • 如果DLL具有许多导出函数,则与函数名称相比,使用导出需要调用GetProcAddress会稍微快一些,因为到处序号充当DLL导出表中的索引。使用导出序号,GetProcAddress可以直接查找函数,而不是将指定名称与DLL导出表中的函数名进行比较

  • 但仅当可控制将序号分配给.def文件中的导出函数时,才应使用导出序号调用

9.FreeLibrary和AfxFreeLibrary

  • 当不再需要DLL模块,显式链接到DLL的进程会调用FreeLibrary函数
  • 此函数使模块引用计数递减
  • 如果引用计数为0则从进程的地址空间取消映射

全文为MSDN — DLL的学习笔记

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐