## CSDN博客

### 利用DelayLoad来优化应用程序的性能.拦截API.

               -- 中文翻译, by snake.(http://snake12.top263.net)

利用DelayLoad来优化应用程序的性能.       拦截API.

在 1998年12月的MSJ出版刊物中, Jeffrey和我

在开始的时候，让我重申一遍:DelayLoad不是最

DelayLoadProfile, 实现了一个很小功能,很多程序

通常的,当调用一个dll中的函数时,连接器

当加载该程序的时候,win32程序加载器会扫描

Address Table, IAT).简单说来,IAT就是一个函数

那么,DelayLoad的机理是什么呢?当你为一个Dll

imports段,相反,它为每个DelayLoad的引入函数的

上面的是简化的步骤.实际上,根区是一小段代码，

和引用Dll相比,DelayLoad不会加太多的时间和空间,

然而，DelayLoad带来的好处也是不可比拟的.例如：

另外一个好处就是：DelayLoad可以避免调用某些目标

DelayLoad是很容易使用的。当你决定哪个dll你想使用
DelayLoad，只需要简单的增加/DELAYLOAD:DLLNAME。其中，
DLLNAME是相关的DLL文件名。你还需要增加DELAYIMP.LIB到

SHELL32.LIB /DELAYLOAD:SHELL32.DLL DELAYIMP.LIB
很不幸，Visual Studio 6.0 IDE 不提供一个简单的方法

/DELAYLOAD:XXX 命令行到 "Project settings"->"Link"->
"Project Options"中。

当你有小的工程，它调用了多个dll，就是一个好的DelayLoad

然而，我想找到一个简单的，自动的方法来跟踪。于是，

exe文件对dll的调用，直到你的exe结束。它打印出dll被调

我在这里强调：DelayLoadProfile只是针对exe有效，当它

DelayLoadProfile:详细描述
其实DelayLoadProfile的原理很简单：重定向 exe中，IAT

第一，你必须决定，要在哪里运行你的代码，实现对exe的IAT

接着的艰苦工作是要在和目的exe相同的进程空间里完成。几

当确认到需要在目的进程中，加载我的代码的时候，下个问

在这点上，我想到了加载程序，然后，插入我的
DelayLoadProfiledll进去，一个技术就是用CreateRemoteThread，

win9x中，不提供CreateRemoteThread.
很久以前的MSJ读者可能记得我5年前写的一个叫APISPY32的

重新温习一下，DelayLoadProfile包含2部分，一，是进程

完成所有的工作，包括 监视 一个程序的引入的dll,叫
DelayLoadProfileDLL.(看Figure 1).它用到DLL_PROCESS_ATTACH

当DllMain获得DLL_PROCESS_ATTACH的消息的时候，
DelayLoadProfileDLL调用PrepareToProfile(),在PrepareToProfile中,

RedirectIAT是比较复杂的函数。如果你理解了 winini.h中

IAT和相关的引入名字表，然后，计算有多少个IAT入口，扫描

DLPD_IAT_STUB根区，每个根区对应一个IAT入口。
最后，代码重新扫描IAT，获取每个IAT入口的地址，用根区

DLPD_IAT_STUB根区。我在后面将还会继续解析。
在重定向IAT入口的根区中，有2个值得提起：1，IAT常常

另外一个要注意的地方，就是在重定向IAT的时候，有数据引

那么，如何判断一个IAT是数据呢？一个商业的软件，应该用

现在看一下根区，在DelayLoadProfileDLL.h中定义的
DLPD_IAT_STUB结构包含着代码和数据。简单来说，就是如下：
CALL DelayLoadProfileDLL_UpdateCount
JMP xxxxxxxx //original IAT 地址
DWORD count
DWORD pssNameOrOrdinal

当exe调用其中一个重定向的函数时，控制权被转到根区的CALL

DelayLoadProfileDLL_UpdateCount函数，在call指令返回时，继续

汇编高手会对DelayLoadProfileDLL_UpdateCount函数能确定

5个字节，根据这些算法，可以确定COUNT字段的地址。
有一个问题值得提醒，就是DelayLoadProfileDLL_UpdateCount

DelayLoadProfileDLL_UpdateCount更改了EAX.
既然这个是由于EAX引起的问题，那么，我增加了PUSHAD和POPAD，

