【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 4283|回复: 8

[算法/加密解密] [转帖]动态链接库--- DLL基础

[复制链接]
发表于 2006-3-28 17:29:25 | 显示全部楼层 |阅读模式

动态链接库(1) DLL基础自

从Microsoft公司推出第一个版本的Windows操作系统以来,动态链接库(DLL)一直是这个操作系统的基础。WindowsAPI中的所有函数都包含在DLL中。3个最重要的DLL是Kernel32.dll,它包含用于管理内存、进程和线程的各个函数;User32.dll,它包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数;GDI32.dll,它包含用于画图和显示文本的各个函数。Windows还配有若干别的DLL,它们提供了用于执行一些特殊任务的函数。例如,AdvAPI32.dll包含用于实现对象安全性、注册表操作和事件记录的函数;ComDlg32.dll包含常用对话框(如FileOpen和FileSave);ComCtl32.dll则支持所有的常用窗口控件。
  本章将要介绍如何为应用程序创建DLL。下面是为什么要使用DLL的一些原因:
  1、它们扩展了应用程序的特性。由于DLL能够动态地装入进程的地址空间,因此应用程序能够在运行时确定需要执行什么操作,然后装入相应的代码,以便根据需要执行这些操作。例如,当一家公司开发了一种产品,想要让其他公司改进或增强该产品的功能时,那么就可以使用DLL。
  2、它们可以用许多种编程语言来编写。可以选择手头拥有的最好的语言来编写DLL。也许你的应用程序的用户界面使用MicrosoftVisualBasic编写得最好,但是用C++来处理它的商用逻辑更好。系统允许VisualBasic程序加载C++DLL、CobolDLL和FortranDLL等。
  3、它们简化了软件项目的管理。如果在软件开发过程中不同的工作小组在不同的模块上工作,那么这个项目管理起来比较容易。但是,应用程序在销售时附带的文件应该尽量少一些。我知道有一家公司销售的产品附带了100个DLL——每个程序员最多有5个DLL。这样,应用程序的初始化时间将会长得吓人,因为系统必须打开100个磁盘文件之后,程序才能执行它的操作。
  4、它们有助于节省内存。如果两个或多个应用程序使用同一个DLL,那么该DLL的页面只要放入RAM一次,所有的应用程序都可以共享它的各个页面。C/C++运行期库就是个极好的例子。许多应用程序都使用这个库。如果所有的应用程序都链接到这个静态库,那么sprintf、strcpy和malloc等函数的代码就要多次存在于内存中。但是,如果所有这些应用程序链接到DLLC/C++运行期库,那么这些函数的代码就只需要放入内存一次,这意味着内存的使用将更加有效。
  5、它们有助于资源的共享。DLL可以包含对话框模板、字符串、图标和位图等资源。多个应用程序能够使用DLL来共享这些资源。
  6、它们有助于应用程序的本地化。应用程序常常使用DLL对自己进行本地化。例如,只包含代码而不包含用户界面组件的应用程序可以加载包含本地化用户界面组件的DLL。
  7、它们有助于解决平台差异。不同版本的Windows配有不同的函数。开发人员常常想要调用新的函数(如果它们存在于主机的Windows版本上的话)。但是,如果你的源代码包含了对一个新函数的调用,而你的应用程序将要在不能提供该函数的Windows版本上运行,那么操作系统的加载程序将拒绝运行你的进程。即使你实际上从不调用该函数,情况也是这样。如果将这些新函数保存在DLL中,那么应用程序就能够将它们加载到Windows的老版本上。当然,你仍然可以成功地调用该函数。
  8、它们可以用于一些特殊的目的。Windows使得某些特性只能为DLL所用。例如,只有当DLL中包含某个挂钩通知函数的时候,才能安装某些挂钩(使用SetWindowsHookEx和SetWinEventHook来进行安装)。可以通过创建必须在DLL中生存的COM对象来扩展WindowsExplorer的外壳程序。对于可以由Web浏览器加载的、用于创建内容丰富的Web页的ActiveX控件来说,情况也是一样.

 楼主| 发表于 2006-3-28 17:30:48 | 显示全部楼层
