动态链接库基础

所有内容摘自《windows核心编程》

一:DLL基础

1.dll有助于节省内存,如果两个或两个以上的应用程序使用同一个dll,那么该dll只需载入内存一次,以后所有的应用程序就可以共享该dll在内存中的页面。C/C++运行库就是一个绝佳的例子。许多应用程序都会用到C/C++运行库,如果所有的应用程序都链接到C/C++运行库的静态版本,那么诸如_tcscpy,malloc之类的函数会在内存中出现多次。但是,如果这些应用程序链接到C/C++运行库的DLL版本,那么这些函数在内存中只会出现一次,这意味着内存的使用率更高。


二:DLL和进程的地址空间

1.在应用程序能够调用一个DLL中的函数之前,必须将该DLL的文件映像映射到调用进程的地址空间中。可以通过隐式载入时链接或显示运行时链接。
2.一旦系统将一个DLL的文件映像映射到调用进程的地址空间中之后,进程中的所有县城就可以调用该DLL中的函数了。


三:纵观全局

构建DLL:

1.头文件,其中包含待导出函数的原型、结构和符号申明

2.C/C++源文件,其中包含待导出函数的实现和变量

3.编译器为每个C/C++源文件生成.obj文件

4.链接器将每个.obj模块合并,从而生成DLL

5.如果至少导出了一个函数/变量,那么链接器会同时生成.lib文件

构建EXE

1.头文件,其中包含待导出函数的原型、结构和符号的声明

2.C/C++源文件,其中包含待导出函数的实现和变量

3.编译器为每个C/C++源文件生成.obj文件

4.链接器将每个.obj模块合并,并使用.lib文件来解析对导入的函数/变量的引用,从而生成了.exe(它包含一个导入表,其中列出了必须的dll和导入的符号)


构建DLL需要以下步骤:

1.必须先创建一个头文件,在其中包含我们想要在dll中导出的函数原型、结构以及符号。为了构建该dll,dll的所有源文件需要包含这个头文件。在构建可执行文件的时候需要用到同一个头文件。

2.创建C/C++源文件来实现想要在dll模块中导出的函数和变量。

3.在构建dll模块的时候,编译器会对每个源文件进行处理并产生一个.obj模块,每个源文件对应一个.obj模块。

4.当所有的.obj模块都创建完毕后,链接器将对所有.obj模块的内容合并起来,产生一个单独的dll映像文件。这个映像文件或模块包含dll中所有的二进制代码以及全局/静态变量。

5.如果链接器检测到dll的源文件输出了至少一个函数或变量,那么链接器还会产生一个.lib文件。这个.lib文件非常小,这是因为它并不包含任何函数或变量。它只是列出了所有被导出的函数和变量的符号名。为了构建可执行模块,这个文件是必须的。

构建可执行模块步骤:

1.在所有引用了dll导出的函数、变量、数据结构或符号的源文件中,必须包含由dll的开发人员所创建的头文件。

2.创建C/C++源文件来实现想要包含在可执行模块中的函数和变量。

3.在构建可执行模块的时候,编译器会对每个源文件进行处理并为每一个源文件产生一个.obj文件

4.当所有.obj模块都创建完毕后,链接器会将所有.obj模块的内容合并起来,产生一个单独的可执行映像文件。这个映像文件包含了可执行文件中所有的二进制代码以及全局/静态变量。该可执行模块还包含一个导入段(import section),其中列出了所有它需要的dll模块的名称。此外,对列出的每个dll,该段还记录了可执行文件的二进制代码从中引用的函数和变量的符号名。

一旦dll和可执行模块都构建完毕,进程就可以执行了。当我们试图运行可执行模块的时候,操作系统的加载程序会执行下面的步骤。

