关于DLL加载和运行的性能优化总结

本文从三个方面总结了加快DLL加载和运行速度的方法,他们分别是:

  1. 使用Rebase和Bind。
  2. 按序号的方式导入函数。
  3. 使用预读取技术(chromium加载dll时使用了此项)。

首先,讨论一下使用Rebase和Bind,提高DLL加载和运行性能的方法。我们知道,在编译链接DLL的时候,连接器会给DLL一个加载基址,而这个加载基址对于DLL模块中的使用了硬编码地址的代码和数据是至关重要的。因为这些硬编码的地址都是连接器通过DLL加载基址计算出来的。

这样,当我们的DLL被加载到进程空间当中的时候,如果足够幸运的话,DLL正好被加载到连接器所指定的地址,那么那些硬编码的地址就是完全正确的,加载过程不需要其他额外的工作就能让DLL正确运行了。

但是,真实的情况并不是这么理想,因为我们的进程可能不得不加载许许多多DLL,这样就会导致,某些DLL预定的基址可能已经被其他DLL使用。因此,前者就不得不加载到其他地址。由此带来的影响也是显而易见的,硬编码的地址就会出错。为了不让这种错误发生,加载器也就不得不多做一些工作,就是重新矫正这些硬编码。这样,如果DLL里包含的硬编码地址越多,加载的速度就会越慢,从而导致DLL加载性能下降。

另一方面,由于DLL加载基址是不可预见的,所以DLL加载的时候,加载器会根据导入表搜索所有的导入函数,计算并且确定导入函数的正确地址。这个过程也会对DLL加载性能有一定影响。

微软已经考虑到了这个问题的优化方法,在SDK中为我们提供了Rebase和Bind工具。其中Rebase工具,可以合理安排进程中DLL的加载地址,并且修改到PE中,从而避免DLL加载时Rebase操作。这样,一旦我们确认了DLL的加载基址是不会发生改变,那么我们就可以使用Bind工具将导入表进行绑定,这样的好处就是加载器不需要在加载的时候去计算确定导入函数地址,因为这些地址以及被预设了。

需要注意的一点,即使你做了这么多的事情,也不能完全避免加载基址的冲突,例如使用ASLR的DLL,其加载的地址是会发生变化的。所以就不能保证Rebase和Bind的有效性。具体能优化多少性能,在不同的案例中可能结果不同,需要具体实验才能知道。

第二种优化DLL运行性能的方案就是使用序号而非名称的方式导入函数。这一点也就非常容易解释了,如果通过函数名确定导入函数地址,那么加载器就不得不对字符串进行比较,从而确定正确的函数地址,虽然DLL的导出表是按顺序字母排列的,并且查找方式也是二分查找,但是如果函数很多,这依然是个耗时的工作。例如,我机器上的MFC100.dll有14000多个导出函数,如果进程需要按照名称确定自己需要的函数是哪一个,那么工作量还是不小的吧。所以MFC100.dll很明智的使用了导出序号的方法,这样加载器计算导入函数的时候,就能够使用序号来确定函数地址了,也就是简单使用数组搜索,从而得到目标函数地址。

这种方法的优化效果同样也要根据具体情况而定,如果导入函数少,目标DLL导出的函数也非常少,那么这种优化应该是没有什么意义的。相反,如果需要导入函数很多,而且目标DLL也导出了很多函数,那么在想提高程序加载性能的时候,不妨试一试这个方案。

最后一个方法是Pre-Read技术,使用在chromium中的。这种方法的原理是将DLL预先存入系统缓存,从而减少Page Fault来达到提高性能的目的。这种优化主要针对的是进程冷启动加载DLL的情况。进程第一次启动的时候,加载所需的DLL,DLL会被MAP到内存空间,虽然如果查询这片MAP的内存,会发现确实是COMMIT状态。但是,实际上系统并没要保证这些内存在Working Set中。一般情况下,系统只会把你想要用到的内存加载到Working Set中,以节约物理内存。这样,当我们每次用到这个并没有对应的Working Set的虚拟内存的时候,就发生了Page Fault,系统这个时候才会把这些内存加载到Working Set。而Page Fault对性能的影响很是比较大的。所以Pre-Read技术就有了用武之地。