动态链接库(2) DLL与进程的地址空间创建DLL常常比创建应用程序更容易,因为DLL往往包含一组应用程序可以使用的自主函数。在DLL中通常没有用来处理消息循环或创建窗口的支持代码。DLL只是一组源代码模块,每个模块包含了应用程序(可执行文件)或另一个DLL将要调用的一组函数。当所有源代码文件编译后,它们就像应用程序的可执行文件那样被链接程序所链接。但是,对于一个DLL来说,你必须设定该连链程序的/DLL开关。这个开关使得链接程序能够向产生的DLL文件映像发出稍有不同的信息,这样,操作系统加载程序就能将该文件映像视为一个DLL而不是应用程序。

  在应用程序(或另一个DLL)能够调用DLL中的函数之前,DLL文件映像必须被映射到调用进程的地址空间中。若要进行这项操作,可以使用两种方法中的一种,即加载时的隐含链接或运行期的显式链接。隐含链接将在本章的后面部分介绍,显式链接将在第20章中介绍。

  一旦DLL的文件映像被映射到调用进程的地址空间中,DLL的函数就可以供进程中运行的所有线程使用。实际上,DLL几乎将失去它作为DLL的全部特征。对于进程中的线程来说,DLL的代码和数据看上去就像恰巧是在进程的地址空间中的额外代码和数据一样。当一个线程调用DLL函数时,该DLL函数要查看线程的堆栈,以便检索它传递的参数,并将线程的堆栈用于它需要的任何局部变量。此外,DLL中函数的代码创建的任何对象均由调用线程所拥有,而DLL本身从来不拥有任何东西。

  例如,如果VirtualAlloc函数被DLL中的一个函数调用,那么将从调用线程的进程地址空间中保留一个地址空间的区域,该地址空间区域将始终处于保留状态,因为系统并不跟踪DLL中的函数保留该区域的情况。保留区域由进程所拥有,只有在线程调用VirtualFree函数或者进程终止运行时才被释放。

  如你所知,可执行文件的全局变量和静态变量不能被同一个可执行文件的多个运行实例共享。Windows98能够确保这一点,方法是在可执行文件被映射到进程的地址空间时为可执行文件的全局变量和静态变量分配相应的存储器。Windows2000确保这一点的方法是使用第13章介绍的写入时拷贝(copy-on-write)机制。DLL中的全局变量和静态变量的处理方法是完全相同的。当一个进程将DLL的映像文件映射到它的地址空间中去时,系统将同时创建全局数据变量和静态数据变量的实例。

  注意 必须注意的是,单个地址空间是由一个可执行模块和若干个DLL模块组成的。这些模块中,有些可以链接到静态版本的C/C++运行期库,有些可以链接到一个DLL版本的C/C++运行期库,而有些模块(如果不是用C/C++编写的话)则根本不需要C/C++运行期库。许多开发人员经常会犯一个常见的错误,因为他们忘记了若干个C/C++运行期库可以存在于单个地址空间中。请看下面的代码:
VOID EXEFunc(){ <br> PVOID pv = DLLFunc(); <br> //Access the storage pointed to by pv... <br> //Assumes that pv is in EXEs C/C++ run-time heap <br> free(pv); <br>} <br>VOID DLLFunc(){ <br> // Allocate block from DLLs C/C++ run-time heap <br> return(malloc(100)); <br>} <br>


  那么你是怎么看待这个问题的呢?上面这个代码能够正确运行吗?DLL函数分配的内存块是由EXE的函数释放的吗?答案是可能的。上面显示的代码并没有为你提供足够的信息。如果EXE和DLL都链接到DLL的C/C++运行期库,那么上面的代码将能够很好地运行。但是,如果两个模块中的一个或者两个都链接到静态C/C++运行期库,那么对free函数的调用就会失败。我经常看到编程人员编写这样的代码,结果都失败了。

  有一个很方便的方法可以解决这个问题。当一个模块提供一个用于分配内存块的函数时,该模块也必须提供释放内存的函数。让我们将上面的代码改写成下面的样子:
VOID EXEFunc(){ <br> PVOID pv = DLLFunc(); <br> //Access the storage pointed to by pv... <br> //Assumes that pv is in EXEs C/C++ run-time heap <br> DLLFreeFunc(pv); <br>} <br>  <br>VOID DLLFunc(){ <br> // Allocate block from DLLs C/C++ run-time heap <br> PVOID pv = malloc(100); <br> return(pv); <br>} <br>  <br>BOOL DLLFreeFunc(PVOID pv){ <br> //Free block from DLLs C/C++ run-time heap <br> return(free(pv)); <br>} <br>


  这个代码是正确的,它始终都能正确地运行。当你编写一个模块时,不要忘记其他模块中的函数也许没有使用C/C++来编写,因此可能无法使用malloc和free函数进行内存的分配。应该注意不要在代码中使用这些假设条件。另外,在内部调用malloc和free函数时,这个原则对于C++的new和delete操作符也是适用的。
 楼主| 发表于 2006-3-28 18:46:06 | 显示全部楼层
动态链接库(3) DLL的总体运行情况

为了全面理解DLL是如何运行的以及你和系统如何使用DLL,让我们首先观察一下DLL的整个运行情况。图19-1综合说明了它的所有组件一道配合运行的情况。

现在要重点介绍可执行模块和DL模块之间是如何隐含地互相链接的。隐含链接是最常用的链接类型。Windows也支持显式链接(第20章介绍这个问题)。

在图19-1中你可以看到,当一个模块(比如一个可执行文件)使用DLL中的函数或变量时,将有若干个文件和组件参与发挥作用。为了简单起见,我将“可执行模块”称为来自DLL的输入函数和变量,将“DLL模块”称为用于可执行模块的输出函数和变量。但是要记住,DLL模块能够(并且确实常常)输入包含在其他DLL模块中的函数和变量。

创造DLL:

1)建立带有输出原型/结构/符号的头文件。

2)建立实现输出函数/变量的C/C++源文件。

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