5.加载程序先为新的进程创建一个虚拟地址空间,并将可执行模块映射到新进程的地址空间中。加载程序接着解析可执行模块的导入段,对导入段中列出的每个dll,加载程序会在用户的系统中对该dll模块进行定位,并将该dll映射到进程的地址空间中。注意,由于dll模块可以从其他dll模块中导入函数和变量,因此dll模块可能有自己的导入段并需要将它所需的dll模块映射到进程的地址空间中。


构建DLL模块:

1.在导出符号、变量、函数、类的头文件中,对要导出的变量(不建议)或函数要用extern "C" __declspec(dllexport)来修饰。

extern "C":

     C++编译器通常会对函数名和变量名进行改编(mangle),这在链接的时候会导致严重的问题。假设一个dll是用C++编写的,而可执行文件是用C编写的。在构建DLL的时候,编译器会对函数名进行改编,但在构建可执行文件的时候,编译器不会对函数名进行改编。当链接器试图链接可执行文件的时候,会发现可执行文件引用了一个不存在的符号并报错。extern "C"用来告诉编译器不要对变量名或函数名进行改编,这样C、C++或者任何编程语言编写的可执行模块都可以访问该变量或函数。

何为导出?
     当Microsoft的C/C++编译器看到用这个修饰符修饰的变量、函数原型或C++类的时候,会在生成的.obj文件中嵌入一些额外信息。当链接器在链接dll所有的.obj文件时,会解析这些信息。在链接dll的时候,链接器会检测到这些与导出的变量、函数或类有关的嵌入信息,并生成一个.lib文件。这个.lib文件列出了该dll导出的符号。在链接任何可执行模块的时候,只要可执行模块引用该dll导出的符号,那么这个.lib文件当然是必须的。除了创建这个.lib文件之外,链接器还会在生成的dll文件中嵌入一个导出符号表。这个按字母顺序排列的导出断列出了变量、函数和类的符号名。链接器还会保存相对虚拟地址(RVA),表示每个符号可以在dll模块中的何处找到。
     如果编译器看到一个变量、函数或C++类是用__declspec(dllimport)来修饰的,那么它会知道应该从某个dll模块中导入该符号,至于具体的dll模块是哪一个,编译器不知道,也不需要知道,因为是动态链接的。
     接下来,为了创建可执行模块,链接器必须将所有的.obj模块合并在一起。由于链接器必须确定代码中引用的导入符号来自哪个dll,因此我们必须将dll的.lib文件传给链接器。

构建可执行模块:
何为导入:
     在导入符号的时候,不必使用__declspec(dllimport)关键字,而可以直接使用标准C语言的extern关键字。但是,如果编译器能够提前知道我们引用的符号是从一个dll的.lib文件导入的,那么它能够产生略微高效的代码。有鉴于此,建议在导入函数和数据符号的时候使用__declspec(dllimport)关键字。
     当链接器在解决导入符号的时候,会在生成的可执行模块中嵌入一个特殊的段,它的名字叫导入段。导入段列出了该模块所需的dll模块,以及它从每个dll模块中引用的符号、函数等。


运行可执行模块:
     启动一个可执行模块的时候,操作系统的加载程序会先为进程创建虚拟地址空间,接着把可执行模块映射到进程的地址空间中。之后加载程序会检查可执行模块的导入段,试图对所需的dll进行定位并将它们映射到进程的地址空间中。
     由于加载程序会对载入的dll模块进行记录,因此即使多个模块用到了同一个模块,该模块也只会被载入和映射一次。
     加载程序会检查每个dll的导入段,如果一个dll有导入段,那么加载程序会继续讲所需的额外dll模块映射到进程的地址空间中。
     加载程序会取得dll导出符号的RVA并加上dll模块被载入到的虚拟地址(从而得到符号在进程的地址空间中的位置)。接着加载程序会将这个虚拟地址保存到可执行模块的导入段中。现在,当代码引用到一个导入符号的时候,会查看调用模块的导入段并得到被导入符号的地址,这样就能够成功地访问被导入的变量、函数或C++类成员函数了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值