Forever
Do not lose heart,you will be successful sooner or later。

Win32动态连接库基址重置技术

摘要

本文讨论了Microsoft Windows NT和Microsoft Windows 95动态链接库(dynamic-link library,DLL)基址重置(rebasing)的若干问题(在本文中,“Rebasing,基址重置”一词指的是在内存空间中改变动态链接库基地址的过程)。随本文章提供了一个应用程序实例和一个动态链接库套件,读者可以从中进行一些比较。

引言

开发人员常问到的一个问题是,“当操作系统对动态链接库(DLL)进行基址重置时,会出现什么效果?基址重置会带来什么损失?有什么办法可以避免这些损失?是否有办法修改代码以减少基址重置所带来的修补(fixup)工作?”

由于这些问题很有代表性,所以本文将集中讨论一下加载动态链接库(DLL)的有关问题,希望为使用动态链接库(DLL)的读者提供一定的参考。

事实上,本文给出的结果也许并不太新奇,也不太具有“革命性”,比方说:最好选择较大的动态链接库(DLL),而不宜选择一大堆小的动态链接库;确定系统不需要花费很长时间搜索动态链接库;如果操作系统有可能对动态链接库进行基址重置,应尽量避免由此而带来的修补工作(或者也可以使用另一种办法,尽量选定用户基地址,防止基址重置现象的发生)。然而,正如一句古老言语所说的那样,“过程即目标(The journey is the goal. )”。换句话说,作者在写作本文时,发现了大量的有关动态链接库和内存管理的小问题、小技巧,作者认为,这些问题和技巧是值得与大家分享的。也许,这篇文章更合适的名字应该是“动态链接库点滴”。

在本文中,将给出一个作者自己写的样本测试应用程序,此程序用于测试动态链接库的加载时间。本文还将提供一些测试用的动态链接库。

应用程序

用于测试动态链接库(DLL)加载时间的测试集结构非常简单,主要包括:使用Microsoft基础类库(Foundation Class libraries,MFC)编写的PAGETEST应用程序,此程序包含两个线程。第一个线程(主应用程序)建立并拥有默件(mutex)对象。此线程对当前时间进行采样,然后调用LoadLibrary函数,以显式方式加载作者提供的库(本文下一节将讨论这些库)。与此同时,第二个线程等待默件(mutex)对象发送信号。

所有的库都由动态链接库的入口程序组成。在动态链接库(DLL)入口程序的PROCESS_ATTACH发送点,默件(mutex)对象发送信号。此时,辅助的应用程序线程被唤醒,在调用LoadLibrary函数之前,计算采样时间和当前时间之间的差值。这个时间差值大致上就是DLL装入内存所需的时间。MFC应用程序有一个反复加载和卸载DLL的选项(50次),通过反复加载,可以计算出有意义的平均加载时间。

文章不需要讨论应用程序的特性,因为本文中使用的程序都是简单的MFC应用程序,所有的相关代码都存在于显示类中。显示类由CEasyOutputView函数引出,用于提供简单的显示结果。(欲知进一步的细节,请参阅“Windows NT Security in Theory and Practice”,Windows NT安全性理论与实践)

读者应该注意,这种经验式的测试方法有很多缺陷,因此测试结果有可能与实际结果相背离,这主要因为:

作者假设时间抽样机制是有效而可靠的,抽样力度足够细。(作者使用了系统性能计数器)

作者假设线程切换机制是连续有效的,对唤醒辅助线程所需时间没有非常不良的影响。

测试结果在很大程度上,依赖于底层硬件(也就是说,下列一些因素也会对测试结果产生影响,如运行测试程序的计算机速度、所使用的处理器数目、硬盘控制器的速度、等等)。

测试结果使用特定版本的软件进行抽样。(操作系统版本、C运行时间库版本、等等。)

通常情况下,大多数动态链接库都是隐式加载的,而不是显示加载的。这就要求作出如下假设:隐式加载DLL与显示加载DLL所用的时间一样长,相应的其他参数也相同。

更糟糕的是,作者所得到的数据有时相差很大。因此,读者应该有保留地接受这些结果。在测试结果中,更有参考价值的不是加载所需的绝对时间,而是相对时间。换句话说,重要的是调整某一参数对加载行为的影响,以及不同策略之间的加载结果比较。