4)链接程序将生成DLL的.obj模块链接起来。

5)如果至少输出一个函数/变量,那么链接程序也生成lib文件。

创造EXE:

6)建立带有输入原型/结构/符号的头文件。

7)建立引用输入函数/变量的C/C++源文件。

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

9)链接程序将各个.obj模块链接起来,产生一个.exe文件(它包含了所需要DLL模块的名字和输入符号的列表)。

运行应用程序:

10)加载程序为.exe创建地址空间。

11)加载程序将需要的DLL加载到地址空间中进程的主线程开始执行;应用程序启动运行。

图19-1应用程序如何创建和隐含链接DLL的示意图

若要创建一个从DLL模块输入函数和变量的可执行模块,必须首先创建一个DLL模块。然后就可以创建可执行模块。

若要创建DLL模块,必须执行下列操作步骤:

1)首先必须创建一个头文件,它包含你想要从DLL输出的函数原型、结构和符号。DLL的所有源代码模块均包含该头文件,以帮助创建DLL。后面将会看到,当创建需要使用DLL中包含的函数和变量的可执行模块(或多个模块)时,也需要这个头文件。

2)要创建一个C/C++源代码模块(或多个模块),用于实现你想要在DLL模块中实现的函数和变量。由于这些源代码模块在创建可执行模块时是不必要的,因此创建DLL的公司能够保护公司的秘密。

3)创建DLL模块,将使编译器对每个源代码模块进行处理,产生一个.obj模块(每个源代码模块有一个.obj模块)。

4)当所有的.obj模块创建完成后,链接程序将所有.obj模块的内容组合在一起,产生一个DLL映象文件。该映像文件(即模块)包含了用于DLL的所有二进制代码和全局/静态数据变量。为了执行这个可执行模块,该文件是必不可少的。

5)如果链接程序发现DLL的源代码模块至少输出了一个函数或变量,那么链接程序也生成一个.lib文件。这个.lib文件很小,因为它不包含任何函数或变量。它只是列出所有已输出函数和变量的符号名。为了创建可执行模块,该文件是必不可少的。

一旦创建了DLL模块,就可以创建可执行模块。其创建步骤是:

6)在引用函数、变量、数据、结构或符号的所有源代码模块中,必须包含DLL开发人员创

建的头文件。

7)要创建一个C/C++源代码模块(或多个模块),用于实现你想要在可执行模块中实现的函数和变量。当然该代码可以引用DLL头文件中定义的函数和变量。

8)创建可执行模块,将使编译器对每个源代码模块进行处理,生成一个.obj模块(每个源代码模块有一个.obj模块)。

9)当所有.obj模块创建完成后,链接程序便将所有的.obj模块的内容组合起来,生成一个可执行的映像文件。该映像文件(或模块)包含了可执行文件的所有二进制代码和全局/静态变量。该可执行模块还包含一个输入节,列出可执行文件需要的所有DLL模块名(关于各个节的详细说明,参见第17章)。此外,对于列出的每个DLL名字,该节指明了可执行模块的二进制代码引用了哪些函数和变量符号。下面你会看到操作系统的加载程序将对该输入节进行分析。一旦DLL和可执行模块创建完成,一个进程就可以执行。当试图运行可执行模块时,操作系统的加载程序将执行下面的操作步骤:

10)加载程序为新进程创建一个虚拟地址空间。可执行模块被映射到新进程的地址空间。加载程序对可执行模块的输入节进行分析。对于该节中列出的每个DLL名字,加载程序要找出用户系统上的DLL模块,再将该DLL映射到进程的地址空间。注意,由于DLL模块可以从另一个DLL模块输入函数和变量,因此DLL模块可以拥有它自己的输入节。若要对进程进行全面的初始化,加载程序要分析每个模块的输入节,并将所有需要的DLL模块映射到进程的地址空间。如你所见,对进程进行初始化是很费时间的。

