本文已参与「新人创作礼」活动,一起开启掘金创作之路。
在应用程序能够调用DLL中的函数之前,DLL文件映像必须被映射到调用进程的地址空间中。若要进行这项操作,可以使用两种方法中的一种,即==加载时的隐含连接或运行时期的显式链接==。 #注:本文所说的模块均指
*.lib
OR*.dll
以下内容基本参考自《Windows核心编程一书》 @[toc]
DLL中的一些注意点:
- 一旦DLL的文件映像被映射到调用进程的地址空间中,DLL的函数就可以供进程中所有的线程使用。
- 对于进程中的线程来说,DLL的代码和数据看上去就像恰巧是在进程的地址空间中的额外代码和数据一样。当一个线程调用DLL函数时,该DLL函数要查看线程的堆栈,以便检索它传递的参数。
- DLL中函数的代码创建的任何对象均由调用线程所拥有,而DLL本身从来不拥有任何东西。
- 如果一个模块提供一个用于==分配内存块的函数时 ,该模块也必须提供释放内存的函数。==
隐式链接
隐含链接是最常用的连接类型。
创建DLL模块
若要创建一个从DLL模块输入函数和变量的可执行模块,必须首先创建一个DLL模块。然后就可以创建可执行模块。
1) 创建DLL模块
若要创建DLL模块,必须执行下列操作步骤:
- 首先必须创建一个头文件,它包含你想要从DLL输出的函数原型、结构和符号。DLL的所有源代码模块均包含该头文件,以帮助创建DLL。这个头文件也会在创建需要使用DLL中包含的函数和变量的可执行模块时使用。
- 要创建一个C/C++源代码模块(或多个模块)。用于实现你想要在DLL模块中实现的函数和变量。由于这些源代码==在创建可执行模块时是不必要的==。因此创建DLL的公司能够保护公司的密码。
- 创建DLL模块,将使编译器对每个源代码模块进行处理,产生一个.obj模块(每个源代码都有一个.obj模块)
- 当所有的.obj模块创建完成后,链接程序将所有.obj模块的内容组合在一起,产生一个DLL映像文件。该映像文件(即模块)包含了用于DLL的所有二进制代码和全局/静态数据变量。为了执行这个可执行模块,该文件是必不可少的。
- 如果链接程序发现DLL的源代码模块至少输出一个函数或变量,那么链接程序也生成一个.lib文件。这个.lib文件很小,因为它不包含任何函数或变量。它只是列出所有一输出函数和变量的符号名。为了创建可执行模块,该文件是必不可少的。
2)创建可执行模块
一旦创建了DLL模块,就可以创建执行模块。其创建步骤是:
- 在引用函数、变量、数据、结构或符号的所有源代码模块中,==必须包含DLL开发人员创建的头文件。==
- 要创建一个C/C++源代码模块(或多个模块),用于实现你想要在可执行模块中实现的函数和变量。当然该代码可以引用DLL头文件中定义的函数和变量。
- 创建可执行模块,将编译器对每个源代码模块进行处理,生成一个.obj模块(每个源代码模块有一个.obj模块)。
- 当所有.obj模块创建完成后,链接程序便将所有的.obj模块的内容组合起来,生成一个可执行的映像文件。该映像文件(或模块)包含了可执行文件的所有二进制代码和全局/静态变量。该可执行模块还包含一个输入节,列出可执行文件需要的所有DLL模块名。此外,对于列出的每个DLL名字,该节指明了可执行模块的二进制代码引用了哪些函数和变量符号。
3)执行
一旦DLL和可执行模块创建完成,一个进程就可以执行。当试图运行可执行模块时,操作系统的加载程序将执行下面的操作步骤:
- 加载程序为新进程创建一个虚拟地址空间。可执行模块被映射到新进程的地址空间。家在程序对可执行模块的输入节进行分析。对于该节中列出的每个DLL名字,加载程序要找出用户系统上的DLL模块,再将该DLL映射到进程的地址空间。注意,由于DLL模块可以从另一个DLL模块输入函数和变量,因此DLL模块可以拥有它自己的输入节。 若要对进程进行全面的初始化,加载程序要分析每个模块的输入节,并将所有需要的DLL模块映射到进程的地址空间。
一旦可执行模块和所有的DLL模块被映射到进程的地址空间中,进程的主线程就可以启动运行,同事应用程序也可以启动运行。
显式链接
这个方法的优点就是一切操作都是在应用程序运行时进行的。
1)创造DLL
- 建立带有输出原型/结构/符号的头文件
- 建立实现输出函数/变量的C/C++源文件
- 编译器为每个C/C++源文件生成.obj模块
- 链接程序将生成DLL的.obj模块链接起来
- 如果至少输出一个函数/变量,那么链接程序也生成.lib文件
2)创造exe
- 建立带有输入原型/结构/符号的头文件(视情况而定)。
- 建立不引用输入函数/变量的 C/C++源文件。
- 编译器为每个 C/C++源文件生成 .obj源文件。
- 链接程序将各个 .obj模块链接起来,生成 .exe文件。 注: DLL的lib文件是不需要的,因为并不直接引用输出符号。.exe 文件不包含输入表。
3)运行应用程序:
- 加载程序为 .exe 创建模块地址空进程的主线程开始执行;应用程序启动运行。 显式加载DLL:
- 一个线程调用
LoadLibrary (Ex)
函数,将DLL加载到进程的地址空间这时线程可以调用GetProcAddress
以便间接引用DLL的 输出符号。
显式链接相关操作如
LoadLibrary
、GetProcAddress
等可以查看这篇文章,里边部分细节有对上述的补充【一文搞懂】GetProcAddress函数
DLL加载程序的搜索顺序
- 包含可执行映像文件的目录
- 进程的当前目录
- Windows系统目录
- Windows目录
- PATH环境变量中列出的各个目录
本文参考
- 《Windows核心编程》