延迟加载动态库 delay-load Dlls
该文详细介绍了VC6中的一个新功能:延迟加载动态库(delay-load Dlls),虽然写于1998年,但在今天看来也没有过时。原文还包含一段其它的内容,我没翻译。另外,MSDN里也有很多相关的说明值得一阅,特别地,在delayhlp.cpp中,有__delayLoadHelper2和__FUnloadDelayLoadedDLL2的实现源码,不可不看。
声明: 严格地讲,这不是一篇翻译,我更愿意把它叫作“意译”。即,我可以保证文章内容与原文完全一致,但我不保证与原文表达完全一致。特别地,对常用术语、我拿不准的句子,或者我懒得写汉字的话,我会直接引用英文。
Question:
我的应用程序中需要使用几个DLL,这些DLL非常影响load/initialization性能。我想我已经用了每一种可能的方法来提高load-time性能,包含重定位基址(rebasing)和绑定(binding)DLLs。我非常希望能够延迟加载我的动态库,直至它们被实际使用。我知道我可以通过使用显示链接(explicit linking)(调用LoadLibrary和GetProcAddress)来实现它,但是这意味着我必须跟踪是否这些DLLs已经被加载,写这样的代码非常令人厌烦(tedious)。难道没有一种简单的方法可以延迟加载动态库,直到实际调用它们中的一个函数时吗?
Kristin Trace
我正在开发一个需要同时运行在Windows 9x下和Windows NT下的应用程序。我的程序检查是否一个指定的进程运行在系统上。当我的程序运行在Windows NT上时,我使用PSAPI函数EnumProcessModules和GetModuleFileNameEx。而在Windows 9x下,我使用Toolhelp函数CreateToolhelp32Snapshot,Process32First, Process32Next等等。当我的程序初始化时,我调用GetVersionEx函数来决定我的程序正在哪一个OS上运行,然后只调用合适的函数集。我的程序编译链接都没问题,但是当我运行在Windows NT上时,我得到下面的信息:"The procedure entry point Process32Next could not be located in the dynamic link library KERNEL32.dll." 同样的,当我运行在Windows 9x下时,我得到针对PSAPI.DLL的类似信息。你能告诉我怎样才能去掉这样的运行时错误吗?
Conor Kiernan
Answer:
这两个问题都可以通过VC6提供的一个新功能:延迟加载动态库(delay-load DLLs)解决。一个延迟加载的动态库是隐式链接的(implicitly linked),但是启动程序(loader)不会加载DLL直到你的代码中实际调用了DLL里的一个函数。这种方法可以提高Kristin的初始化性能,因为启动程序没有在一开始就做所有的事情。对于Conor来说,那些运行时错误也会
消失了,因为他的程序只会加载需要的DLL。
我花了很多时间来玩VC6的这个新功能,而且我必须承认微软做得非常漂亮。Delay-load DLLs提供了很多功能,并且在Windows 9x和Windows NT下都工作得非常好。让我们从简单的部分开始-只是让它运行起来。
要使delay-load DLLs在VC6下运行起来,首先象平常一样创建你的DLL,然后也象平常一样创建你的可执行程序。但是你必须改变两个链接器开关(linker switches),然后重新链接可执行程序。下面是你需要加的两个链接开关:
/Lib:DelayImp.lib
/DelayLoad:MyDll.dll
'/Lib'告诉链接器,嵌入一个特殊的__delayLoadHelper函数到可执行文件中。第二个开关告诉链接器下面几件事情:
(1)把MyDll.dll从执行文件的表中移走,这个表用于告诉OS loader当进程初始化时隐式地加载动态库。
(2)在执行文件中嵌入一个特殊的表,用于指示MyDll中有哪些函数。
(3)通过把调用跳转到__delayLoadHelper,来重解析(resolve)对延迟加载的函数的调用。
当应用程序运行时,对延迟加载的函数的调用实际上是在调用__delayLoadHelper函数。这个函数引用我前面提到的那个特殊的表,并且会调用LoadLibrary和GetProcAddress。一旦得到了被延迟加载的函数的地址,__delayLoadHelper会修复(fixes up)对这个函数的调用,所以以后再调用就是直接调它了(译注:即不再调用__delayLoadHelper了)。但要注意的是,当你第一次调这个DLL的其他函数时,还是要先修复(译注:即第一次调DLL中的其他函数时,还是调用__delayLoadHelper)。另外,你可以指定/DelayLoad开关多次,一次指定一个DLL。
Error Conditions
OK,就这么多。非常简单!但有两个问题你必须要知道。正常情况下,当OS loader加载你的程序时,它会试图加载需要的动态库。如果一个DLL不能被加载,启动程序会弹出一个消息框,象Figure 1这样。但是对延迟加载的动态库,初始化时不检查DLL是否存在。所以,如果当调用一个延迟加载的函数时,这个动态库不到的话,__delayLoadHelper函数会raise一个软件异常。你可以使用SEH结构捕获到这个异常,并且使你的程序继续运行。如果你不捕获这个异常,你的程序会被中止。
Figure 1 Message Box
另一个问题出现在__delayLoadHelper到了你的动态库,但你试图调用的函数却不在该DLL时。如果loader到的是一个老版本的DLL,这种情况就可能发生。这种情况下,__delayLoadHelper会raise另一
个软件异常,并运用同样的规则。Figure 2中的代码说明了怎么写合适的SEH代码来处理这些错误。当你查看这些代码时,你会注意到大量与SEH和错误处理无关的部分。这些代码是用于delay-load DLLs的其他
特的。我很快就会描述这些高级的功能。如果你不使用这些功能,你可以删掉这些额外的代码。
正如你看到的,VC小组定义了两个软件异常代码:VcppException(ERROR_ SEVERITY_ERROR, ERROR_MOD_NOT_FOUND)和VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND),分别用来描述DLL没被到和function没被到两种情况。(译注:在delayimp.h中定义
#define FACILITY_VISUALCPP ((LONG)0x6d)
#define VcppException(sev,err) ((sev) | (FACILITY_VISUALCPP<<16) | err))
我的异常过滤(filter)函数DelayLoadDllExceptionFilter会检查这两种情况。如果这两种异常都没有发生,我的filter会返回EXCEPTION_CONTINUE_ SEARCH,就象其它任何好的filter做的一样(永远不要swallow任何你不知道如何处理的异常)。如果这两个异常代码中的任何一个被抛出(throw),__delayLoadHelper函数会提供一个指向DelayLoadInfo结构的指针,它包含了一些额外的信息。这个DelayLoadInfo结构在VC中的DelayImp.h中定义,如下所示:
typedef struct DelayLoadInfo {
DWORD cb; // size of structure
PCImgDelayDescr pidd; // raw form of data // (everything is there)
FARPROC * ppfn; // points to address of // function to load
LPCSTR szDll; // name of dll
DelayLoadProc dlp; // name or ordinal of // procedure
HMODULE hmodCur; // the hInstance of the // library we have loaded
FARPROC pfnCur; // the actual function that // will be called
DWORD dwLastError; // error received (if an // error notification)
} DelayLoadInfo, * PDelayLoadInfo;
这个结构由__delayLoadHelper函数分配和初始化。当这个函数运行至动态加载DLL和得到被调函数的
指针时,它填充这个结构的成员。在你的SEH filter里,'szDll'成员指向试图加载的DLL的名字,'dlp'成员包含你试图查的函数的信息。因为函数可以通过序号(ordinal)或通过名字查,所以'dlp'成员如下所示:
enum函数typedef struct DelayLoadProc {
BOOL fImportByName;
union {
LPCSTR szProcName;
DWORD dwOrdinal;
};
} DelayLoadProc;
如果DLL被成功加载,但没有包含需要的函数,你可以通过'hmodCur'成员查看DLL加载的内存地址。另外,如果你想知道哪一个Win32错误导致了异常的发生,可以查看'dwLastError'成员。对一个异常filter
来说,这可能是不需要的,因为异常代码就能告诉你发生了什么。'pfnCur'包含了函数的地址。如果发生异常的话,这个值总为NULL,因为__delayLoadHelper不到函数的地址。
剩下的几个成员里,'cb'用于版本检查的(译注:Win32 API里,喜欢用cb,即数据结构的大小来判断用的是哪一个版本的数据结构),'pidd'指向EXE文件中包含了延迟加载的动态库和
函数的table。'pidd'和'ppfn'被__delayLoadHelper内部使用;它们是用于那些非常高级的应用的,你不是一定需要理解它们的。
Unloading a Delay-load DLL
到现在为止,我已经解释了使用延迟加载动态库的基本方法和如何从错误中恢复。然而,微软并没有就此为止。他们给你的应用程序增加了卸载(unload)一个delay-load DLL的能力。例如,你的程序可能需要一个特殊的DLL,用于打印用户的文档。这个DLL非常适合作为delay-load DLL,因为大多数情况下,它可能不会被使用。但是,如果用户选择了打印选项,你就需要调用这个DLL的一个函数,并且它会被自动加载。这非常好,但是等文档打印完了,用户未必会立即打印另一个文档,那么你就可以unload这个DLL,释放系统资源。如果用户决定打印另一个文档,这个DLL将被重新加载。
为了unload a delay-load DLL,你必须做两件事情。首先,当你编译时,你必须指定一个额外的链接开
关(/Delay: unload)。然后,你必须修改你的源代码,并且在你希望DLL被卸载的地方加上对__FUnloadDelayLoadedDLL的调用:
BOOL __FUnloadDelayLoadedDLL(LPCSTR szDll);
'/Delay:unload'告诉linker在文件里放上另一个table。这个表包含了重置(reset)你已经调用过的函数所需要的信息,这样这些函数会重新调用__delayLoadHelper函数(译注:原本这些函数会直接调用)。当你调用__FUnloadDelayLoadedDLL时,你传递你想要卸载的DLL的名字。然后这个函数会到文件中的unload table,并且reset所有函数的地址。最后,它会调用FreeLibrary卸载DLL。
我要指出几个潜在的问题。
(1)要保证你自己不会调用FreeLibrary,因为那样的话,函数的地址不会被reset,当你下次试图调用一个函数时,会导致access violation。
(2)当你调用__FUnloadDelayLoadedDLL时,DLL的名字不能包含路径,并且字符的大小写必须与你在'/DelayLoad'里指定的DLL的名字一样,否则__FUnloadDelayLoadedDLL会失败。
(3)如果你永远不会unload一个delay-load DLL,不要指定'/Delay:unload'。这样,你的可执行文件会小一些。
(4)如果你在一个没有指定'/Delay:unload'的模块中调用了__FUnloadDelayLoadedDLL,什么也不会发生;__FUnloadDelayLoadedDLL什么也不做,直接返回FALSE。
Other Features
delay-load DLls的另一个特是:缺省地,the functions that you call are bindable to a memory address where the system thinks the function will be in a process's address(译注:这一句翻译不好)。我不会在这个栏目里详细介绍binding,因为在MSDN知识库(Knowledge Base)里有大量相关的信息。然而,binding一个模块可以使它加载明显变快,因为强烈推荐这么做。如果你不熟悉binding,你应该好好研究它,从速度
和内存使用两方面来提高你的程序的性能。因为创建bindable delay-load DLL tables会使你的可执行文件更大,所以linker也提供了一个'/Delay:nobind'开关。大多数程序不应该使用这个开关,因为binding通常是首选的。
delay-load DLls的最后一个特是针对高级用户的,它实在是显示了微软对细节的关注。当__delayLoadHelper函数执行时,它可以调用你提供的hook函数。这些函数可以接收__delayLoadHelper提供的进度和错误的通知。另外,这些函数可以重写(override)DLL如何被加载和函数的内存地址如何被获得的过程。
为获得通知(notification)或重写(override)行为,你必须在你的代码里做两件事情。首先,你必须写一个hook函数。这个函数必须象Figure 2中的DliHook函数一样。DliHook框架函数没有影响__delayLoadHelper的操作。要改变它的行为的话,你可以以DliHook做为起点,然后加上必要的修改。一旦你写完这个函数,你还需要告诉__delayLoadHelper你的Hook函数的地址。在DelayImp.LIB静态链接库里,定义了两个全局变量__pfnDliNotifyHook 和__pfnDliFailureHook(译注:其实不是定义,而是用extern做的外部引用。真正的定义应该是在你自己的代码里)。这两个变量都是PfnDliHook类型:
typedef FARPROC (WINAPI *PfnDliHook)(unsigned dliNotify, PDelayLoadInfo pdli);
正如你看到的,这是一个函数类型定义,并且与我的DliHook函数原型相匹配。在DelayImp.LIB里,这两个变量被初始化为NULL,即是告诉__delayLoadHelper不调用任何hook函数。所以为了让你的hook函数被调用,你必须调置这两个变量中的一个为你的hook函数的地址。在我的代码里,我简单地在全局域内加了下面两行:
PfnDliHook __pfnDliNotifyHook = DliHook;
PfnDliHook __pfnDliFailureHook = DliHook;
正如你所见,__delayLoadHelper实际上是和两个Callback函数一起工作。__delayLoadHelper 调用其
中一个发出通知,调用另一个发出失败通知。因为这两个函数的原型一样,并且第一个参数'dliNotify'就可以告诉你函数被调用的原因,所以为了使我的生活简单点,我就只写了一个函数,然后简单地设置这两个变量都指向这个函数。
Wrap-up
VC6的这个delay-load DLL新功能非常的酷,而且我知道很多开发者很多年前就已经在渴望这个功能了。我可以想象在未来会有很多应用(特别是微软的应用程序)会利用到这个功能。Try it out for yourself and see how it can help the performance of your apps.
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论