一旦可执行模块和所有DLL模块被映射到进程的地址空间中,进程的主线程就可以启动运行,同时应用程序也可以启动运行。下面各节将更加详细地介绍这个进程的运行情况。

 楼主| 发表于 2006-3-28 18:46:35 | 显示全部楼层
动态链接库(4) 创建DLL模块

当创建DLL时,要创建一组可执行模块(或其他DLL)可以调用的函数。DLL可以将变量、函数或C/C++类输出到其他模块。在实际工作环境中,应该避免输出变量,因为这会删除你的代码中的一个抽象层,使它更加难以维护你的DLL代码。此外,只有当使用同一个供应商提供的编译器对输入C++类的模块进行编译时,才能输出C++类。由于这个原因,也应该避免输出C++类,除非知道可执行模块的开发人员使用的工具与DLL模块开发人员使用的工具相同。

当创建DLL模槭保?紫扔Ω媒?⒁桓鐾肺募??梦募???四阆胍?涑龅谋淞浚ɡ嘈秃兔?郑┖秃??ㄔ?秃兔?郑?M肺募?贡匦攵ㄒ逵糜谑涑龊??捅淞康娜魏畏?藕褪?萁峁埂D愕腄LL的所有源代码模块都应该包含这个头文件。另外,必须分配该头文件,以便它能够包含在可能输入这些函数或变量的任何源代码中。拥有单个头文件,供DLL创建程序和可执行模块的创建程序使用,就可以大大简化维护工作。

下面的代码说明了应该如何对单个头文件进行编码,以便同时包含可执行文件和DLL的源代码文件:

/*************************************************************************

Module: MyLib.h

*************************************************************************/

#ifdef MYLIBAPI

// MYLIBAPI should be defined in all of the DLLs source

// code modules before this header file is included.

// All functions/variables are being exported

#else

// This header file is included by an EXE source code module

//Indicate that all functions/variables are being imported

#define MYLIBAPI extern "C" _declspec(dllimport)

#endif

/////////////////////////////////////////////////////////////////

//Define any data structures and symbols here

//////////////////////////////////////////////////////////////////

//Define exported variables here .(NOTE: Avoid exporting variables.)

MYLIBAPI int g_nResult;

/////////////////////////////////////////////////////////////////

//Define exported function prototypes here.

MYLIBAPI int Add(int nLeft , int nRight);

////////////////////////// End of File /////////////////////////

在你的每个DLL源代码文件中,应该包含下面的头文件:

/*****************************************************************

Module: MyLibFile1.cpp

******************************************************************/

// Include the standar Windows and C-Runtime header files here.

#include

//This DLL source code file exports functions and variables

#define MYLIBAPI extern "C" _declspce(dllexport)

//Include the exported data structures,symbols,functions,and variable.

#include "MyLib.h"

///////////////////////////////////////////////////////////////

// Place the code for this DLL source code file here

int g_nResult;

int Add(int nLeft,int nRight) {

g_nResult = nLeft + nRight;

return(g_nResult)

};

//////////////////////// End of File ////////////////////////

当上面的DLL源代码文件被编译时,在MyLib.h头文件的前面使用__declspec(DLLexport)对MYLIBAPI进行定义。当编译器看到负责修改变量、函数或C++类的__declspec(DLLexport)时,它就知道该变量、函数或C++类是从产生的DLL模块输出的。注意,MYLIBAPI标志被置于头文件中要输出的变量的定义之前和要输出的函数之前。

另外,在源代码文件(MyLibFile1.cpp0)中,MYLIBAPI标志并不出现在输出的变量和函数之前。MYLIBAPI标志在这里是不必要的,因为编译器在分析头文件时能够记住要输出哪些变量或函数。

你会发现,MYLIBAPI标志包含了extern“C”修改符。只有当你编写C++代码而不是直接编写C代码时,才能使用这个修改符。通常来说,C++编译器可能会改变函数和变量的名字,从而导致严重的链接程序问题。例如,假设你用C++编写一个DLL,并直接用C编写一个可执行模块,当你创建DLL时,函数名被改变,但是,当你创建可执行模块时,函数名没有改变。当链接程序试图链接可执行模块时,它就会抱怨说,可执行模块引用的符号不存在。如果使用extern“C”,就可以告诉编译器不要改变变量名或函数名,这样,变量和函数就可以供使用C、C++或任何其他编程语言编写的可执行模块来访问。