Chromium对Pre-Read实现的非常好,代码的具体位置是http://src.chromium.org/viewvc/chrome/trunk/src/chrome/app/image_pre_reader_win.cc 。代码中,分别针对XP和XP以上的系统使用了不同的方法让系统缓存目标DLL。在XP以上的系统中,代码简单的通过ReadFile将文件读取到内存中,然后释放内存,关闭文件句柄,就可以达到缓存目标DLL的目的。而在XP系统下,做法有些不同,它使用LoadLibraryEx函数,将文件Load到内存空间,然后尝试对每个Page进行读取操作,已达到让数据载入Working Set的目的。

以上是我所知道的三种加载DLL的优化方法,也可能还有更多更好的方法,有兴趣的可以一起讨论下。不过无论什么性能优化方法,都必须建立在实际项目的基础上,并且有科学的依据和论证,切不可只从理论上下结论来优化程序,纸上谈兵,这样很可能适得其反,让性能变得更糟。(例如这个例子里所提到的,看上去不错的优化可能还不如什么都不做:http://blogs.msdn.com/b/oldnewthing/archive/2004/12/17/317157.aspx)。至于说,如何得到程序运行数据用于总结出有效的优化方案,这里强烈推荐一款神器XPerf。实际上,也是因为使用了XPerf,才让我对性能优化产生了浓厚的兴趣!

DebuggingNTInternals

gdi_handle_study —— 查看进程GDI资源情况的工具

gdi_handle_study 是一个用于查看进程中gdi句柄资源的工具。可以用于监控gdi资源是否泄露,已经对gdi资源的使用情况。使用方法非常简单:

1
2
3
4
5
6
<blockquote>usage: gdi_handle_study.exe [-c] [-v [-f <filter>]] [processname|pid]
processname    List GDI handles loaded by process (partial name accepted)
pid                   List GDI handles associated with the specified process id
-c                    Show GDI count information.
-v                    Show GDI handle information.
-f                    Filter the GDI handle type.</blockquote>

在不用任何参数的情况下,工具会显示所有进程的gdi资源使用概况,如图所示:
20130504154700

值得注意的是,GDI Total和GDI All的区别在于,GDI Total统计出来的数量,是通过工具本身枚举可统计GDI资源后得出统计值,而GDI All是通过系统API直接获得的值,有些的情况下,GDI Total的值是小于GDI All的值的。这种情况可能因为某些GDI资源是系统保留的。另外一个要注意的是,如果要显示所有进程的gdi情况,需要有管理员权限运行该工具。

processname和pid参数能让我们指定需要查看的进程名或者进程ID。参数-c能查看更为详细的gdi资源的统计情况。如下图所示:
20130504155533

从上图可以看出,qq这种DirectUI程序,用的Bitmap资源何其的多啊。。。

-v参数是用来查看更为详细的GDI资源信息,其中就包括额资源的句柄,资源的种类以及资源的内核对象地址。如图所示:
20130504160314

最后工具还能利用-f filter,来查看想看到的资源情况,例如上图中,bitmap不是自己想看的资源,但是却占据了大量的视野。这个时候filter就能用上了。如图:
20130504160837

上图就是利用filter,显示的Brush资源的详细情况了。

下载gdi_handle_study

NTInternals

ProcMem —— 进程内存查看工具

ProcMem是一个进程内存的查看工具,他可以显示进程中的内存分配情况,以及内存大概的用途,并且Dump指定的内存模块。工具界面如下图

20130421231145

ProcMem并不是实时监控目标进程的内存情况,而是对内存情况作了一次快照和统计,并且显示出来。所以想看到进程最新的内存状态,可以点击Refresh菜单。

工具上半部分就是显示的目标进程的内存分布情况,以及一些细节信息。这里必须要谈到一点,Windows的标准控件中没有TreeList,对我这个写100个程序99个没有界面的人来说,自绘这个东西差点没要了我的命。

工具的下半部分用来显示TreeList选中项的内存情况,十六进制表示。值得注意的是,这里只会显示选中内存头个PAGE_SIZE大小的内存情况。如果想查看该项内存的全部情况,可以使用Dump功能,把内存Dump下来,然后用WinHex这样的工具查看,这个简单的内存显示区,只是为了提供一个预览功能而已。

值得一提的是,菜单Find,不是用来查找下方十六进制内存显示的内容,而是用来查找TreeList中的项目。例如想找到有关ntdll的内存区域,可以在查找框中输入ntdll,这样就可以定位如图所示的项目了。

这个工具是我花了大半周的业余时间弄的,时间比较仓促,不可避免的可能会有些bug。如果你刚好用上了这个工具,而且发现了bug,不妨通过邮件联系我(邮箱地址见About Me页面)。

下载ProcMem(包括32和64位版本)

DebuggingNTInternals

记VC6中STL的map的一处BUG

今天和同事一起调了一个vc6.0中stl的map的一个bug。

BUG的起因是,我们的项目中使用了stl的map类。而这个map类的对象被用在了不同的DLL模块中,在这样的条件写,BUG就产生了,一个模块内部map对象指针能正常工作,另一个模块内部就出了问题。起初我们就觉得很奇怪,很简单一份代码,怎么会出现访问无效内存的情况,我们还是通常的思路,先在自己身上找问题。调了一会发现,自己的代码确实没有错误。于是我们把目光转向了vc6的stl本身。

跟踪了一下stl的代码,发现错误发生在下面这段代码内部。

20130419140128

在正常的模块中while (_x != _Nil)这个循环只经历了一次,就跳出循环了。而发生错误模块中第二次进入这个循环,也就在这次的循环中,出现了内存访问异常的情况。很明显就要看两次_x != _Nil比较的详细结果。刚开始我被误导了,以为_Nil就是一个为0的常量,把注意力留在_X上后来发现,正确和错误的模块中,_X值一直都是相同的。这才缓过神来,_Nil这个值有问题。

确实,这个不是一个0,更不是一个常量,他是一个静态指针变量。在map对象被创建的时候,生成了一个填充为0的结构体,并且把结构体指针存到了这个变量中。

真相大白了,由于在不同的模块中都是用了stl的map代码,这样map的代码就被编译了两份,同样每个模块中map的_Nil也存放在各自的模块地址范围内。这样就使得_Nil值是不相同的,如果在非创建这个map对象的模块中引用对象指针,并且调用map的函数。如果遇到了_Nil,就会引用此模块自己的_Nil,而不是创建对象模块的_Nil,如果这个模块没有初始化过map对象,那么这个模块的_Nil就是0,即使初始化过,两个模块的_Nil也没可能是同一个值。

新版stl中这个bug必然已经解决,简单来看看vs2010的stl

20130419142503

循环中检查是否为空,用到了函数_Isnil这个,而这个函数查看了_Nodeptr结构中的_Isnil成员变量。判断空放在成员对象内部,这样在多模块之间调用该对象就不会有任何问题了。

话说,vc6的stl确实是bug一堆,很早之前,人们就喜欢用sgi-stl来代替vc6自带的stl了。

DebuggingTips

文件搜索工具everythings工作原理简介

everythings是一个非常强大而且好用的文件搜索工具。他搜索文件的速度非常之快,基本上刚刚输入要查找的文件名,文件已经搜索了出来。这里就简单介绍一下他的工作原理。

有一点已经非常明显,everythings在查找工作开始之前会建立全盘文件索引的数据库,那么这个数据库必然就是搜索文件快速的最重要的原因。而这个数据库的建立的用时似乎非常之短,是普通方法下遍历整个卷所不能及的。

实际上,建立数据库的方法确实比较特殊。简单来说就是遍历目标卷NTFS文件系统的Master File Table(简称MFT)的记录,这也解释了为什么everythings只能工作在是NTFS的文件系统的卷上。MFT可以看成ntfs中文件的索引,MFT中的每条记录都是指向卷中的一个文件。遍历这个索引的速度可要比按照目录递归整个卷的速度要快得多了。不过可惜的是,Windows并没有提供能够直接访问MFT的API,除非你直接解析NTFS磁盘格式(参考这篇文章)。而且就算让你直接遍历的MFT,要监控文件的变化并且写入数据库也是一个不好办的工作。不过可喜的是,微软提供了一种间接遍历MFT记录的方式,并且通过这样的方式可以监控卷上文件的变化,他就是Change Journal(官方文档.aspx))。

