MicrosoftVisualC++6.0提供了一个出色的新特性,它能够使DLL的操作变得更加容易。这个特性称为延迟加载DLL。延迟加载的DLL是个隐含链接的DLL,它实际上要等到你的代码试图引用DLL中包含的一个符号时才进行加载。延迟加载的DLL在下列情况下是非常有用的:>>如果你的应用程序使用若干个DLL,那么它的初始化时间就比较长,因为加载程序要将所有需要的DLL映射到进程的地址空间中。解决这个问题的方法之一是在进程运行的时候分开加载各个DLL。延迟加载的DLL能够更容易地完成这样的加载。>>如果调用代码中的一个新函数,然后试图在老版本的系统上运行你的应用程序,而该系统中没有该函数,那么加载程序就会报告一个错误,并且不允许该应用程序运行。你需要一种方法让你的应用程序运行,然后,如果(在运行时)发现该应用程序在老的系统上运行,那么你将不调用遗漏的函数。例如,一个应用程序在Windows2000上运行时想要使用PSAPI函数,而在Windows98上运行想要使用ToolHelp函数(比如Process32Next)。当该应用程序初始化时,它调用GetVersionEx函数来确定主操作系统,并正确地调用相应的其他函数。如果试图在Windows98上运行该应用程序,就会导致加载程序显示一条错误消息,因为Windows98上并不存在PSAPI.dll模块。同样,延迟加载的DLL能够使你非常容易地解决这个问题。我花费了相当多的时间来检验VisualC++6.0中的延迟加载DLL特性,必须承认,Microsoft在实现这个特性方面做了非常出色的工作。它提供了许多特性,并且在Windows98和Windows2000上运行得都很好。下面让我们从比较容易的操作开始介绍,也就是使延迟加载DLL能够运行。首先,你象平常那样创建一个DLL。也要象平常那样创建一个可执行模块,但是必须修改两个链接程序开关,并且重新链接可执行模块。下面是需要添加的两个链接程序开关:/Lib:DelayImp.lib/DelayLoad:MyDll.dllLib开关告诉链接程序将一个特殊的函数--delayLoadHelper嵌入你的可执行模块。第二个开关将下列事情告诉链接程序:>>从可执行模块的输入节中删除MyDll.dll,这样,当进程被初始化时,操作系统的加载程序就不会显式加载DLL。>>将新的DelayImport(延迟输入)节(称为.didata)嵌入可执行模块,以指明哪些函数正在从MyDll.dll输入。>>通过转移到对--delayLoadHelper函数的调用,转换到对延迟加载函数的调用。当应用程序运行时,对延迟加载函数的调用实际上是对--delayLoadHelper函数的调用。该函数引用特殊的DelayImport节,并且知道调用LoadLibrary之后再调用GetProcAddress。一旦获得延迟加载函数的地址,--delayLoadHelper就要安排好对该函数的调用,这样,将来的调用就会直接转向对延迟加载函数的调用。注意,当第一次调用同一个DLL中的其他函数时,必须对它们做好安排。另外,可以多次设定/delayLoad链接程序的开关,为想要延迟加载的每个DLL设定一次开关。好了,整个操作过程就这么简单。但是还应该考虑另外两个问题。通常情况下,当操作系统的加载程序加载可执行模块时,它将设法加载必要的DLL。如果一个DLL无法加载,那么加载程序就会显示一条错误消息。如果是延迟加载的DLL,那么在进行初始化时将不检查是否存在DLL。如果调用延迟加载函数时无法找到该DLL,--delayLoadHelper函数就会引发一个软件异常条件。可以使用结构化异常处理(SEH)方法来跟踪该异常条件。如果不跟踪该异常条件,那么你的进程就会终止运行(SEH将在第23、24和25章中介绍)。当--delayLoadHelper确实找到你的DLL,但是要调用的函数不在该DLL中时,将会出现另一个问题。比如,如果加载程序找到一个老的DLL版本,就会发生这种情况。在这种情况下,--delayLoadHelper也会引发一个软件异常条件,对这个软件异常条件的处理方法与上面相同。下一节介绍的示例应用程序显示了如何正确地编写SEH代码以便处理这些错误。你会发现代码中有许多其他元素,这些元素与SEH和错误处理毫无关系。但是这些元素与你使用延迟加载的DLL时可以使用的辅助特性有关。下面将要介绍这些特性。如果你不使用更多的高级特性,可以删除这些额外的代码。如你所见,VisualC++开发小组定义了两个软件异常条件代码,即VcppException(ERROR_SEVERITY_ERROR、ERROR_MOD_NOT_FOUND)和VcppException(ERROR_SEVERITY_ERROR、ERROR_PROC_NOT_FOUND)。这些代码分别用于指明DLL模块没有找到和函数没有找到。我的异常过滤函数DelayLoadDllExceptionFilter用于查找这两个异常代码。如果两个代码都没有找到,过滤函数将返回EXCEPTION_CONTINUE_SEARCH,这与任何出色的过滤函数返回的值是一样的(对于你不知道如何处理的异常代码,请不要随意删除)。但是如果这两个代码中的一个已经找到,那么--delayLoadHelper函数将提供一个指向包含某些辅助信息的DelayLoadInfo结构的指针。在VisualC++的DelayImp.h文件中,DelayLoadInfo结构定义为下面的形式:typedef struct DelayLoadInfo{ DWORD cb;//sizeofstruct PCImgDelayDescr p1dd;//Rawdata(everythingisthere) FARPROC* ppfn;// Pointstoaddressoffunctiontoload LPCSTR szDll;//Nameofdll DelayLoadProc dlp;//Nameorordinalofprodure HMODULE hmodCur;//hInstanceofloadedlibrary FARPROC pfnCur;//Actualfunctionthatwillbecalled DWORD dwLastError;//Errorreceived}DelayLoadInfo,*PDelayLoadInfo;这个数据结构是由--delayLoadHelper函数来分配和初始化的。在该函数按步骤动态加载DLL并且获得被调用函数的地址的过程中,它将填写该结构的各个成员。在SEH结构的内部,成员szDll指向你要加载的DLL的名字,想要查看的函数则在成员dlp中。由于可以按序号或名字来查看各个函数,因此dlp成员类似下面的样子:typedef struct DelayLoadProc{ BOOL fImportByName; union{ LPCSTRszProcName; DWORDdwOrdinal; }}DelayLoadProc;如果DLL已经加载成功,但是它不包含必要的函数,也可以查看成员hmodCur,以了解DLL被加载到的内存地址。也可以查看成员dwLastError,以了解是什么错误导致了异常条件的引发。不过对于异常过滤函数来说,这是不必要的,因为异常代码能够告诉你究竟发生了什么问题。成员pfnCur包含了需要的函数的地址。在过滤函数中它总是置为NULL,因为--delayLoadHelper无法找到该函数的地址。在其余的成员中,cb用于确定版本,pidd指向嵌入模块中包含延迟加载的DLL和函数的节,ppfn是函数找到时,函数的地址应该放入的地址。最后两个成员供--delayLoadHelper函数内部使用。它们有着超高级的用途,现在还没有必要观察或者了解这两个成员。到现在为止,已经讲述了如何使用延迟加载的DLL和正确解决错误条件的基本方法。但是Microsoft的延迟加载DLL的实现代码超出了迄今为止我已讲述的内容范围。比如,你的应用程序能够卸载延迟加载的DLL。假如你的应用程序需要一个特殊的DLL来打印一个文档,那么这个DLL就非常适合作为一个延迟加载的DLL,因为大部分时间它是不用的。不过,如果用户选择了Print命令,你就可以调用该DLL中的一个函数,然后它就能够自动进行DLL的加载。这确实很好,但是,当文档打印后,用户可能不会立即打印另一个文档,因此可以卸载这个DLL,释放系统的资源。如果用户决定打印另一个文档,那么DLL就可以根据用户的要求再次加载。若要卸载延迟加载的DLL,必须执行两项操作。首先,当创建可执行文件时,必须设定另一个链接程序开关(/delay:unload)。其次,必须修改源代码,并且在你想要卸载DLL时调用--FUnloadDelayLoadedDLL函数:BOOL _FUnloadDelayLoadedDll(PCSTRszDll);/Delay:unload链接程序开关告诉链接程序将另一个节放入文件中。该节包含了你清除已经调用的函数时需要的信息,这样它们就可以再次调用--delayLoadHelper函数。当调用--FUnloadDelayLoadedDll时,你将想要卸载的延迟加载的DLL的名字传递给它。该函数进入文件中的未卸载节,并清除DLL的所有函数地址,然后--FUnloadDelayLoadedDll调用FreeLibrary,以便卸载该DLL。下面要指出一些重要的问题。首先,千万不要自己调用FreeLibrary来卸载DLL,否则函数的地址将不会被清除,这样,当下次试图调用DLL中的函数时,就会导致访问违规。第二,当调用--FUnloadDelayLoadedDll时,传递的DLL名字不应该包含路径,名字中的字母必须与你将DLL名字传递给/DelayLoad链接程序开关时使用的字母大小写相同,否则,--FUnloadDelayLoadedDll的调用将会失败。第三,如果永远不打算卸载延迟加载的DLL,那么请不要设定/Delay:unload链接程序开关,并且你的可执行文件的长度应该比较小。最后,如果你不从用/Delay:unload开关创建的模块中调用--FUnloadDelayLoadedDll,那么什么也不会发生,--FUnloadDelayLoadedDll什么操作也不执行,它将返回FALSE。延迟加载的DLL具备的另一个特性是,按照默认设置,调用的函数可以与一些内存地址相链接,在这些内存地址上,系统认为函数将位于一个进程的地址中(本章后面将介绍链接的问题)。由于创建可链接的延迟加载的DLL节会使你的可执行文件变得比较大,因此链接程序也支持一个/Delay:nobind开关。因为人们通常都喜欢进行链接,因此大多数应用程序不应该使用这个链接开关。延迟加载的DLL的最后一个特性是供高级用户使用的,它真正显示了Microsoft的注意力之所在。当--delayLoadHelper函数执行时,它可以调用你提供的挂钩函数。这些函数将接收--delayLoadHelper函数的进度通知和错误通知。此外,这些函数可以重载DLL如何加载的方法以及如何获取函数的虚拟内存地址的方法。若要获得通知或重载的行为特性,必须对你的源代码做两件事情。首先必须编写类似清单20-1所示的DliHook函数那样的挂钩函数。DliHook框架函数并不影响--delayLoadHelper函数的运行。若要改变它的行为特性,可启动DliHook函数,然后根据需要对它进行修改。接着将函数的地址告诉--delayLoadHelper。在DelayImp.lib静态链接库中,定义了两个全局变量,即--pfnDliNotifyHook和--pfnDliFailureHook。这两个变量均属于pfnDliHook类型:typedef FARPROC( WINAPI* PfnDllHook)( unsigneddllNotify, PDelayLoadInfopdl1);如你所见,这是个数据类型的函数,与我的DliHook函数的原型相匹配。在DelayImp.lib文件中,两个变量被初始化为NULL,它告诉--delayLoadHelper不要调用任何挂钩函数。若要使你的函数被调用,必须将这两个函数中的一个设置为挂钩函数的地址。在我的代码中,我只是将下面两行代码添加到全局作用域:PfnDllHook_pfnDllNotifyHook=DllHook;PfnDllHook_pfnFailureHook=DllHook;如你所见,--delayLoadHelper实际上是与两个回调函数一道运行的。它调用一个函数以便报告通知,调用另一个函数来报告失败情况。由于这两个函数的原型是相同的,而第一个参数dliNotify告诉为什么调用这个函数,因此我总是通过创建单个函数并将两个变量设置为指向我的一个函数,使我的工作变得简单一些。VisualC++6.0的延迟加载DLL的新特性非常出色,许多编程人员几年前就希望使用这个特性。可以想像许多应用程序(尤其是Microsoft的应用程序)都将充分利用这个特性
延迟加载DLL
最新推荐文章于 2024-08-07 09:47:57 发布