现在你已经知道DLL源代码文件是如何使用这个头文件的。但是,可执行模块的源代码文件情况又是如何呢?可执行模块的源代码文件不应该在这个头文件的前面定义MYLIBAPI。由于MYLIBAPI没有定义,因此头文件将MYLIBAPI定义为__declspec(DLLimport)。编译器看到可执行模块的源代码文件从DLL模块输入变量和函数。

如果观察Microsoft的标准Windows头文件,如WinBase.h,你将会发现Microsoft使用的方法基本上与上面介绍的方法相同。

 楼主| 发表于 2006-3-28 18:47:24 | 显示全部楼层
动态链接库(5) 输出的真正含义是什么

上一节介绍的一个真正有意思的东西是__declspec(DLLexport)修改符。当Microsoft的C/C++编译器看到变量、函数原型或C++类之前的这个修改符的时候,它就将某些附加信息嵌入产生的.obj文件中。当链接DLL的所有.obj文件时,链接程序将对这些信息进行分析。

当DLL被链接时,链接程序要查找关于输出变量、函数或C++类的信息,并自动生成一个.lib文件。该.lib文件包含一个DLL输出的符号列表。当然,如果要链接引用该DLL的输出符号的任何可执行模块,该.lib文件是必不可少的。除了创建.lib文件外,链接程序还要将一个输出符号表嵌入产生的DLL文件。这个输出节包含一个输出变量、函数和类符号的列表(按字母顺序排列)。该链接程序还将能够指明在何处找到每个符号的相对虚拟地址(RVA)放入DLL模块。

使用Microsoft的VisualStudio的DumpBin.exe实用程序(带有-exports开关),你能够看到DLL的输出节是个什么样子。下面是Kernel32.DLL的输出节的一个代码段(我已经删除了DUMPBIN的某些输出,这样就不会占用本书的太多篇幅)。

动态链接库(6) 创建用于非VisualC++工具的DLL

如果使用MicrosoftVisualC++来创建DLL和将要链接到该DLL的可执行模块,可以跳过本节内容的学习。但是,如果使用VisualC++创建DLL,而这个DLL要链接到使用任何供应商的工具创建的可执行模块,那么必须做一些额外的工作。

前面讲过当进行C和C++混合编程时使用extern“C”修改符的问题。也讲过C++类的问题以及为什么因为名字改变的缘故你必须使用同一个编译器供应商的工具的问题。当你直接将C语言编程用于多个工具供应商时将会出现另一个问题。这个问题是,即使你根本不使用C++,Microsoft的C编译器也会损害C函数。当你的函数使用__stdcall(WINAPI)调用规则时会出现这种问题。这种调用规则是最流行的一种类型。当使用__stdcall将C函数输出时,Microsoft的编译器就会改变函数的名字,设置一个前导下划线,再加上一个@符号的前缀,后随一个数字,表示作为参数传递给函数的字节数。例如,下面的函数是作为DLL的输出节中的_MyFunc@8输出的:

_declspec(dllexport) LONG _stdcall MyFunc(int a, int b);

如果用另一个供应商的工具创建了一个可执行模块,它将设法链接到一个名叫MyFunc的函数,该函数在Microsoft编译器已有的DLL中并不存在,因此链接将失败。

若要使用与其他编译器供应商的工具链接的Microsoft的工具创建一个可执行模块,必须告诉Microsoft的编译器输出没有经过改变的函数名。可以用两种方法来进行这项操作。第一种方法是为编程项目建立一个.def文件,并在该.def文件中加上类似下面的EXPORTS节:

EXPORTS

MyFunc

当Microsoft的链接程序分析这个.def文件时,它发现_MyFunc@8和MyFunc均被输出。由于这两个函数名是互相匹配的(除了截断的尾部外),因此链接程序使用MyFunc的.def文件名来输出该函数,而根本不使用_MyFunc@8的名字来输出函数。

现在你可能认为,如果使用Microsoft的工具创建一个可执行模块,并且设法将它链接到包含未截断名字的DLL,那么链接程序的运行将会失败,因为它将试图链接到称为_MyFunc@8的函数。当然,你会高兴地了解到Microsoft的链接程序进行了正确的操作,将可执行模块链接到名字为MyFunc的函数。

如果想避免使用.def文件,可以使用第二种方法输出未截断的函数版本。在DLL的源代码模块中,可以添加下面这行代码:

#pragma comment(linker , "/export:MyFunc = _MyFunc@8")

这行代码使得编译器发出一个链接程序指令,告诉链接程序,一个名叫MyFunc的函数将被输出,其进入点与称为_MyFunc@8的函数的进入点相同。第二种方法没有第一种方法容易,因为你必须自己截断函数名,以便创建该代码行。另外,当使用第二种方法时,DLL实际上输出用于标识单个函数的两个符号,即MyFunc和_MyFunc@8,而第一种方法只输出符号MyFunc。第二种方法并没有给你带来更多的好处,它只是使你可以避免使用.def的文件而已。

 楼主| 发表于 2006-3-28 18:47:47 | 显示全部楼层