Change Journal实际上是Windows 2000的NTFS文件系统就提供了的功能。其目的就是如同名字一样,记录文件改变的日志,方便NTFS文件系统对文件的恢复,这确实是个不错的特性。如何使用这个功能去遍历和监控MFT的记录,我这里就不做详细介绍了。因为有一片更加好的文章已经写的非常的清楚,我的demo也参考了他的很多代码。这篇文章叫做《Keeping an Eye on Your NTFS Drives: the Windows 2000 Change Journal Explained》是1999年9月份的msdn杂志发表的。

我这里假设你已经阅读了这篇文章。我们已经知道了遍历和监控MFT记录的方法,并且通过这个方法获得了每个文件的文件名,file reference和parent reference(可以理解为文件id和其父目录id)。那么这里就可以把这三个元素作为一条记录存储在我们制定的数据库中。

下面简单说下如何利用这个数据库,当需要查找文件的时候:
1.获得用户输入的文件名。
2.通过文件名A可以从数据库中筛选出一些记录,而这些记录中就包括了文件的parent reference。
3.通过parent reference再次查找数据库的file reference字段,获得对应的文件名B,这个文件名B就是文件A的父目录了。重复这一步直到根目录为止。
这样就能找到文件的详细路径了。

我写demo的时候并没有考虑怎么构建这个数据库,直接用了sqlite。不过如果能自己设计一个专门为存储这些数据的小数据库,也应该有更高的效率吧。everythings的数据库就是经过bzip压缩的自定义的文件。