如果读者希望在自己的计算机上重建测试结果,可以按照下一节将讨论的动态链接库定位指令,运行PTAPP.EXE,并在Multiple Test菜单中,选择Run All Tests选项。

动态链接库

下面是动态链接库与加载时间有关的一些性质:

动态链接库的大小。

需要重定位的条目数量。

动态链接库是否初始化C启动代码。

动态链接库是否输出符号。

动态链接库是否与其他库隐式链接。

操作系统需要多长时间,才能重定位可执行的动态链接库。

除了上述因素以外,还有一些独立于动态链接库的因素,也决定着动态链接库加载速度的快慢。例如,底层操作系统、当前计算机上总的工作负荷、应用程序的工作集、动态链接库是否需要基址重置、等等。

为简单起见,作者给出18个小的动态链接库(其中有些并不太小),这些DLL几乎代表了以下特性的全部组合:

动态链接库大小(可以大也可以小)

如果是大的动态链接库,是否需要修正加载时间

C运行时间支持(没有支持、隐式链接的、显示链接的)

动态链接库输出的符号(yes或no)

作者在同一台机器上,在Windows NT版本3.51和Windows 95环境下,将全部18个动态链接库通过各自首选的虚拟地址区,加载到各自首选的基地址中。每个测试程序也首先将动态链接库定位到当前目录中,然后,沿着路径下行搜索,测试将动态链接库定位到当前搜索到的目录中,操作系统所需花费的时间。如前所述,每次测试运行50此,得到统计平均值。

作者的第一个发现是,在Windows NT环境下,任何给定的动态链接库的初始加载时间,平均是在以后加载同一个动态链接库所需时间的三倍。这种现象是由Windows NT内存管理设计的副作用造成的:一旦初始加载了某一动态链接库,卸载后,属于此动态链接库映象的页仍然存在内存中;这些页被放在等待列表中(等待列表是系统为丢弃的页维护的一个列表,如果原来的应用程序又需要此列表中的页,或新的应用程序需要访问列表中的页,这些页可以被重新起用。)读者如果想了解有关等待列表的进一步信息,请参阅Helen Custer的Inside Windows NT(Windows NT内幕)一书的194页到196页。

从等待列表页中重载动态链接库的页,比从磁盘上重载有关页效率高的多。随着时间的推移,有关的页会从等待列表中移到自由列表中。如果在加载初始动态链接库和加载后续动态链接库之间,有很多内存分配和内存访问操作,两种情况下加载动态链接库所花的时间将不会有太大差别。为模拟上述行为(一定通过多次测试,取得加载动态链接库时间的统计平均值),作者加入了一个选项,此选项允许应用程序尽可能多地占用内存,使等待列表尽快用完。伴随着Windows NT Resource Kit,微软将提供一个小小的实用程序,此程序可以强制将一页从等待列表中取出,此程序是CLEARMEM.EXE。

实用上述方法确实十分有效,但遗憾的是,作者释放了所占用的内存之后,加载时间一下变成平均时间的20倍----初始加载时间的7倍。

这种现象使作者陷入了一个两难境地:一方面,作者希望得到在正常工作条件下,加载动态链接库的可靠的统计平均时间;另一方面,作者所能得到的唯一的可靠的、一致的时间不是在正常工作条件下得到的。于是,作者使用了如下的冒险策略(但却是合理的方法),解决了这一两难问题:假设初始加载时间和随后的平均加载时间之间的关系是固定的,将测试结果建立在比较加载动态链接库的平均时间的基础上。使用这种办法,在正常工作情况下,所得的比较结果仍然是有意义的。

读者如果希望重建动态链接库、加入自己的动态链接库变量、或者只是想了解一下作者如何建立了18个动态链接库,可以继续阅读(或浏览一下)下一小节。否则,可以跳过这小节,直接阅读“理论”一节。

建立动态链接库

使用Visual C++ 版本2.2产生makefile,可以建立动态链接库。读者可以在PAGETEST子目录下附加的样本代码中,找到相应的工程文件。全部18个动态链接库都是由相同的工程文件产生的;读者可以建立自己的动态链接库原始版本(未经调试的版本),然后使用以下的命名规则,将产生的可执行代码复制到新的位置。

PTAPP样本应用程序要求在动态链接库的名字中,包含DLL内容信息。动态链接库名字的每一个字母代表一种属性,对应的命名规则如下:

第一个字母代表动态链接库是小库(不包含任何数据)还是大库(包含100,000个静态数据元素)。此位置如果是S,表示动态链接库是小库;此位置如果是L,表示动态链接库是大库;F表示所有的100,000个数据元素都初始化为可重定位的自串,当动态链接库进行基址重置时,字串的地址必须在加载时进行调整。注意:有100,000个可重定位的字串并不表示一定要进行100,000次重定位。对于Visual C++版本2.x来说,还存在一个问题,Visual C++的连接器将可移植的可执行文件中的可重定位项数目限制在64K之内。因此,如果在某个名字以F开始的动态链接库中,运行.EXE头实用程序,例如YAHU,用户会发现只有34K可重定位的地址。这一问题将在新版本的Visual C++中得到解决。

第二个字母表示动态链接库是否支持CRT代码(C run-time)。如果此位为N,表示DLL有一个自定义入口点,不调用CRT初始化代码(C run-time);如果此位为C,表示动态链接库的入口地址是DllMain,可以隐式地初始化CRT(C run-time);如果此位为D,表示动态链接库有一个自定义的入口地址,调用_CRT_INIT,动态地初始化CRT(C run-time)库。

最后,第三个字母如果是N,表示动态链接库不输出任何符号;如果是E表示输出一个函数。其余的字母现在还未指定含义。例如,SCNNNNNN.DLL是一个小的动态链接库,隐式调用CRT(C run-time)初始化代码,但不输出任何符号。FNENNNNN.DLL是一个大的动态链接库,有许多重定位地址,不调用CRT(C run-time)初始化代码,但输出一个符号。

为了不将任何不需要的副作用引入比较过程中去,作者将动态链接库编写的尽可能的小。作者提供的最小的动态链接库,含有一些自定义的动态链接库入口点,不初始化CRT(C run-time)支持代码。

在作者提供的任何动态链接库中,都不支持MFC。这主要因为MFC动态链接库隐式链接到其他动态链接库上,并执行作者不希望引入到测试程序中的自定义初始化过程。所有其他动态链接库的变量都是按照如下方式,通过对工程文件略作修改而得到的:

为了将小的动态链接库制成大的动态链接库,需要将MANYPAGES符号加入到预处理器伪指令(preprocessor directive)中。可以通过在预处理器定义中,加入MANYPAGES和FIXUPS,产生大的动态链接库,这种动态链接库中有很多修复地址。如果需要建立一个使用自定义入口指针,调用CRT(C run-time)初始化代码的动态链接库,可以加入DYNACRT符号;如果想建立一个具有隐式CRT初始化代码的动态链接库,可以加入STANDARDENTRY符号,并在Settings/Link/Entry文本框中,将动态链接库的入口指针指向DllMain。最后,如果需要动态链接库输出一个符号,可以加入预处理器伪指令HASSYMBOLS。

作者还定义了一个名为HUGEBINARY的符号,用于产生真正的大动态链接库(即具有大约15,000个数据页的动态链接库,将此符号与FIXUPS符号连用,将产生大约15,000个重定位地址。)动态链接库的大小在40到61MB之间,具体大小取决于用户是否定义了FIXUPS。在测试实例中,作者没有在动态链接库测试集中加入二进制代码。

无论用户选择哪一种选项来建立动态链接库,最终产生的可执行程序都会存储在PAGETEST工程的WINREL子目录下的PAGETEST.DLL文件中。建立动态链接库后,用户可以将这个DLL拷贝到不同的位置,并根据上述的命名规则,将此动态链接库重新命名。

为了察看搜索动态链接库二进制字对加载时间的影响,可以在计算机上保存每个动态链接库的两份拷贝,一个与PTAPP.EXE(测试应用程序)存在同一目录下,另一个存在搜索路径最末端的子目录中(作者计算机上的C:\DOS目录)。使用与可执行文件在相同目录下的动态链接库,运行测试程序之后,将所有动态链接库重命名,强制操作系统在另一目录下查找动态链接库。

在建立动态链接库时,作者为了测试方便,改变了一些选项,测试之后,无法重新建立原来的结构。因此,读者在重建DLL时,一定要确定自己的重建方式和作者的方式相同,以下是作者采用的工程选项:

编译器

/nologo /MT /W3 /GX /YX /O2 /D /FAcs /Fa "WinRel/" FR "WinRel/" /Fp "WinRel/pagetest.pch" /Fo "WinRel" /c