动态链接库(7) 创建可执行模块

下面的代码段显示了一个可执行的源代码文件,它输入了DLL的输出符号,并且在代码中引用了这些符号。

/*****************************************************************

Module: MyExeFile1.cpp

******************************************************************/

// Include the standar Windows and C-Runtime header files here.

#include

//Include the exported data structures,symbols , functions,and variables

#include "MyLib\MyLib.h"

///////////////////////////////////////////////////////////////

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, LPTSTR pszCmdLine,int) {

int nLeft = 10, nRight = 25;

TCHAR sz[100];

wsprintf(sz,TEXT("%d+%d = %d"), nLeft,nRight,Add(nLeft,nRight));

MessageBox(NULL,sz,TEXT("Calculation"),MB_OK);

wsprintf(sz,TEXT("The result from the last Add is: %d"), g_nResult);

MessageBox(NULL, sz, TEXT("Last Result"),MB_OK);

return(0);

}

//////////////////////// End of File ////////////////////////

当创建可执行源代码文件时,必须加上DLL的头文件。如果没有头文件,输入的符号将不会被定义,而且编译器将会发出许多警告和错误消息。

可执行源代码文件不应该定义DLL的头文件前面的MYLIBAPI。当上面显示的这个可执行源代码文件被编译时,MYLIBAPI由MyLib.h头文件使用__declspec(DLLimport)进行定义。当编译器看到修改变量、函数或C++类的__declspec(DLLimport)时,它知道这个符号是从某个DLL模块输入的。它不知道是从哪个DLL模块输入的,并且它也不关心这个问题。编译器只想确保你用正确的方法访问这些输入的符号。现在你在源代码中可以引用输入的符号,一切都将能够正常工作。

接着,链接程序必须将所有.obj模块组合起来,创建产生的可执行模块。该链接程序必须确定哪些DLL包含代码引用的所有输入符号的DLL。因此你必须将DLL的.lib文件传递给链接程序。如前所述,.lib文件只包含DLL模块输出的符号列表。链接程序只想知道是否存在引用的符号和哪个DLL模块包含该符号。如果连接程序转换了所有外部符号的引用,那么可执行模块就因此而产生了。

 楼主| 发表于 2006-3-28 18:48:11 | 显示全部楼层
动态链接库(8) 输入的真正含义是什么

上一节介绍了修改符--declspec(DLLimport)。当输入一个符号时,不必使用关键字--declspec(DLLimport),只要使用标准的C关键字extern即可。但是,如果编译器预先知道你引用的符号将从一个DLL的.lib文件输入,那么编译器就能够生成运行效率稍高的代码。因此建议你尽量将--declspec(DLLimport)关键字用于输入函数和数据符号。当你调用标准Windows函数中的任何一个时,Microsoft将为你进行这项设置。

当链接程序进行输入符号的转换时,它就将一个称为输入节的特殊的节嵌入产生的可执行模块。输入节列出了该模块需要的DLL模块以及由每个DLL模块引用的符号。

使用VisualStudio的DumpBin.exe实用程序(带有-imports开关),能够看到模块的输入节的样子。下面是Calc.exe文件的输入节的一个代码段(同样,我删除了DUMPBIN的某些输出,这样它就不会占用太多的篇幅)。

C:\WINNT\SYSTEM32>DUMPBIN -imports Calc.EXE

Microsoft (R) COFF Binary File Dumper Version 6.00.8168

Copyright (C) Microsoft Corp 1992-1998.All rights reserved.

Dump of file calc.exe

File Type: EXECUTABLE IMAGE

Section contains the following imports;

SHELL32.dll

10010F4 Import Address Table

1012820 Import Name Table

FFFFFFFF time date stamp

FFFFFFFF Index of first forwarder reference

77C42983 7A ShellAboutW

MSVCRT.dll

1001094 Import Address Table

10127C0 Import Name Table

FFFFFFFF time date stamp

FFFFFFFF Index of first forwarder reference

78010040 295 memmove

78018124 42 _EH_prolog

78014C34 2D1 toupper

78010F6E 2DD wcschr

78010668 2E3 wcslen

.

.

.

ADVAPI32.dll

1001000 Import Address Table

101272C Import Name Table

FFFFFFFF time date.stamp

FFFFFFFF Index of first forwarder reference

779858F4 19A RegQueryValueExA

77985196 190 RegOpenKeyExA

77984BA1 178 RegCloseKey