DEMO——构建文件索引数据库:
20130409003609

DEMO——查找文件
20130409003752

P.S 如果采用sqlite作为文件数据库的话,建立索引的插入数据操作一定要利用sqlite的事务机制,否则会在插入数据上花费很多时间。

P.S.2 everythings之所以需要管理员权限才能工作,是因为他需要打开本地卷的句柄,这个就需要管理员权限了。

最后说一点这种方法的不足吧。那就是这种方法对于有多个hardlinks的文件,只能枚举出一个路径。关于hardlink的介绍可以参考这篇文章。由于这个不足,你在查找system32下的文件的时候,往往搜索不到,搜索到的又往往在其他目录,出现尤其多的应该是Winsxs目录了(而关于Winsxs可以看看这篇翻译,翻译质量很不错)。例如我们搜索C盘下的notepad.exe,everythings搜索的结果是这样的:
20130409021029

可以注意到并没有system32下notepad.exe的身影。我们的demo同样也会遇到这样的情况:
20130409021252

下图显示的是同一个notepad.exe的4个存储位置。
20130409021829

虽然everythings有这样的一个不足,但是瑕不掩瑜,他的的确确是一个非常非常优秀的工具!我这里探讨的也仅仅是他的大概原理。实际上这个工具如此优秀,必然是在很多细节上做的非常好才行的。强烈推荐大家使用!