预处理器

确切的预处理器选项依赖于所建立的库的类型,库类型在前文中已经讨论过。

连接器

kernel32 advapi msvcrt /nologo /subsystem:windows /DLL /incremental:no /PDB: "WinRel/pagetest.pdb" /MACHINE:I386

注意事项:PE文件格式包含时间标记。也就是说,如果用户两次建立相同的动态链接库,产生的结果二进制映象是不同的。作者提供的Byte-Byte-Byte文件比较实用程序,可以分两组报告六个不同位元组的信息,每个组包括三个连续的位元组。每一对独立开发的、完全相同的动态链接库对应一个位元组。

理论

要加载动态链接库操作系统必须完成以下各步:

在磁盘上定位动态链接库可执行文件。

仔细查看已经加载进应用程序地址空间中的动态链接库列表,判断动态链接库是否已经加载了。

为动态链接库分配驻留内存,并将动态链接库二进制文件映射到内存中(在Windows NT中,映射跨越了段对象)。

为使动态链接库正常运行,执行一系列必要的处理(例如,解析动态链接库中做过的修正,等等)。

不同的参数决定了动态链接库的加载时间不同。以下给出了需要考虑的各种因素,事实上可能还有一些因素也会影响到动态链接库的加载时间:

底层软硬件:计算机本身的速度以及运行哪一种操作系统。

当前系统和应用程序的状态:在虚拟内存中系统的紧密情况,以及动态链接库是否可以被加载在首选基地址。

动态链接库本身:动态链接库本身有多大,动态链接库中有多少位置需要修正(结合两者综合考虑),此动态链接库是否隐式链接到另一个同样需要加载的动态链接库中。

由上述分析可知,对某一动态链接库进行基址重置绝不是影响动态链接库加载时间的唯一决定因素。在本文中,作者使用了很多数据,说明了动态链接库加载时间的变化范围以及应用程序可能对加载时间的影响程度。

读者应该注意到,对某一动态链接库进行基址重置,不仅可能造成加载时间的大幅度增加,还需要增加页文件(pagefile)的开销。加载动态链接库的第一步,需要创建区段对象(section object),区段对象是由动态链接库可执行文件支持的、内存中的一个相邻的区段。一旦动态链接库的某一页被从应用程序工作集中移去了,在下一次访问此页时,操作系统就会从动态链接库可执行文件中重新加载这一页。

当然,当动态链接库进行基址重置时,这一策略不再起作用,这主要是因为包含重定位地址的页与动态链接库可执行文件映射中的相应页不同。因此,一旦在加载可执行文件时,操作系统企图修正地址,就会拷贝相应的页(由于区段是通过COPY_ON_WRITE标志打开的)。在拷贝中进行的所有的修改,操作系统都会记住,从现在开始,页将在系统页文件中换入换出,而不再在可执行映象中进行页交换。

采用这种机制,潜在的性能命中有两种:首先,每个包含需要重定位地址的页都占用一页系统页文件(其结果是,减少了所有应用程序可用的虚拟内存数);另外,由于操作系统执行动态链接库页中的第一次修正操作,新的页必须从页文件中分配,并将整个页拷贝下来。

尽管扫描动态链接库重定位区段以及进行内存修正的算法都有很高的效率,执行修正操作还是会增加动态链接库的加载时间。(跨段操作的复杂性是需要修复地址数量的线性函数。)

地址修正

关于动态链接库基址重置的一类经常问到的问题是,“地址修正究竟是什么含义?程序员是否有办法调整代码,以减少可执行程序中的地址修正?”对于这两个问题的回答是,这一切在很大程度上依赖于可执行程序建立在哪一种平台上。在本文中,我们将平台限制在Intel 386,、486、和奔腾(Pentium)处理器上,讨论相应的可执行程序。(注意:为其他平台建立的可执行程序,相应的地址修复概念与本文讨论的概念不同。)

在386、486、或奔腾处理器上,两种情况可能导致某地址被标识为“可重定位”的状态:一是静态对象(static objects),另一种情况是绝对跳转(absolute jumps)。

首先,如果动态链接库引用了静态对象,就会使用对象的绝对地址(假设动态链接库被加载到首选地址中)。例如,在如下的代码段中:

LPSTR lpName="Name";