KERNEL32.dll

100101C Import Address Table

1012748 Import Name Table

FFFFFFFF time date stamp

FFFFFFFF Index of first forwarder reference

77ED4134 336 lstrcpyW

77ED33EB 1E5 LocalAlloc

77EDEF36 DB GetCommandLineW

77ED1610 15E GetProfileIntW

77ED4BA4 1EC LocalReAlloc

.

.

.

Header contains the followint bound import information

Bount to SHELL32.dll [36E449E0] Mon Mar 08 14:06:24 1999

Bount to MSVCRT.dll [36888379] Fri Feb 05 15:49:13 1999

Bount to ADVAPI32.dll [36E449E1] Mon Mar 08 14:06:25 1999

Bount to KERNEL32.dll [36DDAD55] Wed Mar 03 13:44:53 1999

Bount to GDI32.dll [36E449E0] Mon Mar 08 14:06:24 1999

Bount to USER32.dll [36E449E0] Mon Mar 08 14:06:24 1999

Summary

2000 .data

3000 .rsrc

13000 .text

如你所见,这一节为Calc.exe需要的每个DLL设置了一个项目,这些DLL是Shell32.DLL、MSVCRt.DLL、AdvAPI32.DLL、Kernel32.DLL、GDI32.DLL和User32.DLL。在每个DLL的模块名下面,有一个Calc.exe从该特定模块输入的符号列表。例如,Calc模块调用包含在Kernel32.DLL中的下列函数:lstrcpyW、LocalAlloc、GetCommandLineW和GetProfileIntW等。

紧靠符号名左边的数字是符号的提示(hint)值,它与讨论无关。每个符号行最左边的数字用于指明该符号在进程的地址空间中所在的内存地址。该内存地址只有在可执行模块相链接时才出现。在DumpBin的输出的结尾处,可以看到更多的链接信息。

 楼主| 发表于 2006-3-28 18:49:55 | 显示全部楼层
动态链接库(9) 运行可执行模块

当一个可执行文件被启动时,操作系统加载程序将为该进程创建虚拟地址空间。然后,加载程序将可执行模块映射到进程的地址空间中。加载程序查看可执行模块的输入节,并设法找出任何需要的DLL,并将它们映射到进程的地址空间中。

由于该输入节只包含一个DLL名而没有它的路径名。因此加载程序必须搜索用户的磁盘驱动器,找出DLL。下面是加载程序的搜索顺序:

1)包含可执行映像文件的目录。

2)进程的当前目录。

3)Windows系统目录。

4)Windows目录。

5)PATH环境变量中列出的各个目录。

应该知道其他的东西也会影响加载程序对一个DLL的搜索(详细说明参见第20章)。当DLL模块映射到进程的地址空间中时,加载程序要检查每个DLL的输入节。如果存在输入节(通常它确实是存在的),那么加载程序便继续将其他必要的DLL模块映射到进程的地址空间中。加载程序将保持对DLL模块的跟踪,使模块的加载和映射只进行一次(尽管多个模块需要该模块)。

如果加载程序无法找到需要的DLL模块,用户会看到图19-2、图19-3所示的消息框中的一个:如果是Windows2000,那么将出现图19-2所示的消息框,如果是Windows98,则出现图19-3所示的消息框。

当所有的DLL模块都找到并且映射到进程的地址空间中之后,加载程序就会确定对输入的符号的全部引用。为此,它要再次查看每个模块的输入节。对于列出的每个符号,加载程序都要查看指定的DLL的输出节,以确定该符号是否存在。如果该符号不存在(这种情况很少),那么加载程序就显示图19-4、图19-5所示的消息框之一:如果是Windows2000,那么出现图19-4所示的消息框,如果是Windows98,则出现图19-5所示的消息框。

如果Windows2000版本的消息框指明漏掉的是哪个函数,而不是显示用户难以识别的错误代码0xC000007B,那么这将是非常好的。也许下一个Windows版本能够做到这一点。

如果这个符号不存在,那么加载程序将要检索该符号的RVA,并添加DLL模块被加载到的虚拟地址空间(符号在进程的地址空间中的位置)。然后它将该虚拟地址保存在可执行模块的输入节中。这时,当代码引用一个输入符号时,它将查看调用模块的输入节,并且捕获输入符号的地址,这样它就能够成功地访问输入变量、函数或C++类的成员函数。好了,动态链接完成,进程的主线程开始执行,应用程序终于也开始运行了!