NTInternals

proc_dump_study —— 进程dump工具

proc_dump_study是逆向sysinternals的Procdump的一个工具。在功能上几乎和procdump一模一样。有一点差距就是目前没有支持clr的异常,也就是procdump的-g参数。其他的usage基本上相同,这里也不细说了。想说的一点是,proc_dump_study和procdump一样,功能比较强大,参数也比较多。所以为了方便使用,我把用的比较多的功能总结了一下,写了一个带UI shell程序。这样就方便测试人员或者不想深入理解命令行程序的人员使用。

20130404013936

简单介绍一下使用方法
1.选择要监控或者dump的进程,确定生成dump的文件位置。
2.选择dump类型,包括mini dump,full dump以及effective dump,dump的大小分别为小,大,中等。
3.选择是否监控进程的cpu使用率
4.选择是否监控进程的内存提交数量
5.选择是否监控进程窗口是否挂起
6.选择是否监控进程发生异常
7.选择是否监控进程推出
8.最后点击dump按钮

这样,一旦监控的时候任何监控点达到要求,就会产生dump文件了。如果不需要监控任何进程窗口,程序会立刻dump进程。实际上直接使用proc_dump_study会有更多的功能可以使用,不过这个UI版本应该可以应付大多数的情况了吧。

下载proc_dump_ui

最后放一张测试图
20130402230714

DebuggingNTInternals

find_links_study —— 仿FindLinks的文件硬链接查找工具

在上一篇讨论hardlink的文章中,我在最后提到要写一个查找hardlinks的工具。正如上一篇文章中介绍这种工具的工作原理一样,他的代码比较简单。所以前几天find_links_study就已经写完了,只是一直没空发出来。这个工具的使用和FindLinks一模一样,这里也不多做介绍了。下图是工具的工作效果:

20130316214417

从图中我们可以发现,原来我们在系统中看到的多个notepad文件实际上就是一个文件而已,只不过他利用了hardlink的技术,产生了多个路径罢了。我感觉hardlink还是一个非常实用的技术,具体用在哪大家可以发挥想象力。就我个人看来,在某些情况下,用hardlink来备份文件,倒是一种不错的选择。

下载find_links_study

NTInternals

使用ntfs_study探寻hardlink的本质

在推出ntfs_study的博文中,我谈到过要用一些例子来简单介绍这个工具的用法,本文就算是这个工具的使用介绍以及hardlink在ntfs文件系统底层的简单探讨。

MSDN中写到,hardlink是在文件系统中,用多个在同一个卷的路径表示同一个文件的方法。那么在ntfs格式中这些被link的文件是怎么存在的呢?下面进行一些简单的探讨。

首先,我们需要去创建hardlink的文件。
20130312202150

图中,第一个命令,在target_file.txt的同目录下,创建了hardlink文件link_file.txt。第二个命令,在不同目录(otherdir)下创建了第二个hardlink文件,link_file_in_otherdir.txt。第三个命令返回了错误,原因是我试图在不同卷里面来创建hardlink文件。失败的原因文章后面会介绍。

现在,让我们用ntfs_study来查看ntfs对这三个文件的处理到底是怎么样的。
20130312202324

在这张图中,我们可以清楚的看到,这三个文件的file reference,也就是在主文件表(MFT)的id都是一样的!有一点我们必须明白,一个文件的存在不是因为在目录里面显示了文件名,而是他在MFT中有自己的位置,另外文件名只是文件的一个属性而已,没什么特别的。这也就解释了,看似三个文件为什么会指向同一个文件,因为在目录的记录中,他们指向了同一个id。

为了更加深入的探讨这个问题,我们来看一看id为0xB4A6这个具体情况。首先看看他的file record的数据。

20130312202509