动态链接库的载入程序就会将“Name”字串分配到动态链接库数据段中,并将此字串的起始地址填写到lpName变量对应的位置中去。如果由于DLL不能被加载到基地址而使得“Name”字串必须重定位,lpName必须相应地进行调整。注意,在这种情况下,代码段中每个引用lpName的变量也必须作相应的地址修正。

可以重定位的对象包括文字字串(例如,在上例中的“Name”字串)、任何一种类型的全局或静态数据、包含静态分配的C++对象。应该注意到,特别是在C++中,可能存在着许多从一个静态对象到另一个静态对象的交叉索引。未初始化的数据不需要在重定位过程中修正地址,但指向未初始化静态数据的索引需要进行地址修正。

在i386可执行代码中,另一类可以进行重定位的项是绝对跳转和函数调用,包括系统函数调用。注意到程序开发人员很难通过修改程序代码来避免地址重定位,唯一可以采用的办法就是缩减静态分配数据的数目。要缩减静态分配数据,一种办法就是尽量避免使用名字进行资源索引,而应该通过坐标进行资源索引(因为,程序员在代码中显式使用的每一个名字都会自动变成一个可重定位的项)。

尽管如此,作者并不建议程序开发人员带着减少加载时间这一特殊目的开发动态链接库代码,除非存在以下两种情况:(1)静态分配对象的数据可以大幅度减少;(2)程序员依据此种方式进行编程时,不会影响其他编程中需要考虑到的因素。

除此之外,程序开发人员还可以通过简单的优化方法,减少加载时间。例如,可以将所有可以重新定位的数据集中到几页中。很显然,如果动态链接库需要进行基址重置,每页含有一个重定位项的两页,都需要有页文件来支持。如果所有的重定位项都出现在同一页中,只有一页需要页文件来支持,因此只有一页会受到影响。在必要的情况下,读者可以使用pragma (data_seg,数据段)伪指令,确保尽可能多的可重定位项被分配到尽可能少的页中去。

工具

作者在研究动态链接库加载时间时,最有趣的收获是,作者进一步了解了操作系统的内部工作机制,以及可执行文件的格式。以下,本文将介绍一些工具,这些工具对分析动态链接库的映象机制和运行时的状态非常有帮助:

可执行头实用程序(例如,在文章“YAHU, or Yet Another Header Utility”中论述的YAHU实用程序),通过该程序,程序员几乎可以得到动态链接库在编译连接后和加载之前的任何信息----例如,共有多少个段、每个段有多大、以及在所有段中多少可以重定位、等等。

过程漫步者程序(process walker),例如,Win32 SDK中提供的PWALK程序。此程序可以告诉开发人员,动态链接库在过程地址空间的何处加载、以及动态链接库的独立段放在何处。

过程显示程序(process viewer),例如Windows NT Resource Kit(Windows NT资源箱中提供的PVIEW)。此程序可以告诉程序员,在任意给定时刻,动态链接库占用了内存中的多少页。

性能监控程序(performance monitor),此程序随Windows NT提供,在缺省情况下,安装在Administrative Tools组中。此程序是一个非常好的性能监控工具,可以用来监控加载动态链接库对系统的影响,例如使用页文件的情况。

下面,本文将向大家介绍一下如何使用这些工具,使大家能够进一步了解动态链接库内部的工作机制。在动态链接库SNNNNNNN.DLL上运行YAHU,读者可以得到关于动态链接库中五个段的如下信息:

.TEXT段,段大小为0x28字节,位于文件头后的0x400字节处。此段中包括全部的动态链接库代码。该段将在动态链接库映象的起始地址之后的第一页处(0x1000)加载。

.DATA段,段大小为9,位于文件头后的0x600字节处。此段中包含全部的初始数据。该段将在动态链接库映象的起始地址之后的第二页处(0x2000)加载。

.IDATA段,段大小为0x6c,位于文件头后的0x800字节处。在此段中,包含了很多重要数据,其中有可执行文件需要连接的动态链接库的名字、动态连接库中调用的函数名等。该段将在动态链接库映象的起始地址之后的第三页处(0x3000)加载。

.EDATA段,段大小为0x35,位于文件头后的0xA00字节处。在此段中,包含了动态链接库的输出信息。该段将在动态链接库映象的起始地址之后的第四页处(0x4000)加载。

.RELOC段,段大小为0x32,位于文件头后的0xC00字节处。在此段中,包含了整个动态链接库的全部重定位信息。该段将在动态链接库映象的起始地址之后的第五页处(0x5000)加载。