当你的进程停止的时候，系统对所有加载的DLL发送一个
DLL_PROCESS_DETACH消息。DelayLoadProfileDLL使用这个选项来

在DelayLoadProfileDLL安装的阶段，重定向IAT，它保存exe的IAT

ReportProfileResults用到这个指针来再次遍历引入段。如果这个
IAT是被重定向的，那么，第一个IAT的指针应该指到第一个

总的说来，所有的都很正常，并且，第一个IAT入口指到我的根区

ReportProfileResults格式化一个字符串，输出该dll的名字，和调

本程序加载你的exe，注射DelayLoadProfileDLL.dll将会调用，
（你猜到了），是DelayLoadProfile.exe（源文件可以在msj的网站

CDebugInjector类。我将简单的介绍它。函数主要包含了目的exe

在运行目标程序之前，最后的步骤是调用
CDebugInjector::SetOutputDebugStringCallBack。当DelayLoadProfileDLL

描述3（hoodtextfigs.htm#fig3）说明了CDebugInjector类。这是

调试进程运行后（这里是DelayLoadProfile)进入了一个循环，不断的

WaitForDebugEvent返回，都有些东西发生在调试程序身上。可能是一个

CDebugInjector::Run过程包含这个循环的代码。
那么，如何让目的进程作为一个被调试进程，帮助你注射一个dll呢？

在某些特定场合，CDebugInjector合成了一小段代码根区来调用
LoadLibrary。LoadLibrary的dll名字参数，指到要被注射的dll的名字。
CDebugInjector写那个根区（和相关联的dll名字）到被调试者的地址

LoadLibrary根区。所有的相关代码在CDebugInjector::PlaceInjectionStub

立刻的，根区中的LoadLibrary调用后，是一个断点(int 3)。这个暂停

如果你不想那么多，这个注射进程不会觉得太难，但是，一些有兴趣的

最后，我决定让被调试者运行，直到碰到了第一个断点。我在程序入口

顺便说一下：断点从哪里来呢？通过定义，一个被调试的win32的进程，

DebugBreak在Kernel32.dll初始化之前，被调用，那么，CDebugInjector

在之前，我提到在LoadLibrary调用后，发生的一个断点。这是第3个
CDebugInjector要处理的断点，所有的处理不同断点的技巧，可以参考
CDebugInjector::HandleException。
另外一个关于注射dll的有兴趣的问题，就是在那里写LoadLibrary单元，

DelayLoadProfile是一个输出结果到标准输出的命令行程序。在命令行

DelayLoadProfile notepad c:/autoexec.bat

DelayLoadProfile的结果：
[d:/column/col66/debug]delayloadprofile calc
DelayLoadProfile: 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，你没有调用它。如果你

如果只是因为DelayLoadProfile报告一个dll没有被调用，或者很少调用，

在你的测试过程中，你的测试的程序的个数，也是值得考虑的。如果你

我发现几个DLL可以从/DELAYLOAD处得益。从上所述，SHELL32.DLL是

OL3AUT32.DLL。一个多态的程序，在小容器中，用到COM和OLE，那么，

DelayLoadProfile并不是没有它的毛病，当我在很多程序针对IAT，用
DelayLoadProfileDLL成功测试后，你可能还会碰到不正确的运行的情况。

我知道某些引入mfc42.dll和mfc42u.dll的程序会和DelayLoadProfile

IsModuleOKToHook函数，我放了MFC42.DLL，MFC42U.DLL和KERNEL32.DLL进

我希望DelayLaodProfile会帮助你的程序采用/DELAYLOAD。我以后应该

matt@wheaty.net，或者 http://www.wheaty.net