这里可以看到hardlinks的值是4!看到这里,应该就感到奇怪了,我们明明只创建了两个hardlink的文件,为什么这里写的是4呢?实际上,对于ntfs的文件而已,文件名以及他们在那个目录,这些都是属性而已,没有本体和hardlink之分,也就是说,我们原始创建的target_file.txt对于文件本身,也是一个hardlink。那么这个问题还是没解决啊,就算加上本身,最多也就是3个hardlinks,但是这里明明写的是4个!

让我们更加具体的看一看到底是什么回事吧。

20130312202536

首先,我们看到了这个文件的属性中,居然有4个文件名,其中有三个实际上我们已经能够猜到,他们应该分别是target_file,link_file和link_file_in_otherdir这三个名称,那么第四个又是什么呢?只能再进一步看了。

20130312202616

看了这幅图,估计大家就明白了,这个是为了兼容8.3文件名而产生的一个hardlink,只不过在我们现在的系统上隐藏了这个文件hardlink而已。其他三个文件名,如我们刚刚所料,就是那三个文件的名字。

现在解释下为什么hardlink只能在同一个卷里了。原因很显而易见,hardlink实际上是依赖于ntfs的MFT的,而不同的卷,会有不同的MFT,所以不能在不同卷之间创建hardlink也是理所当然的。

SysinternalsSuite中有一个工具叫做findlinks,用来找到一个文件所有的hardlink。其中实现的方法在不同的系统中有所不同,在vista以下的系统中程序调用GetFileInformationByHandle获得文件的MFT id,然后查找整个卷的文件,打开他们获得句柄,再调用GetFileInformationByHandle得到这些文件的id,与之前的id进行比对。可以说,这是非常费时的。而在vista中,这个耗时的问题得到了解决,调用FindFirstFileNameW和FindNextFileNameW就能够文件所有的hardlinks了。

我也计划过两天写一个find_links_study。

NTInternals

access_enum_study —— 仿AccessEnum的工具

access_enum_study 是我逆向AccessEnum所写的程序。写这个逆向加上写这个程序大概用了1个多星期的时间吧。不过说实话,逆向算法还是比较麻烦的事情,所以这个程序里面有一些算法是我自己想的。不过果然不出我所料,效率比起Mark的正牌工具差了不少。这下,真的只能当作玩具玩玩了。

20130309230748

下载access_enum_study

NTInternals

ntfs_info_study —— 仿NtfsInfo工具

ntfs_info_study 这个工具可以显示ntfs卷的一些信息。主要也是学习NtfsInfo的功能,而仿造的一个小工具。ntfs_info_study能显示的信息包括卷大小,扇区数量,簇数量,扇区字节数,簇字节数,主文件表每条记录字节数以及主文件表的一些信息。当然它还可以显示部分NTFS系统文件的信息,例如:$Volume。

实际上ntfs_info_study稍微修复了NtfsInfo的一个问题。原来的NtfsInfo已经无法显示NTFS系统文件的信息了。原因是这个工具调用FindFirstFile这样的函数来查找NTFS系统文件。我不知道什么版本的Windows可以这么做,至少现在Windows 7上,这个方法是行不通的。所以在我从写的工具里,是先打开系统文件,然后查询文件信息,但是普通的CreateFile是打不开这些文件的,这里我的方法是调用OpenFileById。不过实际上,我还没找到正规而且完美显示所有NTFS系统文件的方法,因为部分系统文件在打开的时候会提示访问拒绝。

当然不正规的但是却比较完美的查看NTFS系统文件的方法也有,就是直接打开卷,解析NTFS文件系统数据结构。这个功能已经在ntfs_study中实现了,具体可以移步这个链接

20130227235402

以上是一副对比图,其他功能是一样的,唯一的区别就是最后一项中,ntfs_info_study能够显示部分NTFS系统文件信息。

Usage: ntfs_info_study.exe

使用方法自然也不必说明了,有兴趣的各位可以下载玩玩。

下载ntfs_info_study

NTInternals