在其他动态链接库中,读者可能会发现更多的段----例如,.BSS段,其中包括未初始化的数据。

注意,文件中每个段各自的偏移量可以帮助读者阅读二进制数据。例如,在Visual C++中,以二进制模式打开动态链接库,滚动光标,找到偏移量0xc00。读者就可以看到在六个数据字节之后,跟着八个字节的头信息。重定位纪录的确切格式详见Microsoft Systems Journal(微软公司系统杂志),该杂志Development Library(开发文库)中的“Peering Inside the PE: A Tour of the Win32 Executable File Format”一文(作者Pietrek,作于1994年)详细讨论了重定位纪录的格式。注意,.RELOC段中,包含了程序员所需的、在内存中何处执行重定位操作的全部信息。

因此,SNNNNNNN.DLL动态链接库的映象由六页组成:PE头以及前面列出的五个段,其中每一段刚好由一页组成。现在,在PWALK控制下运行PTAPP.EXE,并选择一个没有输出、且在Select DLL菜单中没有CRT支持的、小的动态链接库。读者将会看到一个消息框,此消息框说明SNNNNNNN.DLL被定位在用户硬盘的某一位置上。在Run Single Tests菜单上选择Load DLL选项。此时读者将看到一个消息框,说明动态链接库被加载到某一地址。然后,回到PWALK,重新执行上述过程,使用滚动条,滚动到PTAPP给出的加载地址处(如果动态链接库被加载到首选基地址处,此地制止应该是0x10000000)。此时,读者将会看到动态链接库的六个页,这六个页严格地以可执行文件头中定义的次序排列。注意,属于.RELOC段的页被列为.EDATA段的第二页。

然后,运行PVIEW.EXE,并从过程列表组合框中,选择PTAPP.EXE过程。在User Address Space组框中,从组合框中选择SNNNNNNN.DLL。用户将会看到以访问类型排序的所有的动态链接库的页:所列的DLL共占24K地址空间(六页)。其中12K(三页)被列为只读的动态链接库头页(read-only--the DLL header page),起始地址为0x10000000,.EDATA段中的两页起始地址为0x10004000。.IDATA段中的一页被标记为可读可写状态(read/write)。这一段必须是可读可写状态,因为输入指令有可能指向需要基址重置的动态数据库,因此,这一段中的入口可能需要更新。.TEXT区段中的一页被标志为可执行状态,.DATA区段的页被标记为保护状态,不可以拷贝和写入。

如果读者对大的动态链接库运行同一过程,将会发现.DATA区段会变大(如事先预测的一样),而且此段内所有的可重定位数据都将会被标记为不可拷贝和写入的保护状态。如前所述,不可拷贝和写入的保护状态,可以确保重定位操作不在动态链接库映象的物理页上进行,而在页文件的一份拷贝上进行。

数据

作者前面曾经警告过,在测量动态链接库加载时间的时候,测出的结果可能有很大的差异。作者多次运行测试集之后发现,尽管可以总结出测试结果的某些模式及一般关系,但计算机总负载仍然可能在很大程度上影响着测试的结果,测试结果的差异可能高达百分之二十。

本文首先介绍一下作者是如何得到这些数据的,然后将详细解释测试结果。读者请参见附录A中的测试流程,本文正是采用附录A中的流程,取得评价信息的。

为了取得测试数据,可以运行测试应用程序PTAPP,并从Run Multiple Tests菜单上选择Run All Tests选项。完成了这些操作之后,脚本程序将分别调用全部的18个动态链接库,并各运行50次。(如果在Select DLL菜单上选择某一特定的动态链接库,选择Finish定位某一动态链接库,进行测试初始化,然后从Run Multiple Tests菜单中选择Run Without Hogging选项,就可以测试某一单独的脚本。也可以通过单一模式(one-shot fashion),选中一个动态链接库进行测试,具体方法是,在Run Single Tests菜单中,选择Load DLL菜单选项。)注意:测试过程可能需要几分钟的时间。

每次测试的结果,都将显示在应用程序主窗口中。第一行显示系统性能计数器的分析结果,(可以用来计算绝对时间),在最后的测试完成之后,读者将会看到一个含有36个数据的表格。这些数据是18个动态链接库中每一个的平均加载时间(在性能计数器中),加载位置(包括首选地址和基址重置地址)。如前所述,计数器值以及性能计数分析结果可以用来计算绝对加载时间,具体计算公式如下:

加载时间(以秒记)=计数器值/性能计数分析值

loading times in seconds = number of ticks/ performance counter resolution

在每一个结果之后,测试程序在括号中给出相对加载时间,相对加载时间是基于运行测试程序所得到的最小结果计算出来的。

为了得到附录A中所示的四组36个数据的统计表,读者需要运行四次测试程序:两次在Windows NT环境下(一次将动态链接库定位在与PTAPP.EXE相同的目录中,另一次将动态链接库定位在搜索树的较深目录中),两次在Windows 95环境下(与前一情况相同)。

正如本文前面所述,所得的测试结果都是在我们意料之中的。以下是我们得出的重要结论:

在Windows NT环境下得出的测试结果与在Windows 95环境下得出的测试结果差别不大,只是Windows 95似乎在加载小动态链接库时比Windows NT慢一些,而在加载大的动态链接库时比Windows NT快。

在所有其它因素都相同的情况下,动态链接库的大小对测试结果不会有什么影响,也就是说,加载小的动态链接库所花的时间和加载大的动态链接库所花的时间完全相同。因此,在需要考虑动态链接库加载时间的情况下,如果可能的话,应该尽量避免编写大量的小动态链接库,而应代之以少量一些大的动态链接库。注意,这些结果对于相当大范围的动态连接库都有效,作者在大的二进制动态连接库(包括15,000页)上运行测试程序,所得的结果和在小的动态链接库(只包括6页)上进行测试,所得的结果差别不大。

动态链接库的基址重置在Windows NT上会带来大约百分之六百的系统开销,在Windows 95上会带来大约百分之四百的系统开销。注意:这些额外的系统开销表示系统中有大量的地址修复工作(事实上,在本文给出的示例套件中,有大约34,000处地址调整)。对于典型的动态链接库来说,地址修复的平均次数要小的多,例如,在随Visual C++ 2.x提供的MFC30D.DLL动态链接库调试版本中,大约有1700处地址调整,这个数字大约是示例套件中地址修复数目(34,000)的百分之五。

使动态链接库加载速度减缓的最重要因素是动态链接库的位置。LoadLibrary中的文档详细描述了操作系统用来定位动态链接库映象的算法;加载到最先搜索目录(当前目录)的动态链接库的典型加载时间,通常是加载到深层目录所需时间的百分之二十或更少。非常显然,确切的加载时间和路径长度、 底层文件系统的效率、以及需要搜索的文建数和目录数有着密切的关系。

这里以单纯的检索格式给出测试数据。读者可参见附录A,在附录A中,详细给出了数据的统计方式。

建议

想要加快动态链接库的加载时间,主要的解决办法之一是,程序开发人员应确保操作系统在定位动态链接库方面不要花费太多时间。大家可以将动态链接库放在与启动的可执行文件相同的目录中,也可以在环境变量中预先调整好有关设置,使启动的可执行程序可以很快找到所需的动态链接库。做到了这一点,不用改动动态链接库本身,就可以提高加载速度。如果用户需要反复地隐式加载动态链接库,可以先使用SearchPath应用编成界面(API),找到动态链接库的全路径名,在加载动态链接库之前,将精确的路径名提供给操作系统。

如果在动态链接库中有大量的需要进行地址调整的项,还有一种重要方法可以用来加快动态链接库的加载速度,这种方法是,尽量确保操作系统不对动态链接库进行基址重置。读者也许会注意到,对于小的动态链接库,如果存在C运行时间初始化代码(C run-time initialization),相应的动态链接库加载时间就会有所增加。

读者从上述的统计数据中可以看到,不论动态链接库本身的大小如何,加载时间基本上都是固定的。因此,在可能的情况下,开发人员最好能够开发一个大的动态链接库,而不要开发一大堆小的动态链接库。

最后,本文需要再一次强调,由于Windows NT和Windows 95当前所采用的处理废弃页的管理方式(事实上,这些页被存在内存中,并将在以后适当的时候复用),如果相同的可执行程序已经加载在应用程序地址空间中,或在近期曾经被加载过、且仍然保存在准备列表中,加载这个可执行程序会快很多。

posted on 2012-05-04 20:11  sybtj  阅读(833)  评论(0编辑  收藏  举报