副标题#e#
在 1998年12月的MSJ出书刊物中, Jeffrey和我写了关于 在 vc6中利用DelayLoad 成果的专栏.最终功效,是证明白它是何等cool.可是,不幸的是,尚有许多人不相识DelayLoad,他们觉得这个新特点是 最新版本的WINNT才有的.
在开始的时候,让我重申一遍:DelayLoad不是最新的操纵系统带的特有成果,它可以在任何win32系统中起浸染.我将写一个简朴例子来说明. DelayLoadProfile, 实现了一个很小成果,许多措施都可以得益于它.
预览:
凡是的,当挪用一个dll中的函数时,毗连器会将dll和函数插手你的可执行文件.最后,所有引用的函数会放在imports段中. 当加载该措施的时候,win32措施加载器会扫描所有imports段的每个dll.加载,和从头定位imports段的所有函数,将信息写入 引入地点表(ImportAddress Table, IAT).简朴说来,IAT就是一个函数指针的表.挪用该 引入函数的时候,就到IAT中去找. 那么,DelayLoad的机理是什么呢?当你为一个Dll举办"DelayLoad"的时候,毗连器不将本来的值放入imports段,相反,它为每个DelayLoad的引入函数的名称和地点,生成一个小的根区, 备份下来。第一次引用的时候,它挪用LoadLibrary加载Dll,然后,它挪用GetProcAddress取得该函数的地点。最后,改写本身在IAT的值,以便今后的措施可以直接挪用.
上面的是简化的步调.实际上,根区是一小段代码,它以静态的方法毗连到可执行文件中.代码在delayimp.lib中,必需被 毗连措施引用.而且,该代码要足够智能,当一个函数第一次被引用的时候,要挪用LoadLibrary,今后挪用就不消引用了. 和引用Dll对比,DelayLoad不会加太多的时间和空间,这种方法 挪用LoadLibrary只会引起稍微一点点的机能损失.每次措施启动,在针对引入表的函数地点定位的时候,依次对DelayLoad引入的挪用GetProcAddress,相对付Win32加载器来说,所损失的机能也可以忽略不记.
#p#副标题#e#
然而,DelayLoad带来的长处也是不行相比的.譬喻:假如你的措施从来没有 从Delay挪用引入的函数,Dll的第一次是不会被加载的。有时候,这个环境的呈现频率出乎你想象。如果,你的措施中,包括打印的代码,毫无疑问,纵然用户没有利用打印成果,你的措施也必然要加载winspool.drv。在这种环境下,利用DelayLoad,你就不必加载和初始化Winspool.drv.
别的一个长处就是:DelayLoad可以制止挪用某些方针平台不存在的API。譬喻,如果你的措施需要挪用AnimateWindow,这个API在Win2000和Win98中存在,可是在Win95和WinNT4中,就不存在,如果你用通例的方法挪用AnimateWindow,那么,你的措施将不能再早期的平台中运行。然而,你可以用DelayLoad举办对AnimateWindow的加载查抄。这样,你就不必改写你的代码为LoadLibrary和GetProcAddress的方法了。
DelayLoad是很容易利用的。当你抉择哪个dll你想利用DelayLoad,只需要简朴的增加/DELAYLOAD:DLLNAME。个中,DLLNAME是相关的DLL文件名。你还需要增加DELAYIMP.LIB到毗连库中,你也需要本来的LIB,譬喻,SHELL32.LIB。把全部放到一块,毗连的呼吁就如下: SHELL32.LIB /DELAYLOAD:SHELL32.DLL DELAYIMP.LIB 很不幸,Visual Studio 6.0 IDE 不提供一个简朴的要领去实现一个Dll的DelayLoad。所以,你必需手工插手:/DELAYLOAD:XXX 呼吁行到 "Project settings"->"Link"->"Project Options"中。
什么时候需要DelayLoad:
当你有小的工程,它挪用了多个dll,就是一个好的DelayLoad候选例子。然而,工程大概在今后由于其它开拓者的插手而变大,很容易丢失挪用dll的跟踪。我凡是用sdk中的depends.exe。一个只有少数函数要引入的dll就是一个好的开始。
然而,我想找到一个简朴的,自动的要领来跟踪。于是,出来了DelayLoadProfile措施。它是一个exe,可以监督你的exe文件对dll的挪用,直到你的exe竣事。它打印出dll被挪用的环境的汇总,包罗几多个dll被挪用,每个dll有几多个函数被引入。
我在这里强调:DelayLoadProfile只是针对exe有效,当它涵盖你的措施所关联的所有dll的时候,有时会造成一点点巨大。DelayLoadProfile只给你哪个dll可以用DelayLoad开关的体现,你最亏得不确定的时候,利用本来的处理惩罚要领。
DelayLoadProfile:具体描写
其实DelayLoadProfile的道理很简朴:重定向 exe中,IAT的函数的指针到一段根区。根区简朴的符号一下,引入的函数被挪用了。然后,跳入本来的Win32加载提供的IAT地点。只是,难的是如何实现。
第一,你必需抉择,要在那边运行你的代码,实现对exe的IAT进口的变动,把他们指定到那段根区去。这些都是在历程外完成。这样可以制止你的代码牵涉到目标exe历程中。这个可以用遍历所有的数据布局,定位和修改IAT布局的要领。我在这里操作了许多ReadProcessMemory挪用。
#p#分页标题#e#
接着的费力事情是要在和目标exe沟通的历程空间里完成。险些是很琐碎的事情:遍历所有的数据布局,成立根区,从定向IAT进口,然后在完成的时候,汇总功效。然而,为了完成历程空间的事情,在exe历程运行的时候,一些 DelayLoadProfile代码必需被加载到目标exe的历程空间。这个是我要做的。
当确认到需要在目标历程中,加载我的代码的时候,下个问题就是如何把我的代码插手到目标历程中。个中一个选择就是,要求用户毗连我的DelayLoadProfile库,这个会造成用户的很大量的对他们源代码工程,可能Makefile的变动,所以,我不能回收,此刻需要一个完全自动化的要领。
在这点上,我想到了加载措施,然后,插入我的DelayLoadProfiledll进去,一个技能就是用CreateRemoteThread,在方针历程,建设一个LoadLibrary的线程。我放弃了这个,因为,win9x中,不提供CreateRemoteThread.
好久以前的MSJ读者大概记得我5年前写的一个叫APISPY32的措施。它加载一个历程,插入一个dll来记录API的挪用。谁人有点像我本日的DelayLoadProfile事情。然而,我在Win200中,挪用谁人dll失败。有一点点问题。我以为此刻是时候要重读那段代码,而且纠正谁人错误了。
继承深入:
从头复习一下,DelayLoadProfile包括2部门,一,是历程加载成果,它会打针一个dll到你的历程的地点空间。然后,谁人dll扫描你的所有的exe IAT,从头定向他们到dll建设的根区中。当你的措施完成后,打针的dll会扫描所有的根区,统计出几多dll和函数它挪用的。假如你曾经用过APIMON的相关部件,你将认出雷同的技能细节。
完成所有的事情,包罗 监督 一个措施的引入的dll,叫DelayLoadProfileDLL.(看Figure 1).它用到DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH来初始化2个主要的事情。
当DllMain得到DLL_PROCESS_ATTACH的动静的时候,DelayLoadProfileDLL挪用PrepareToProfile(),在PrepareToProfile中,代码加载目标EXE的IAT,对付每个它发明的引用的DLL,代码还检测是否安详的重定向IAT。通过IsModuleOKToHook函数来检测,大大都环境下,是安详的,因此,PrepareToProfile包罗了RedirectIAT函数。
RedirectIAT是较量巨大的函数。假如你领略了 winini.h中的引入相关数据布局,你将获得很大的辅佐。首先,函数定位IAT和相关的引入名字表,然后,计较有几多个IAT进口,扫描所有的IAT,查找NULL的指针。获得了数目后,措施将建设一个DLPD_IAT_STUB根区,每个根区对应一个IAT进口。
最后,代码从头扫描IAT,获取每个IAT进口的地点,用根区的一个包括JMP指令的地点,替换IAT进口。它还扫描下一个IATDLPD_IAT_STUB根区。我在后头将还会继承理会。
在重定向IAT进口的根区中,有2个值得提起:1,IAT经常被放到EXE的只读段,凡是,实验改写只读段,会引起会见违规,幸运的,VirtualProtect答允你变动一个目标地点的属性。此刻必需变动iat的属性为读/写。完成后,代码要规复IAT段本来的属性。
别的一个要留意的处所,就是在重定向IAT的时候,有数据引入的问题。固然措施员们很少这样做,可是,很容易用增加的代码去导入数据。vc++运行库DLL(MSVCRT.DLL)有数据导出。假如重定向一个数据IAT的进口,会导致问题。
那么,如何判定一个IAT是数据呢?一个贸易的软件,应该用精确的算法来判定一个IAT进口的范例。可是,我在这里用了一个快捷要领。就是IsBadWritePtr。假如IAT包括的指针是可写的,那么,很大概是一个数据指针。假如是只读的,那么,应该是一段代码。这个测试符合吗?不,可是,它对DelayLoadProfile是足够了。
此刻看一下根区,在DelayLoadProfileDLL.h中界说的DLPD_IAT_STUB布局包括着代码和数据。简朴来说,就是如下:
CALL DelayLoadProfileDLL_UpdateCount
JMP xxxxxxxx //original IAT 地点
DWORD count
DWORD pssNameOrOrdinal
当exe挪用个中一个重定向的函数时,节制权被转到根区的CALL指令中,挪用DelayLoadProfileDLL.CPP中的DelayLoadProfileDLL_UpdateCount函数,在call指令返回时,继承挪用jmp 跳转到IAT本来取得的地点中。Figure2显示了布局示意图。
汇编好手会对DelayLoadProfileDLL_UpdateCount函数能确定根区的COUNT字段的地点,感想迷惑,通过快速的察看代码,会发明DelayLoadProfileDLL_UpdateCount会在仓库中,查找到返回地点。返回地点指着JMP xxxxxxxx指令。因为,CALL挪用老是5个字节,按照这些算法,可以确定COUNT字段的地点。
#p#分页标题#e#
有一个问题值得提醒,就是DelayLoadProfileDLL_UpdateCount没有挪用PUSHAD和POPAD指令来生存/回覆CPU寄存器的值。这段代码在许多措施上都事情正常,可是,却在一些函数中,不能正常事情。最后,发明 MSVCRT.DLL的__CxxFrameHandler和 _EH_prolog有问题,这2个函数 期望eax寄存器被配置成某个值。然而,DelayLoadProfileDLL_UpdateCount变动了EAX. 既然这个是由于EAX引起的问题,那么,我增加了PUSHAD和POPAD,昏厥,问题还存在。在蒙受荆棘后,我查抄了汇编生成的代码。凡是,VC6编译器会插入将所有当地变量都初始化为0xCC的代码。这些代码会在PUSHAD和POPAD前,将EAX改变。我只好移去/GZ的选项。
功效陈诉:
当你的历程遏制的时候,系统对所有加载的DLL发送一个DLL_PROCESS_DETACH动静。DelayLoadProfileDLL利用这个选项来汇集措施运行进程中,得到的功效。也是说,再次遍历所有的根区单位。收集所有得到的数据,输出。
在DelayLoadProfileDLL安装的阶段,重定向IAT,它生存exe的IAT到一个民众的变量出g_pFirstImportDesc。在封锁的进程中,ReportProfileResults用到这个指针来再次遍历引入段。假如这个IAT是被重定向的,那么,第一个IAT的指针应该指到第一个为该DLL分派的DLPD_IAT_STUB根区内存。虽然,代码保持了根基的测试要领,假如某些处所不正确,DelayLoadProfileDLL忽略该特定的dll。
总的说来,所有的都很正常,而且,第一个IAT进口指到我的根区单位。对付每个DLL,代码重复的遍历所有的根区。每个相关的根区,它的包括的字段的值,将加到该DLL的总计数。当遍历完成,ReportProfileResults名目化一个字符串,输出该dll的名字,和挪用的总次数。代码还用OutputDebugString广播该功效。
加载和打针:
本措施加载你的exe,打针DelayLoadProfileDLL.dll将会挪用,(你猜到了),是DelayLoadProfile.exe(源文件可以在msj的网站找到,http://www.microsoft.com/msj)。这个代码主要担任了CDebugInjector类。我将简朴的先容它。函数主要包括了目标exe的呼吁行,而且通报到CDebugInjector::LoadProcess。假如历程被乐成建设,函数会汇报CDebugInjector,哪个dll会被打针,既然是这样,和DelayLoadProfile.exe同目次的DelayLoadProfileDLL.DLL,将会被加载。
在运行方针措施之前,最后的步调是挪用CDebugInjector::SetOutputDebugStringCallBack。当DelayLoadProfileDLL用OutputDebugString来输出陈诉功效的时候,CDebugInjector看到他们,然后通报他们到你已经注册的回调函数中。这个回调函数只是用printfs输出字符串到节制台。最后,函数挪用CDebugInjector::Run。这样,目标历程开始运行,其机缘成熟,打针dll进去。 描写3(hoodtextfigs.htm#fig3)说明白CDebugInjector类。这是代码实现的处所。CDebugtInjector::LoadProcess建设了目标历程,作为一个调试历程,它的分支已经在msdn的许多文档中接头过了,这里,不想作太多详细的接头。
调试历程运行后(这里是DelayLoadProfile)进入了一个轮回,不绝的挪用WaitForDebugEvent和ContinueDebugEvent,直到调试遏制。每次WaitForDebugEvent返回,都有些对象产生在调试措施身上。大概是一个异常(包罗断点),可能加载一个dll,可能建设一个线程,可能其他事件。WaitForDebugEvent文档历包括了所有的大概的事件。CDebugInjector::Run进程包括这个轮回的代码。
那么,如何让目标历程作为一个被调试历程,辅佐你打针一个dll呢?一个调试历程可以节制的被调试历程的执行进程。每次被调试措施有一个信号事件产生,它城市暂停,期待调试者挪用ContinueDebugEvent继承运行。相识了这个,一个调试历程可以增加代码到被调试历程的空间,和姑且改变被调试者的寄存器值,以便增加的代码运行。
在某些特定场所,CDebugInjector合成了一小段代码根区来挪用LoadLibrary。LoadLibrary的dll名字参数,指到要被打针的dll的名字。CDebugInjector写谁人根区(和相关联的dll名字)到被调试者的地点空间。然后,挪用SetThreadContext来改变被调试者的指令寄存器,运行LoadLibrary根区。所有的相关代码在CDebugInjector::PlaceInjectionStub进程中。
立即的,根区中的LoadLibrary挪用后,是一个断点(int 3)。这个暂停被调试者的运行,交回节制权给调试的历程。调试者用SetThreadContext,规复指令寄存器和其他寄存器到本来的值。另一次挪用ContinueDebugEvent,被调试者在dll打针的状态下,继承运行。没有人知道产生了什么工作。
假如你不想那么多,这个打针历程不会以为太难,可是,一些有乐趣的对象,弄巨大了工作。譬喻,什么时候建设根区,改变运行代码,才是适当呢?你不能在CreateProcess后立即做这个,因为,引入的dll还没有被映射到内存中,WIN32加载器还没有成立exe的IAT。相当于:太早了。
#p#分页标题#e#
最后,我抉择让被调试者运行,直到遇到了第一个断点。我在措施进口处,配置了一个本身的断点。当第2次间断被触发,CDebugInjector知道目标历程的DLL,都被初始化了(包罗Kernel32.dll)。可是,在exe中,还没有代码运行。此刻是时候打针DelayLoadProfileDLL.DLL了。
顺便说一下:断点从那边来呢?通过界说,一个被调试的win32的历程,在运行之前,会挪用DebugBreak(也是int3),在我早期的apispy32代码中,我选用了最初的DebugBreak来做打针。在win2k中,很是不幸,这个DebugBreak在Kernel32.dll初始化之前,被挪用,那么,CDebugInjector配置它的断点到exe即将得到节制的处所,那么,kernel32.dll被初始化了。
在之前,我提到在LoadLibrary挪用后,产生的一个断点。这是第3个CDebugInjector要处理惩罚的断点,所有的处理惩罚差异断点的能力,可以参考CDebugInjector::HandleException。
别的一个关于打针dll的有乐趣的问题,就是在哪里写LoadLibrary单位,在winnt4.0今后,你可以用VirtualAllocEx来为某个线程申请内存。我回收了这个要领。此刻,剩下不能支持VirtualAllocEx的Win9x,针对这个问题,我操作了win9x内存映射文件的一个非凡的特性,这些文件在所有的地点空间都可见。而且,是同一个地点。我简朴的操作系统页面文件作为支持,建设了一个小的内存映射文件,写了LoadLibrary根区进去。该根区对付被调试措施,是可见的。更多的具体环境,请看文章首部的连结的CDebugInjector::GetMemoryForLoadLibraryStub。
利用DelayLoadProfile:
DelayLoadProfile是一个输出功效到尺度输出的呼吁行措施。在呼吁行提示中,运行DelayLoadProfile,拟定目标措施,和它需要的参数,譬喻: DelayLoadProfile notepad c:\autoexec.bat下面是针对(windows 2000 Release Candidate2)的calc.exe, 运行DelayLoadProfile的功效:
[d:\column\col66\debug]delayloadprofile calcDelayLoadProfile: SHELL32.dll was called 0 times
DelayLoadProfile: MSVCRT.dll was called 9 times
DelayLoadProfile: ADVAPI32.dll was called 0 times
DelayLoadProfile: GDI32.dll was called 60 times
DelayLoadProfile: USER32.dll was called 691 times
我简朴的开始calc,然后,当即封锁。留意到,shell32.dll和advapi32.dll都没有挪用,这2个dll是最初的calc用来DelayLoad的候选。
你将回以为奇怪,为什么calc挪用shell32.dll,你没有挪用它。假如你针对CALC,挪用DumpBin /IMPORTS可能Depends.exe阐明,你将看到,CALC从SHELL32.DLL中引入的函数只有ShellAboutW。简朴来说,只有你选者CALC的HELP|About Calculator菜单项,才会完全的挪用SHELL32.DLL入内存。这个是一个最明明的/DELAYLOAD显示其代价的例子。顺便说,SHELL322.DLL简朴的,毫无条件的加载SHLWAPI.DLL和COMCTL32.DLL,而且初始化。
假如只是因为DelayLoadProfile陈诉一个dll没有被挪用,可能很少挪用,你就可以自动的 延迟加载,你要当真简直定,哪一个黑暗连结的dll,你要利用/DELAYLOAD。这种环境下,假如由于其他的依赖,你的DLL要被自动的加载和初始化,那么,/DELAYLOAD就没有意义了。平台sdk带的Depends.exe是一个很有用的东西,可以看到一个dll的利用环境。
在你的测试进程中,你的测试的措施的个数,也是值得思量的。假如你测试了所有的措施的成果,所有的被引入的dll都包罗了。小我私家认为,我以为应该只管缩小初始化时间,这个大概是意味着你只是开始你的措施,然后封锁它。要加速初始化,就依次加载dll。用户都是主观的由启动时间判定你的措施的速度。
我发明几个DLL可以从/DELAYLOAD处得益。从上所述,SHELL32.DLL是个中一个。别的一个是打印支持的WINSPOOL.DRV。既然许多用户都不常常打印,那么,就是很好的回收者。尚有,雷同的OLE32.DLL和OL3AUT32.DLL。一个多态的措施,在小容器中,用到COM和OLE,那么,相关的DLL也是可以选用的。譬喻,WIN2000的CDPLAYER.EXE和OLE32.DLL毗连,用到了CreateStreamOnHGlobal函数。可是,在凡是的环境下,我没有发觉到这个函数被挪用。
DelayLoadProfile并不是没有它的短处,当我在许多措施针对IAT,用DelayLoadProfileDLL乐成测试后,你大概还会遇到不正确的运行的环境。要完全办理这个问题,就超出了本次接头的范畴。然而,假如你乐成办理了个中一个问题,请让我知道。我将在未来的一天更新DelayLoadProfile。 我知道某些引入mfc42.dll和mfc42u.dll的措施会和DelayLoadProfile斗嘴,于是,我回收了一个要领,在DelayLoadProfileDLL.cpp,有一个IsModuleOKToHook函数,我放了MFC42.DLL,MFC42U.DLL和KERNEL32.DLL进去。(你不能用 /DELAYLOAD 和KERNEL32.DLL关联,因为,是没有浸染的)假如一个出格的DLL会出问题,你应该放到IsModuleOKToHook函数中。 我但愿DelayLaodProfile会辅佐你的措施回收/DELAYLOAD。我今后应该还会有时间去更新一些专业的带骂,而且,我还但愿听到你的乐成的故事.