当然,这需要加载程序花费相当多的时间来加载这些DLL模块,并用所有使用输入符号的正确地址来调整每个模块的输入节。由于所有这些工作都是在进程初始化的时候进行的,因此应用程序运行期的性能不会降低。不过,对于许多应用程序来说,初始化的速度太慢是不行的。为了缩短应用程序的加载时间,应该调整你的可执行模块和DLL模块的位置并且将它们连接起来。真可惜很少有开发人员知道如何进行这项操作,因为这些技术是非常重要的。如果每个公司都能够使用这些技术,系统将能运行的更好。实际上,我认为操作系统销售时应该配有一个能够自动执行这些操作的实用程序。下一章将要介绍对模块调整位置和进行连接的方法。

 楼主| 发表于 2006-3-28 18:50:22 | 显示全部楼层
动态链接库(11) 显式加载DLL模块

无论何时,进程中的线程都可以决定将一个DLL映射到进程的地址空间,方法是调用下面两个函数中的一个:

HINSTANCE LoadLibrary(PCTSTR pszDLLPathName);

 

HINSTANCE LoadLibrary(

PCTSTR pszDLLPathName,

HANDLE hFile,

DWORD dwFlags);

这两个函数均用于找出用户系统上的文件映像(使用上一章中介绍的搜索算法),并设法将DLL的文件映像映射到调用进程的地址空间中。两个函数返回的HINSTANCE值用于标识文件映像映射到的虚拟内存地址。如果DLL不能被映射到进程的地址空间,则返回NULL。若要了解关于错误的详细信息,可以调用GetLastError.

你会注意到,LoadLibraryEx函数配有两个辅助参数,即hFile和dwFlags。参数hFile保留供将来使用,现在必须是NULL。对于参数dwFlags,必须将它设置为0,或者设置为DONT_RESOLVE_DLL_REFERENCES、LOAD_LIBRARY_AS_DATAFILE和LOAD_WITH_ALTERED_SEARCH_PATH等标志的一个组合。

1.DONT_RESOLVE_DLL_REFERENCES

DONT_RESOLVE_DLL_REFERENCES标志用于告诉系统将DLL映射到调用进程的地址空间中。通常情况下,当DLL被映射到进程的地址空间中时,系统要调用DLL中的一个特殊函数,即DllMain(本章后面介绍)。该函数用于对DLL进行初始化。DONT_RESOLVE_DLL_REFERENCES标志使系统不必调用DllMain函数就能映射文件映像。

此外,DLL能够输入另一个DLL中包含的函数。当系统将一个DLL映射到进程的地址空间中时,它也要查看该DLL是否需要其他的DLL,并且自动加载这些DLL。当DONT_RESOLVE_DLL_REFERENCES标志被设定时,系统并不自动将其他的DLL加载到进程的地址空间中。

2.LOAD_LIBRARY_AS_DATAFILE

LOAD_LIBRARY_AS_DATAFILE标志与DONT_RESOLVE_DLL_REFERENCES标志相类似,因为系统只是将DLL映射到进程的地址空间中,就像它是数据文件一样。系统并不花费额外的时间来准备执行文件中的任何代码。例如,当一个DLL被映射到进程的地址空间中时,系统要查看DLL中的某些信息,以确定应该将哪些页面保护属性赋予文件的不同的节。如果设定了LOAD_LIBRARY_AS_DATAFILE标志,系统将以它要执行文件中的代码时的同样方式来设置页面保护属性。

由于下面几个原因,该标志是非常有用的。首先,如果有一个DLL(它只包含资源,但不包含函数),那么可以设定这个标志,使DLL的文件映像能够映射到进程的地址空间中。然后可以在调用加载资源的函数时,使用LoadLibraryEx函数返回的HINSTANCE值。通常情况下,加载一个.exe文件,就能够启动一个新进程,但是也可以使用LoadLibraryEx函数将.exe文件的映像映射到进程的地址空间中。借助映射的.exe文件的HINSTANCE值,就能够访问文件中的资源。由于.exe文件没有DllMain函数,因此,当调用LoadLibraryEx来加载一个.exe文件时,必须设定LOAD_LIBRARY_AS_DATAFILE标志。

3.LOAD_WITH_ALTERED_SEARCH_PATH

LOAD_WITH_ALTERED_SEARCH_PATH标志用于改变LoadLibraryEx用来查找特定的DLL文件时使用的搜索算法。通常情况下,LoadLibraryEx按照第19章讲述的顺序进行文件的搜索。但是,如果设定了LOAD_WITH_ALTERED_SEARCH_PATH标志,那么LoadLibraryEx函数就按照下面的顺序来搜索文件:

1)pszDLLPathName参数中设定的目录。

2)进程的当前目录。

3)Windows的系统目录。

4)Windows目录。

5)PATH环境变量中列出的目录。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2025-6-24 00:38

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表