In the December 1998 issue of MSJ, Jeffrey Richter and I wrote dueling columns on the DelayLoad feature of the Microsoft® Visual C++® 6.0 linker. The fact that both Jeff and I jumped on this topic is testimony to how cool this feature is. Unfortunately, I still find people who don't know anything about DelayLoad or they think it's some feature that's available only in the latest version of Windows NT®. For starters, let me scream from the highest rooftop that DelayLoad is not an operating system feature. It works on any Win32®-based system. With that off my chest, I'll demonstrate this month's utility, DelayLoadProfile, which makes it almost trivial to determine whether your program can benefit from DelayLoad. As I'll show, even some of Microsoft's own programs can benefit from it.A Quick Review If you're wondering "What's this thing Matt's gone off the deep end over?" a quick recap of DelayLoad is in order. Here's how it works. Normally, when calling an imported function in a DLL, the linker adds information about the imported DLL and function to your executable. Collectively, the information for all the imported functions is known as the imports section. The Win32 loader scans through the imports section at load time and loads each DLL. For each DLL loaded, the loader iterates through all the imported functions and locates their addresses in the imported DLL. These addresses are written back to the imports section in a location known as the Import Address Table (IAT). A simple way to think of an IAT is as an array of function pointers. When calling an imported function, the call uses one of the function pointers from the IAT. How does the picture change with DelayLoad? When you specify DelayLoad for a DLL, the linker doesn't emit the usual data it would put in the imports section. Instead, it generates a small stub for each DelayLoad imported function. This stub points to the imported DLL and function name. Upon calling an imported function for the first time, the stub calls LoadLibrary to load the DLL. Next, it calls GetProcAddress to get the address of the called function. Finally, the stub overwrites part of itself so that subsequent calls to the function go directly to the target code. What I've just described is a slight simplification. In reality, the stub is a small bit of code that calls a routine statically linked into your executable. This routine resides in DELAYIMP.LIB, which must be included in the list of libraries that the linker uses. Also, the stubs and DELAYIMP.LIB code are smart enough to call LoadLibrary only the first time a function in the DLL is used. Subsequent calls to other functions in the same DelayLoad imported DLL don't call LoadLibrary.All things considered, DelayLoad doesn't add much time or space overhead compared to importing the DLL the usual way. Calling LoadLibrary is only slightly less efficient than letting the Win32 loader load the DLL. Likewise, calling GetProcAddress once for each DelayLoad imported function is only slightly slower than having the Win32 loader locate the imported functions at startup. However, the benefits of DelayLoad can easily make up for these small speed penalties. For starters, if you never call a function in a DelayLoad imported DLL, the DLL isn't loaded in the first place. This comes in handy more often than you may think. Consider the situation in which you have printing code in your program. If the user doesn't print something during a program session, you've loaded WINSPOOL.DRV for no reason. In this case, using DelayLoad is actually faster since you never loaded and initialized WINSPOOL.DRV. Another benefit of using DelayLoad is that you avoid calling APIs that are not available on one of your target platforms. For instance, say you want to call AnimateWindow, which is supported in Windows® 98 and Windows 2000, but not Windows 95 or Windows NT 4.0. If you were to call AnimateWindow the usual way, your code wouldn't load on the earlier platforms. However, with DelayLoad you can make a runtime check of which operating system you're on and only call AnimateWindow if it's supported. There's no need for you to muck up your code with calls to LoadLibrary and GetProcAddress. Using DelayLoad is incredibly easy. Once you know which DLLs you want to use DelayLoad with, simply add /DELAYLOAD:DLLNAME, where DLLNAME is the name of the DLL. You'll also need to add DELAYIMP.LIB to the linker's library list, and you'll still need the original import library, for example, SHELL32.LIB. Putting everything together, to DelayLoad against SHELL32.DLL your linker line would need the following:
SHELL32.LIB /DELAYLOAD:SHELL32.DLL DELAYIMP.LIB
Unfortunately, the Visual Studio® 6.0 IDE doesn't have an easy way for you to specify DelayLoading for DLLs. In Visual Studio 6.0, you'll have to add the /DELAYLOAD:XXX command-line fragment manually to the Project Settings | Link | Project Options edit field. When to Use DelayLoad When you have a small project, it's easy to come up with a list of DLLs that are good DelayLoad candidates. However, because projects may grow and can involve many developers, it's just as easy to lose track of who uses which DLL. In the past, I've relied on gut instinct and Depends.EXE from the Platform SDK. A DLL from which only a few functions are imported is a good place to start.However, I wanted a way to automate and simplify the process. Thus was born the DelayLoadProfile program. DelayLoadProfile is a tool that runs your EXE and monitors the DLLs and functions that your EXE calls. After your program terminates, DelayLoadProfile spits out a summary of which DLLs were used and how many calls were made to each DLL. A DLL that's imported, but which had no calls made to it, is a good candidate for DelayLoad importing. Let me emphasize one point before continuing: DelayLoadProfile works only against your EXE. While it could be extended to recurse into all of your imported DLLs and their dependencies, that would significantly complicate its code. As I'll explain later, DelayLoadProfile just gives you hints about which DLLs you might consider using /DELAYLOAD on. You still have to use that neuron-based processing unit between your ears to make sure it makes sense to do so. DelayLoadProfile: The Big Picture The concept behind DelayLoadProfile is simple. Redirecting the function pointers in the EXE's IAT to point to a stub is all that's needed. The stub simply notes that the imported function has been called, then jumps to the address that the Win32 loader originally stored in the IAT. However, the devil is in the details. First, you must decide where the code will run that locates and modifies the EXE's IAT entries to point to the stubs. Doing the work out-of-process in some sort of control program is one option. This avoids the work involved in getting your code into the target EXE's process. The downside is that it's more work to traverse all the data structures necessary to locate and patch the IAT entries, as well as gather the results later. I'd be swimming in ReadProcessMemory calls. The other approach is to do the hard work in the same process space as the target EXE. This makes it almost trivial to march through the data structures, build stubs, redirect the IAT entries, and summarize the results at the end. However, doing the work in-process requires that some of the DelayLoadProfile code be loaded into the target EXE's process as it runs. This is the path I took. Having committed to running my code in-process with the target, the next problem was figuring out how to get my code into the target process. One choice would have been to ask the user to link with the DelayLoadProfile code. Knowing it would require some effort by the target audience, I discarded this option. If a DelayLoadProfile user needed to modify their source, project, or makefile, many would pass. I needed to make DelayLoadProfile a complete no-brainer. At this point, I had boxed myself into some sort of loader program that would run the target EXE and inject my DelayLoadProfile DLL into it. One technique for DLL injection is to use CreateRemoteThread to start a thread in the target process that calls LoadLibrary on your DLL. I discarded this approach because CreateRemoteThread isn't available on Windows 9x, which I wanted to support.Longtime MSJ readers may remember a program I wrote more than five years ago called APISPY32. It loads a process and injects a DLL into it for the purposes of logging API calls. That sounds similar to what I needed DelayLoadProfile to do. Alas, when I ran APISPY32 on Windows 2000, it failed to load the DLL. A little digging revealed the source of the problem, and I decided it was time to revamp this code for a whole new generation of programmers. Into the Trenches To review quickly, DelayLoadProfile is a two-part system. A loader process runs your program. Early on in your program, the loader process injects a DLL into your program's address space. This DLL scans through your EXE's IAT and redirects the imported functions to point to stubs that the DLL creates. When your program shuts down, the injected DLL scans through the stubs it has created and summarizes how many calls were made to each imported DLL. If you've ever used the APIMON utility from the Platform SDK, you'll recognize the similarities. The DLL that does all the work of monitoring a program's use of imports is called DelayLoadProfileDLL (see Figure 1). DelayLoadProfileDLL uses the DLL_PROCESS_ATTACH and DLL_PROCESS_DETACH notifications sent to its DllMain procedure to initiate the two primary phases of the DLL's work. When its DllMain gets the DLL_PROCESS_ATTACH notification, DelayLoadProfileDLL calls PrepareToProfile. Inside PrepareToProfile, the code locates the target EXE's IAT. For each imported DLL it finds, the code determines if it's a DLL that's safe for IAT redirection. It does this by calling the IsModuleOKToHook function. Most of the time, it's OK to redirect the IAT, so PrepareToProfile invokes the RedirectIAT function. RedirectIAT is where things get dirty, and it really helps if you understand the import-related data structures in WINNT.H. First, the function locates the IAT and the associated Import Names Table. The code then counts how many IAT entries there are by scanning through the IAT, looking for a NULL pointer. With this count, an array of DLPD_IAT_STUB stubs is created, with one stub for each IAT entry. Finally, it's time for meatball surgery. The code makes yet another pass through the IAT. This time it grabs the address in each IAT entry, stuffs it into a JMP instruction in the stub, and redirects the IAT entry to point to the stub. As the code advances through each subsequent IAT entry, it also advances to the next DLPD_IAT_STUB stub in the allocated array. I'll explain DLPD_IAT_STUB stubs a little later in this column. Two aspects of redirecting the IAT entries to the allocated stubs are worth mentioning. First, the IAT is often placed in a read-only section of the EXE. Ordinarily, an attempt to modify such an IAT pointer would result in an access violation. Luckily, the VirtualProtect API comes to the rescue and enables you to modify the attributes of a target address, in this case, the IAT. Read-write is the attribute you're looking to modify. When it's finished, the code restores the original memory protection attributes. The other tricky part of redirecting the IAT occurs when you encounter a data import. Although programmers don't frequently do so, it's relatively easy to import data in addition to code. The Visual C++ runtime library DLL (MSVCRT.DLL) has data exports. Redirecting an IAT entry that refers to data in an imported DLL is almost certainly a recipe for problems. So how do you determine whether an import is a normal code import or a data import? A commercial product could implement a sophisticated algorithm to determine the import type of an IAT entry. However, I took a shortcut and used IsBadWritePtr. If the IAT points to memory that's writeable, it's probably pointing to data. Likewise, if it points to read-only memory, odds are that it's pointing to code. Is this a perfect test? No, but it's good enough for DelayLoadProfile's needs. Now let's take a look at the stubs. The DLPD_IAT_STUB structure in DelayLoadProfileDLL.H contains the layout, which is a mixture of code and data. Simplifying this structure, a DLPD_IAT_STUB stub looks like this:
CALL    DelayLoadProfileDLL_UpdateCount
JMP     XXXXXXXX // original IAT address
DWORD   count
DWORD   pszNameOrOrdinal
When the EXE calls one of the redirected functions, control goes to the CALL instruction in the stub. The DelayLoadProfileDLL_UpdateCount routine in DelayLoadProfileDLL.CPP simply increments the value of the count field of the stub. After that CALL returns, the JMP instruction transfers control to the original address that was stored in the IAT before I bashed it. Figure 2 shows the big picture after the IAT has been redirected to the stubs. Assembler junkies might be wondering how the DelayLoadProfileDLL_UpdateCount function knows where the stub's count field is in memory. A quick look at the code shows that DelayLoadProfileDLL_UpdateCount finds the return address pushed on the stack by the CALL instruction. The return address points to the JMP XXXXXXXX instruction following the call. Since the CALL instruction is always five bytes, some pointer arithmetic yields the stub's starting address and easy access to the stub's count field. I had one problem using the DelayLoadProfileDLL_UpdateCount code that's worth mentioning. Originally, the function didn't have the PUSHAD and POPAD instructions to save and restore all of the regular CPU registers. The code worked fine on many programs, but just blew up on others. Finally, I narrowed it down to programs that imported __CxxFrameHandler and _EH_prolog from MSVCRT.DLL. Both of these APIs expect the EAX register to be set to a given value, and DelayLoadProfileDLL_UpdateCount was trashing EAX. Since the trashed EAX was the problem, I added PUSHAD and POPAD. Alas, the problem remained. In frustration, I examined the compiler-generated code, and then smacked my forehead. Normally when generating code for a debug build, the Visual C++ 6.0 compiler inserts code in the function prolog to set all local variables to the value 0xCC. This code was trashing EAX before my PUSHAD got a chance to execute. To get around this, I had to remove the /GZ option from the debug build settings for DelayLoadProfileDLL. Reporting Results As your process shuts down, the system sends the DLL_ PROCESS_DETACH notification to all loaded DLLs. DelayLoadProfileDLL uses this opportunity to harvest the information collected during the run. In a nutshell, this means scanning through all the stub arrays, counting the number of calls that were made through the stubs, and reporting what it finds. During the setup phase when DelayLoadProfileDLL was redirecting the IATs, it stashed away the address of the EXE's IAT into a global variable (g_pFirstImportDesc). At shutdown time, ReportProfileResults uses this pointer to walk through the imports section again. For each imported DLL, it retrieves the address of the DLL's first IAT entry. If this is an IAT that I've redirected, the first pointer in the IAT should point to the first of the DLPD_IAT_STUB stubs allocated for that DLL. Of course, the code does some sanity checking to ensure that this is the case. If something doesn't look right, DelayLoadProfileDLL ignores that particular imported DLL. Generally though, everything looks fine, and the first IAT entry points to my stubs. The code then iterates through all the stubs for the DLL. At each stub, the value of the stub's count field is added to a running total for the DLL. When the iteration completes, ReportProfileResults formats a string with the name of the DLL and how many calls were made through the stubs. The code uses OutputDebugString to broadcast its findings. Loading and Injection The program that loads your EXE and injects DelayLoadProfileDLL.DLL is called—you guessed it—DelayLoadProfile.EXE (the source code is available from the MSJ Web site at http://www.microsoft.com/msj). This code mainly drives the CDebugInjector class, which I'll describe shortly. Function main obtains the target EXE's command line and passes it to CDebugInjector::LoadProcess. If the process is created successfully, function main tells CDebugInjector which DLL it wants injected. In this case, it's DelayLoadProfileDLL.DLL, which should be located in the same directory as DelayLoadProfile.EXE. The last step before letting the target run wild is to call CDebugInjector::SetOutputDebugStringCallback. When DelayLoadProfileDLL reports its results via OutputDebugString, CDebugInjector sees them and passes them to the callback you registered. This callback just printfs the strings to the console. Finally, function main calls CDebugInjector::Run. This call lets the target process begin and, when the time is right, injects the DLL into it.  Figure 3 shows The CDebugInjector class. This is where all the good stuff happens. CDebugInjector::LoadProcess creates the specified process as a debugee process. The ramifications of running as a debugee process have been discussed in many articles and in the MSDN documentation, so I won't go into all the details here. For the purposes of this column, it's sufficient to say that the debugger process (in this case, DelayLoadProfile) has to enter a loop that calls WaitForDebugEvent and ContinueDebugEvent until the debugee terminates. Every time WaitForDebugEvent returns, something has happened in the debugee. This might be an exception (including break—points), a DLL load, a thread creation, or other event. The WaitForDebugEvent documentation covers all the events that might occur. The CDebugInjector::Run method contains the code for this loop. So how does running the target process as a debugee help you inject a DLL? A debugger process has excellent control over the debugee process's execution. Every time a significant event occurs in the debugee, it is suspended until the debugger calls ContinueDebugEvent. Knowing this, a debugger process can add code to the debugee's address space and temporarily change the debugee's registers so that the added code executes. In more specific terms, CDebugInjector synthesizes a small code stub that calls LoadLibrary. The DLL name parameter to LoadLibrary points to the name of the DLL to inject. CDebugInjector writes the stub (and the associated DLL name) to the debugee's address space. It then calls SetThreadContext to change the debugee's instruction pointer (EIP) to execute the LoadLibrary stub. All of this dirty work occurs within the CDebugInjector::PlaceInjectionStub method. Immediately following the LoadLibrary call in the stub is a breakpoint instruction (INT 3). This stops the debugee and gives control back to the debugger process. The debugger then uses SetThreadContext again to restore the instruction pointer and other registers to their original values. Another call to ContinueDebugEvent and the debugee is on its way with the DLL injected, none the wiser that anything has happened. If you don't think too hard, this injection process doesn't sound too messy. Nonetheless, a few interesting problems crop up that complicate things. For example, when is the proper time to create the stub code and redirect control to it? You can't do this immediately after the CreateProcess call because, among other reasons, the imported DLLs haven't been mapped into memory at this point and the EXE's IAT hasn't been fixed up by the Win32 loader. In other words, it's too early. The solution I ultimately decided on was to let the debugee run until it encounters its first breakpoint. Then I set a breakpoint of my own at the entry point of the EXE. When this second breakpoint triggers, CDebugInjector knows that DLLs in the target process (including KERNEL32.DLL) have initialized, but no code in the EXE has run. This is the perfect time for injecting DelayLoadProfileDLL.DLL. Incidentally, where does the first breakpoint come from? By definition, a Win32 process that's being debugged calls DebugBreak (also known as INT 3) very early in its execution. In my ancient APISPY32 code, I used the initial DebugBreak as the occasion to do the injection. Unfortunately in Windows 2000, this DebugBreak occurs before KERNEL32.DLL is initialized. Thus, CDebugInjector sets its own breakpoint to go off when the EXE is about to get control, and thus knows that KERNEL32.DLL has been initialized. Earlier, I mentioned a breakpoint that occurs after the LoadLibrary call returns. This is a third breakpoint for CDebugInjector to handle. All of the mechanics for handling the different breakpoints can be seen in CDebugInjector::HandleException. Another interesting problem to address with DLL injection is where to write the LoadLibrary stub. Under Windows NT 4.0 and later you can allocate space in another process with VirtualAllocEx, so I took that route. That leaves out Windows 9x, which doesn't support VirtualAllocEx. For this scenario, I took advantage of a unique property of Windows 9x memory-mapped files. These files are visible in all address spaces, and at the same address. I simply create a small memory-mapped file using the system page file as backing, and blast the LoadLibrary stub into it. The stub is implicitly accessible in the debugee process. For the details, see the code listing for CDebugInjector::GetMemoryForLoadLibraryStub at the link at the top of this article. Using DelayLoadProfile DelayLoadProfile is a command-line program that writes its results to standard output. From a command prompt, run DelayLoadProfile, specifying the target program and any arguments it needs, such as:
DelayLoadProfile notepad c:/autoexec.bat
Here are the results of running DelayLoadProfile against CALC.EXE from Windows 2000 Release Candidate 2:
[d:/column/col66/debug]delayloadprofile calc
DelayLoadProfile: 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
I simply started CALC and immediately shut it down. Note that SHELL32.DLL and ADVAPI32.DLL both had no calls to them. These two DLLs are prime candidates for CALC to DelayLoad. You may be wondering why CALC loads SHELL32.DLL, yet doesn't call it. It would be easy enough to run DumpBin /IMPORTS or Depends.EXE against CALC. In doing so, you'd see that the only function CALC imports from SHELL32.DLL is ShellAboutW. Simply put, unless you select the Help | About Calculator menu item in CALC, it's a complete waste of time and memory to load SHELL32.DLL. This is a fabulous example of where /DELAYLOAD can really show its worth. Incidentally, SHELL32.DLL implicitly links against SHLWAPI.DLL and COMCTL32.DLL—two additional DLLs that are brought into memory and initialized for no reason. Just because DelayLoadProfile reports that a DLL is receiving few or no calls at all doesn't mean you should automatically DelayLoad it. Be sure to consider whether one of your implicitly linked DLLs also links against the DLL you're considering using DelayLoad with. If this is the case, it's not worth using /DELAYLOAD in your EXE since the DLL is still going to be loaded and initialized because of some other dependency. Depends.EXE from the Platform SDK is a great tool for quickly determining the scope of a DLL's usage. Another thing to consider when using DelayLoadProfile is how much of your app you'll exercise during your test. Obviously, if you exercise all aspects of your app, all the DLLs you import in the EXE will be invoked. Personally, I think minimal load time is a good target to shoot for. This might mean just starting your program and then closing it down. By spreading the work of loading and initializing your DLLs throughout your application as it runs, you can speed the initial load sequence. Users often subjectively judge the speed of your application by its startup time. I've found a few DLLs that will benefit from using /DELAYLOAD. As you saw earlier, SHELL32.DLL is one of them. Another is WINSPOOL.DRV, which is used for printing support. Since most users don't print frequently, it's a good candidate, as are OLE32.DLL and OLEAUT32.DLL. In addition, a variety of programs use COM and OLE in some minimal capacity, making those DLLs possible candidates, too. For example, the Windows 2000 CDPLAYER.EXE links against OLE32.DLL and the CreateStreamOnHGlobal API. Yet in ordinary usage, I didn't observe this function being called. DelayLoadProfile is not without its faults (literally). While I've tested it successfully with a large number of applications, you may still run into the occasional program that doesn't work so well when DelayLoadProfileDLL interfaces with its IAT. Trying to find and locate all these odd scenarios is beyond the scope of this column. However, if you locate and fix one of these problems, please let me know. I may update DelayLoadProfile at some future date. I know that programs that import MFC42.DLL and MFC42U.DLL can crash with DelayLoadProfile. For that reason I've provided an escape hatch. In DelayLoadProfileDLL.CPP it's the IsModuleOKToHook function. I've placed MFC42.DLL, MFC42U.DLL, and KERNEL32.DLL in it. (You can't use /DELAYLOAD with KERNEL32.DLL anyhow, so it's no loss.) If a particular DLL seems to be giving you problems, first try adding it to IsModuleOKToHook. I hope DelayLoadProfile's ease of use will inspire you to tune your applications to make use of /DELAYLOAD. I certainly had a good time updating some classic code, and I'd enjoy hearing your success stories, too. Have a suggestion for Under The Hood? Send it to Matt at matt@wheaty.net or http://www.wheaty.net. 
0 0