为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

DLL基础

2010-12-25 14页 pdf 701KB 31阅读

用户头像

is_164435

暂无简介

举报
DLL基础 下载 第1 9章 D L L基础 自从M i c r o s o f t公司推出第一个版本的Wi n d o w s操作系统以来,动态链接库(D L L)一直是 这个操作系统的基础。 Windows API中的所有函数都包含在 D L L中。3个最重要的 D L L是 K e r n e l 3 2 . d l l,它包含用于管理内存、进程和线程的各个函数; U s e r 3 2 . d l l,它包含用于执行用 户界面任务(如窗口的创建和消息的传送)的各个函数; G D I 3 2 . d l l,它包含用于画图...
DLL基础
下载 第1 9章 D L L基础 自从M i c r o s o f t公司推出第一个版本的Wi n d o w s操作系统以来,动态链接库(D L L)一直是 这个操作系统的基础。 Windows API中的所有函数都包含在 D L L中。3个最重要的 D L L是 K e r n e l 3 2 . d l l,它包含用于管理内存、进程和线程的各个函数; U s e r 3 2 . d l l,它包含用于执行用 户界面任务(如窗口的创建和消息的传送)的各个函数; G D I 3 2 . d l l,它包含用于画图和显示 文本的各个函数。 Wi n d o w s还配有若干别的 D L L,它们提供了用于执行一些特殊任务的函数。例如, A d v A P I 3 2 . d l l包含用于实现对象安全性、注册表操作和事件记录的函数; C o m D l g 3 2 . d l l包含常 用对话框(如File Open和File Save);C o m C t l 3 2 . D L L则支持所有的常用窗口控件。 本章将要介绍如何为应用程序创建D L L。下面是为什么要使用D L L的一些原因: • 它们扩展了应用程序的特性。由于 D L L能够动态地装入进程的地址空间,因此应用程序 能够在运行时确定需要执行什么操作,然后装入相应的代码,以便根据需要执行这些操 作。例如,当一家公司开发了一种产品,想要让其他公司改进或增强该产品的功能时, 那么就可以使用D L L。 • 它们可以用许多种编程语言来编写。可以选择手头拥有的最好的语言来编写 D L L。也许 你的应用程序的用户界面使用Microsoft Visual Basic编写得最好,但是用C + +来处理它的 商用逻辑更好。系统允许Visual Basic程序加载C++ DLL、Cobol DLL和Fortran DLL等。 • 它们简化了软件项目的管理。如果在软件开发过程中不同的工作小组在不同的模块上工 作,那么这个项目管理起来比较容易。但是,应用程序在销售时附带的文件应该尽量少 一些。我知道有一家公司销售的产品附带了 1 0 0个D L L——每个程序员最多有 5个D L L。 这样,应用程序的初始化时间将会长得吓人,因为系统必须打开 1 0 0个磁盘文件之后,程 序才能执行它的操作。 • 它们有助于节省内存。如果两个或多个应用程序使用同一个 D L L,那么该D L L的页面只 要放入R A M一次,所有的应用程序都可以共享它的各个页面。 C / C + +运行期库就是个极 好的例子。许多应用程序都使用这个库。如果所有的应用程序都链接到这个静态库,那 么s p r i n t f、s t r c p y和m a l l o c等函数的代码就要多次存在于内存中。但是,如果所有这些应 用程序链接到DLL C/C++运行期库,那么这些函数的代码就只需要放入内存一次,这意 味着内存的使用将更加有效。 • 它们有助于资源的共享。 D L L可以包含对话框模板、字符串、图标和位图等资源。多个 应用程序能够使用D L L来共享这些资源。 • 它们有助于应用程序的本地化。应用程序常常使用 D L L对自己进行本地化。例如,只包 含代码而不包含用户界面组件的应用程序可以加载包含本地化用户界面组件的 D L L。 • 它们有助于解决平台差异。不同版本的 Wi d n o w s配有不同的函数。开发人员常常想要调 用新的函数(如果它们存在于主机的 Wi n d o w s版本上的话)。但是,如果你的源代码包含 第四部分 动态链接库 了对一个新函数的调用,而你的应用程序将要在不能提供该函数的 Wi n d o w s版本上运行, 那么操作系统的加载程序将拒绝运行你的进程。即使你实际上从不调用该函数,情况也 是这样。如果将这些新函数保存在 D L L中,那么应用程序就能够将它们加载到 Wi n d o w s 的老版本上。当然,你仍然可以成功地调用该函数。 • 它们可以用于一些特殊的目的。 Wi n d o w s使得某些特性只能为 D L L所用。例如,只有当 D L L中包含某个挂钩函数的时候,才能安装某些挂钩(使用 S e t Wi n d o w s H o o k E x和 S e t Wi n E v e n t H o o k来进行安装)。可以通过创建必须在 D L L中生存的C O M对象来扩展 Windows Explorer的外壳程序。对于可以由We b浏览器加载的、用于创建内容丰富的We b 页的A c t i v e X控件来说,情况也是一样 . 19.1 DLL与进程的地址空间 创建D L L常常比创建应用程序更容易,因为D L L往往包含一组应用程序可以使用的自主函 数。在D L L中通常没有用来处理消息循环或创建窗口的支持代码。 D L L只是一组源代码模块, 每个模块包含了应用程序(可执行文件)或另一个 D L L将要调用的一组函数。当所有源代码文 件编译后,它们就像应用程序的可执行文件那样被链接程序所链接。但是,对于一个D L L来说, 你必须设定该连链程序的 / D L L开关。这个开关使得链接程序能够向产生的 D L L文件映像发出 稍有不同的信息,这样,操作系统加载程序就能将该文件映像视为一个 D L L而不是应用程序。 在应用程序(或另一个D L L)能够调用D L L中的函数之前,D L L文件映像必须被映射到调 用进程的地址空间中。若要进行这项操作,可以使用两种方法中的一种,即加载时的隐含链接 或运行期的显式链接。隐含链接将在本章的后面部分介绍,显式链接将在第 2 0章中介绍。 一旦D L L的文件映像被映射到调用进程的地址空间中, D L L的函数就可以供进程中运行的 所有线程使用。实际上, D L L几乎将失去它作为 D L L的全部特征。对于进程中的线程来说, D L L的代码和数据看上去就像恰巧是在进程的地址空间中的额外代码和数据一样。当一个线程 调用D L L函数时,该D L L函数要查看线程的堆栈,以便检索它传递的参数,并将线程的堆栈用 于它需要的任何局部变量。此外,D L L中函数的代码创建的任何对象均由调用线程所拥有,而 D L L本身从来不拥有任何东西。 例如,如果Vi r t u a l A l l o c函数被D L L中的一个函数调用,那么将从调用线程的进程地址空间 中保留一个地址空间的区域,该地址空间区域将始终处于保留状态,因为系统并不跟踪 D L L中 的函数保留该区域的情况。保留区域由进程所拥有,只有在线程调用 Vi r t u a l F r e e函数或者进程 终止运行时才被释放。 如你所知,可执行文件的全局变量和静态变量不能被同一个可执行文件的多个运行实例共 享。Windows 98能够确保这一点,方法是在可执行文件被映射到进程的地址空间时为可执行文 件的全局变量和静态变量分配相应的存储器。Windows 2000确保这一点的方法是使用第1 3章介 绍的写入时拷贝(c o p y - o n - w r i t e)机制。D L L中的全局变量和静态变量的处理方法是完全相同 的。当一个进程将D L L的映像文件映射到它的地址空间中去时,系统将同时创建全局数据变量 和静态数据变量的实例。 注意 必须注意的是,单个地址空间是由一个可执行模块和若干个 D L L模块组成的。 这些模块中,有些可以链接到静态版本的 C / C + +运行期库,有些可以链接到一个 D L L 版本的 C / C + +运行期库,而有些模块(如果不是用 C / C + +编写的话)则根本不需要 C / C + +运行期库。许多开发人员经常会犯一个常见的错误,因为他们忘记了若干个 C / C + +运行期库可以存在于单个地址空间中。请看下面的代码: 464计计第四部分 动态链接库 下载 那么你是怎么看待这个问的呢?上面这个代码能够正确运行吗? D L L函数分配 的内存块是由E X E的函数释放的吗?是可能的。上面显示的代码并没有为你提供 足够的信息。如果E X E和D L L都链接到D L L的C / C + +运行期库,那么上面的代码将能够 很好地运行。但是,如果两个模块中的一个或者两个都链接到静态C / C + +运行期库,那 么对free函数的调用就会失败。我经常看到编程人员编写这样的代码,结果都失败了。 有一个很方便的方法可以解决这个问题。当一个模块提供一个用于分配内存块的 函数时,该模块也必须提供释放内存的函数。让我们将上面的代码改写成下面的样子: 这个代码是正确的,它始终都能正确地运行。当你编写一个模块时,不要忘记其 他模块中的函数也许没有使用C / C + +来编写,因此可能无法使用m a l l o c和f r e e函数进行 内存的分配。应该注意不要在代码中使用这些假设条件。另外,在内部调用 m a l l o c和 f r e e函数时,这个原则对于C + +的n e w和d e l e t e操作符也是适用的。 19.2 DLL的总体运行情况 为了全面理解D L L是如何运行的以及你和系统如何使用 D L L,让我们首先观察一下D L L的 整个运行情况。图1 9 - 1综合说明了它的所有组件一道配合运行的情况。 现在要重点介绍可执行模块和 D L模块之间是如何隐含地互相链接的。隐含链接是最常用 的链接类型。Wi n d o w s也支持显式链接(第2 0章介绍这个问题)。 在图1 9 - 1中你可以看到,当一个模块(比如一个可执行文件)使用D L L中的函数或变量时, 将有若干个文件和组件参与发挥作用。为了简单起见,我将“可执行模块”称为来自 D L L的输 第19章 DLL 基础计计465 下载 入函数和变量,将“D L L模块”称为用于可执行模块的输出函数和变量。但是要记住, D L L模 块能够(并且确实常常)输入包含在其他D L L模块中的函数和变量。 图19-1 应用程序如何创建和隐含链接D L L的示意图 若要创建一个从D L L模块输入函数和变量的可执行模块,必须首先创建一个 D L L模块。然 后就可以创建可执行模块。 若要创建D L L模块,必须执行下列操作步骤: 1) 首先必须创建一个头文件,它包含你想要从D L L输出的函数原型、结构和符号。D L L的 所有源代码模块均包含该头文件,以帮助创建 D L L。后面将会看到,当创建需要使用D L L中包 含的函数和变量的可执行模块(或多个模块)时,也需要这个头文件。 2) 要创建一个C / C + +源代码模块(或多个模块),用于实现你想要在D L L模块中实现的函 数和变量。由于这些源代码模块在创建可执行模块时是不必要的,因此创建 D L L的公司能够保 466计计第四部分 动态链接库 下载 创造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加载到地址空间中进程的主线程开始执行;应用程序启动运行。 编译器 编译器 链接程序 链接程序 编译器 编译器 编译器 编译器 护公司的秘密。 3) 创建D L L模块,将使编译器对每个源代码模块进行处理,产生一个 . o b j模块(每个源代 码模块有一个 . o b j模块)。 4) 当所有的 . o b j模块创建完成后,链接程序将所有 . o b j模块的内容组合在一起,产生一个 D L L映象文件。该映像文件(即模块)包含了用于 D L L的所有二进制代码和全局 /静态数据变 量。为了执行这个可执行模块,该文件是必不可少的。 5) 如果链接程序发现D L L的源代码模块至少输出了一个函数或变量,那么链接程序也生成 一个. l i b文件。这个 . l i b文件很小,因为它不包含任何函数或变量。它只是列出所有已输出函数 和变量的符号名。为了创建可执行模块,该文件是必不可少的。 一旦创建了D L L模块,就可以创建可执行模块。其创建步骤是: 6) 在引用函数、变量、数据、结构或符号的所有源代码模块中,必须包含 D L L开发人员创 建的头文件。 7) 要创建一个C / C + +源代码模块(或多个模块),用于实现你想要在可执行模块中实现的 函数和变量。当然该代码可以引用D L L头文件中定义的函数和变量。 8) 创建可执行模块,将使编译器对每个源代码模块进行处理,生成一个 . o b j模块(每个源 代码模块有一个 . o b j模块)。 9) 当所有. o b j模块创建完成后,链接程序便将所有的. o b j模块的内容组合起来,生成一个可 执行的映像文件。该映像文件(或模块)包含了可执行文件的所有二进制代码和全局/静态变量。 该可执行模块还包含一个输入节,列出可执行文件需要的所有 D L L模块名(关于各个节的详细 说明,参见第1 7章)。此外,对于列出的每个D L L名字,该节指明了可执行模块的二进制代码 引用了哪些函数和变量符号。下面你会看到操作系统的加载程序将对该输入节进行分析。 一旦D L L和可执行模块创建完成,一个进程就可以执行。当试图运行可执行模块时,操作 系统的加载程序将执行下面的操作步骤: 10) 加载程序为新进程创建一个虚拟地址空间。可执行模块被映射到新进程的地址空间。 加载程序对可执行模块的输入节进行分析。对于该节中列出的每个 D L L名字,加载程序要找出 用户系统上的D L L模块,再将该D L L映射到进程的地址空间。注意,由于 D L L模块可以从另一 个D L L模块输入函数和变量,因此D L L模块可以拥有它自己的输入节。若要对进程进行全面的 初始化,加载程序要分析每个模块的输入节,并将所有需要的D L L模块映射到进程的地址空间。 如你所见,对进程进行初始化是很费时间的。 一旦可执行模块和所有D L L模块被映射到进程的地址空间中,进程的主线程就可以启动运 行,同时应用程序也可以启动运行。下面各节将更加详细地介绍这个进程的运行情况。 19.3 创建D L L模块 当创建DLL 时,要创建一组可执行模块(或其他D L L)可以调用的函数。D L L可以将变量、 函数或C / C + +类输出到其他模块。在实际工作环境中,应该避免输出变量,因为这会删除你的 代码中的一个抽象层,使它更加难以维护你的 D L L代码。此外,只有当使用同一个供应商提供 的编译器对输入C + +类的模块进行编译时,才能输出 C + +类。由于这个原因,也应该避免输出 C + +类,除非知道可执行模块的开发人员使用的工具与D L L模块开发人员使用的工具相同。 当创建D L L模块时,首先应该建立一个头文件,该文件包含了你想要输出的变量(类型和 名字)和函数(原型和名字)。头文件还必须定义用于输出函数和变量的任何符号和数据结构。 你的D L L的所有源代码模块都应该包含这个头文件。另外,必须分配该头文件,以便它能够包 第19章 DLL 基础计计467 下载 含在可能输入这些函数或变量的任何源代码中。拥有单个头文件,供 D L L创建程序和可执行模 块的创建程序使用,就可以大大简化维护工作。 下面的代码说明了应该如何对单个头文件进行编码,以便同时包含可执行文件和 D L L的源 代码文件: 在你的每个D L L源代码文件中,应该包含下面的头文件: 468计计第四部分 动态链接库 下载 当上面的D L L源代码文件被编译时,在M y L i b . h头文件的前面使用_ _ d e c l s p e c ( d l l e x p o r t )对 M Y L I B A P I进行定义。当编译器看到负责修改变量、函数或 C + +类的_ _ d e c l s p e c ( d l l e x p o r t )时,它 就知道该变量、函数或C + +类是从产生的D L L模块输出的。注意,M Y L I B A P I标志被置于头文 件中要输出的变量的定义之前和要输出的函数之前。 另外,在源代码文件(M y L i b F i l e 1 . c p p 0)中,MYLIBAPI 标志并不出现在输出的变量和 函数之前。M Y L I B A P I标志在这里是不必要的,因为编译器在分析头文件时能够记住要输出哪 些变量或函数。 你会发现,M Y L I B A P I标志包含了e x t e r n“C”修改符。只有当你编写C + +代码而不是直接 编写C代码时,才能使用这个修改符。通常来说, C + +编译器可能会改变函数和变量的名字, 从而导致严重的链接程序问题。例如,假设你用 C + +编写一个D L L,并直接用C编写一个可执 行模块,当你创建D L L时,函数名被改变,但是,当你创建可执行模块时,函数名没有改变。 当链接程序试图链接可执行模块时,它就会抱怨说,可执行模块引用的符号不存在。如果使用 e x t e r n“C”,就可以告诉编译器不要改变变量名或函数名,这样,变量和函数就可以供使用 C、 C + +或任何其他编程语言编写的可执行模块来访问。 现在你已经知道D L L源代码文件是如何使用这个头文件的。但是,可执行模块的源代码文 件情况又是如何呢?可执行模块的源代码文件不应该在这个头文件的前面定义 M Y L I B A P I。由 于M Y L I B A P I没有定义,因此头文件将M Y L I B A P I定义为_ _ d e c l s p e c ( d l l i m p o r t )。编译器看到可 执行模块的源代码文件从D L L模块输入变量和函数。 如果观察M i c r o s o f t的标准Wi n d o w s头文件,如Wi n B a s e . h,你将会发现M i c r o s o f t使用的方 法基本上与上面介绍的方法相同。 19.3.1 输出的真正含义是什么 上一节介绍的一个真正有意思的东西是 _ _ d e c l s p e c ( d l l e x p o r t )修改符。当M i c r o s o f t的C / C + + 编译器看到变量、函数原型或C + +类之前的这个修改符的时候,它就将某些附加信息嵌入产生 的. o b j文件中。当链接D L L的所有. o b j文件时,链接程序将对这些信息进行分析。 当D L L被链接时,链接程序要查找关于输出变量、函数或 C + +类的信息,并自动生成一 个. l i b文件。该 . l i b文件包含一个D L L输出的符号列表。当然,如果要链接引用该 D L L的输出符 号的任何可执行模块,该 . l i b文件是必不可少的。除了创建 . l i b文件外,链接程序还要将一个输 出符号表嵌入产生的D L L文件。这个输出节包含一个输出变量、函数和类符号的列表(按字母 顺序排列)。该链接程序还将能够指明在何处找到每个符号的相对虚拟地址( RVA)放入D L L 模块。 使用M i c r o s o f t的Visual Studio的D u m p B i n . e x e实用程序(带有 - e x p o r t s开关),你能够看到 第19章 DLL 基础计计469 下载 D L L的输出节是个什么样子。下面是 K e r n e l 3 2 . d l l的输出节的一个代码段(我已经删除了 D U M P B I N的某些输出,这样就不会占用本书的太多篇幅)。 如你所见,这些符号是按字母顺序排列的, RVA这一列下面的数字用于指明在D L L文件映 像中的什么位置能够找到输出符号的位移量。序号列可以与 1 6位Wi n d o w s源代码向后兼容,并 且它不应该用于现在的应用程序中。 h i n t(提示码)列可供系统用来改进代码的运行性能,在 此并不重要。 注意 许多开发人员常常通过为函数赋予一个序号值来输出 D L L函数。对于那些来自 1 6位Wi n d o w s环境的函数来说,情况尤其是如此。但是, M i c r o s o f t并没有公布系统 D L L的序号值。当你的可执行模块或 D L L模块链接到任何一个 Wi n d o w s函数时, 470计计第四部分 动态链接库 下载 M i c r o s o f t要求你使用符号的名字进行链接。如果你按照序号进行链接,那么你的应用 程序有可能无法在其他Wi n d o w s平台或将来的Wi n d o w s平台上运行。 实际上,我就遇到过这样的情况。我曾经发布了一个示例应用程序,它使用 Microsoft System Journal中的序号。我的应用程序在 Windows NT 3.1上运行得很好, 但是当Windows NT 3.5推出时,我的应用程序就无法正确地运行。为了解决这个问题, 我不得不用函数名代替序号。现在该应用程序既能够在 Windows NT 3.1上运行,而且 能够在所有更新的版本上运行。 我问过M i c r o s o f t公司,为什么它不使用序号,我得到的回答是:“我们认为可移植 的可执行文件格式不仅具有序号的优点(查找迅速),而且提供了按名字输入的灵活性。 我们可以随时增加函数。在带有多个实现代码的大型程序项目中,序号很难管理。” 你可以将序号用于你创建的任何 D L L,并且按照序号将你的可执行文件链接到这 些D L L。M i c r o s o f t保证,即使在将来的操作系统版本中,这个方法也是可行的。但是 我在我的工作中总是避免使用序号,并且从现在起只按名字进行链接。 19.3.2 创建用于非Visual C++工具的D L L 如果使用Microsoft Visual C++来创建D L L和将要链接到该D L L的可执行模块,可以跳过本 节内容的学习。但是,如果使用 Visual C++创建D L L,而这个D L L要链接到使用任何供应商的 工具创建的可执行模块,那么必须做一些额外的工作。 前面讲过当进行C和C + +混合编程时使用e x t e r n“C”修改符的问题。也讲过C + +类的问题 以及为什么因为名字改变的缘故你必须使用同一个编译器供应商的工具的问题。当你直接将 C 语言编程用于多个工具供应商时将会出现另一个问题。这个问题是,即使你根本不使用 C + +, M i c r o s o f t的C编译器也会损害C函数。当你的函数使用 _ _ s t d c a l l ( W I N A P I )调用时会出现这 种问题。这种调用规则是最流行的一种类型。当使用 _ _ s t d c a l l将C函数输出时,M i c r o s o f t的编 译器就会改变函数的名字,设置一个前导下划线,再加上一个 @符号的前缀,后随一个数字, 表示作为参数传递给函数的字节数。例如,下面的函数是作为 D L L的输出节中的_ M y F u n c @ 8 输出的: 如果用另一个供应商的工具创建了一个可执行模块,它将设法链接到一个名叫 M y F u n c的 函数,该函数在M i c r o s o f t编译器已有的D L L中并不存在,因此链接将失败。 若要使用与其他编译器供应商的工具链接的 M i c r o s o f t的工具创建一个可执行模块,必须告 诉M i c r o s o f t的编译器输出没有经过改变的函数名。可以用两种方法来进行这项操作。第一种方 法是为编程项目建立一个 . d e f文件,并在该 . d e f文件中加上类似下面的E X P O RT S节: 当M i c r o s o f t的链接程序分析这个 . d e f文件时,它发现_ M y F u n c @ 8和M y F u n c均被输出。由 于这两个函数名是互相匹配的(除了截断的尾部外),因此链接程序使用M y F u n c的. d e f文件名 来输出该函数,而根本不使用_ M y F u n c @ 8的名字来输出函数。 现在你可能认为,如果使用M i c r o s o f t的工具创建一个可执行模块,并且设法将它链接到包 含未截断名字的D L L,那么链接程序的运行将会失败,因为它将试图链接到称为 _ M y F u n c @ 8 的函数。当然,你会高兴地了解到M i c r o s o f t的链接程序进行了正确的操作,将可执行模块链接 到名字为M y F u n c的函数。 第19章 DLL 基础计计471 下载 如果想避免使用 . d e f文件,可以使用第二种方法输出未截断的函数版本。在 D L L的源代码 模块中,可以添加下面这行代码: 这行代码使得编译器发出一个链接程序指令,告诉链接程序,一个名叫 M y F u n c的函数将 被输出,其进入点与称为_ M y F u n c @ 8的函数的进入点相同。第二种方法没有第一种方法容易, 因为你必须自己截断函数名,以便创建该代码行。另外,当使用第二种方法时, D L L实际上 输出用于标识单个函数的两个符号,即 M y F u n c和_ M y F u n c @ 8,而第一种方法只输出符号 M y F u n c。第二种方法并没有给你带来更多的好处,它只是使你可以避免使用 . d e f的文件而 已。 19.4 创建可执行模块 下面的代码段显示了一个可执行的源代码文件,它输入了 D L L的输出符号,并且在代码中 引用了这些符号。 当创建可执行源代码文件时,必须加上 D L L的头文件。如果没有头文件,输入的符号将不 会被定义,而且编译器将会发出许多警告和错误消息。 可执行源代码文件不应该定义D L L的头文件前面的M Y L I B A P I。当上面显示的这个可执行 源代码文件被编译时,M Y L I B A P I由M y L i b . h头文件使用_ _ d e c l s p e c ( d l l i m p o r t )进行定义。当编 译器看到修改变量、函数或C + +类的_ _ d e c l s p e c ( d l l i m p o r t )时,它知道这个符号是从某个D L L模 块输入的。它不知道是从哪个D L L模块输入的,并且它也不关心这个问题。编译器只想确保你 用正确的方法访问这些输入的符号。现在你在源代码中可以引用输入的符号,一切都将能够正 常工作。 472计计第四部分 动态链接库 下载 接着,链接程序必须将所有 . o b j模块组合起来,创建产生的可执行模块。该链接程序必须 确定哪些D L L包含代码引用的所有输入符号的 D L L。因此你必须将D L L的. l i b文件传递给链接 程序。如前所述, . l i b文件只包含D L L模块输出的符号列表。链接程序只想知道是否存在引用 的符号和哪个D L L模块包含该符号。如果连接程序转换了所有外部符号的引用,那么可执行模 块就因此而产生了。 输入的真正含义是什么 上一节介绍了修改符 - - d e c l s p e c ( d l l i m p o r t )。当输入一个符号时,不必使用关键字 - - d e c l s p e c ( d l l i m p o r t ),只要使用标准的C关键字 e x t e r n即可。但是,如果编译器预先知道你引用 的符号将从一个D L L的. l i b文件输入,那么编译器就能够生成运行效率稍高的代码。因此建议 你尽量将 - - d e c l s p e c ( d l l i m p o r t )关键字用于输入函数和数据符号。当你调用标准 Wi n d o w s函数中 的任何一个时,M i c r o s o f t将为你进行这项设置。 当链接程序进行输入符号的转换时,它就将一个称为输入节的特殊的节嵌入产生的可执行 模块。输入节列出了该模块需要的D L L模块以及由每个D L L模块引用的符号。 使用Visual Studio的D u m p B i n . e x e实用程序(带有- i m p o r t s开关),能够看到模块的输入节的 样子。下面是 C a l c . e x e文件的输入节的一个代码段(同样,我删除了 D U M P B I N的某些输出, 这样它就不会占用太多的篇幅)。 第19章 DLL 基础计计473 下载 如你所见,这一节为 C a l c . e x e需要的每个D L L设置了一个项目,这些 D L L是S h e l l 3 2 . d l l、 M S V C R t . d l l、A d v A P I 3 2 . d l l、K e r n e l 3 2 . d l l、G D I 3 2 . d l l和U s e r 3 2 . d l l。在每个D L L的模块名下面, 有一个C a l c . e x e从该特定模块输入的符号列表。例如, C a l c模块调用包含在K e r n e l 3 2 . d l l中的下 列函数:l s t r c p y W、L o c a l A l l o c、G e t C o m m a n d L i n e W和G e t P r o f i l e I n t W等。 紧靠符号名左边的数字是符号的提示( h i n t)值,它与讨论无关。每个符号行最左边的数 字用于指明该符号在进程的地址空间中所在的内存地址。该内存地址只有在可执行模块相链接 时才出现。在D u m p B i n的输出的结尾处,可以看到更多的链接信息。 19.5 运行可执行模块 当一个可执行文件被启动时,操作系统加载程序将为该进程创建虚拟地址空间。然后,加 载程序将可执行模块映射到进程的地址空间中。加载程序查看可执行模块的输入节,并设法找 出任何需要的D L L,并将它们映射到进程的地址空间中。 474计计第四部分 动态链接库 下载 由于该输入节只包含一个D L L名而没有它的路径名。因此加载程序必须搜索用户的磁盘驱 动器,找出D L L。下面是加载程序的搜索顺序: 1) 包含可执行映像文件的目录。 2) 进程的当前目录。 3) Wi n d o w s系统目录。 4) Wi n d o w s目录。 5) PAT H环境变量中列出的各个目录。 应该知道其他的东西也会影响加载程序对一个 D L L的搜索(详细说明参见第 2 0章)。当 D L L模块映射到进程的地址空间中时,加载程序要检查每个 D L L的输入节。如果存在输入节 (通常它确实是存在的),那么加载程序便继续将其他必要的 D L L模块映射到进程的地址空间 中。加载程序将保持对 D L L模块的跟踪,使模块的加载和映射只进行一次(尽管多个模块需 要该模块)。 如果加载程序无法找到需要的 D L L模块,用户会看到图1 9 - 2、图1 9 - 3所示的消息框中的一 个:如果是Windows 2000,那么将出现图1 9 - 2所示的消息框,如果是 Windows 98,则出现图 1 9 - 3所示的消息框。 当所有的D L L模块都找到并且映射到进程的地址空间中之后,加载程序就会确定对输入的 符号的全部引用。为此,它要再次查看每个模块的输入节。对于列出的每个符号,加载程序都 要查看指定的D L L的输出节,以确定该符号是否存在。如果该符号不存在(这种情况很少), 那么加载程序就显示图 1 9 - 4、图1 9 - 5所示的消息框之一:如果是 Windows 2000,那么出现图 1 9 - 4所示的消息框,如果是Windows 98,则出现图1 9 - 5所示的消息框。 如果Windows 2000版本的消息框指明漏掉的是哪个函数,而不是显示用户难以识别的错误 代码0 x C 0 0 0 0 0 7 B,那么这将是非常好的。也许下一个Wi n d o w s版本能够做到这一点。 如果这个符号不存在,那么加载程序将要检索该符号的 RVA,并添加D L L模块被加载到的 虚拟地址空间(符号在进程的地址空间中的位置)。然后它将该虚拟地址保存在可执行模块的 输入节中。这时,当代码引用一个输入符号时,它将查看调用模块的输入节,并且捕获输入符 号的地址,这样它就能够成功地访问输入变量、函数或 C + +类的成员函数。好了,动态链接完 成,进程的主线程开始执行,应用程序终于也开始运行了! 第19章 DLL 基础计计475 下载 图19-2 Windows 2000下加载程序 搜索D L L时出现的消息框 图19-3 Windows 98下加载程序 搜索D L L时的消息框 图19-4 Windows 2000下加载程序查看D L L 的输出节时出现的消息框 图19-5 Windows 98下加载程序 查看D L L时出现的消息框 当然,这需要加载程序花费相当多的时间来加载这些 D L L模块,并用所有使用输入符号的 正确地址来调整每个模块的输入节。由于所有这些工作都是在进程初始化的时候进行的,因此 应用程序运行期的性能不会降低。不过,对于许多应用程序来说,初始化的速度太慢是不行的。 为了缩短应用程序的加载时间,应该调整你的可执行模块和 D L L模块的位置并且将它们连接起 来。真可惜很少有开发人员知道如何进行这项操作,因为这些技术是非常重要的。如果每个公 司都能够使用这些技术,系统将能运行的更好。实际上,我认为操作系统销售时应该配有一个 能够自动执行这些操作的实用程序。下一章将要介绍对模块调整位置和进行连接的方法。 476计计第四部分 动态链接库 下载
/
本文档为【DLL基础】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。
热门搜索

历史搜索

    清空历史搜索