zoukankan      html  css  js  c++  java
  • 回炉重造之重读Windows核心编程-020-DLL高级技术

    第20章 DLL高级技术

    20.1 DLL模块的显示加载和符号链接

    如果线程要调用DLL中的函数,DLL的文件映像就必须映射到调用线程的进程地址空间中。有两种方式:

    1. 让线程只引用DLL中包含的符号,这样当应用程序启动时,加载程序就能够隐含加载所需要的DLL。
    2. 第二张方法是指加载应用程序运行时加载DLL并显示链接到需要的输入符号。这种情况下,应用程序可以在运行时决定调用什么函数。线程将DLL加载到进程的地址空间中后,就可以使用其中的所有函数。

    20.1.1 显式加载DLL模块

    DLL可以被进程中的线程映射到进程的地址空间,只需调用下面的函数之一:

    HINSTANCE LoadLibrary(PCTSTR pszDLLPathName);
    HINSTANCE LoadLibraryEx(
      				PCTSTR pszDLLPathName, 
    					HANDLE hFile,
    					DWORD  dwFlags);
    

    这两个函数用于设法找出用户系统上的DLL文件,并映射到进程的地址空间中,它们的返回值HINSTANCE代表文件映像的虚拟内存地址。如果返回NULL,那么代表不能映射到空间中,错误的原因需要调用GetLasterror函数去查询信息。

    LoadLibraryEx函数有额外的两个参数,hFile供未来使用,而dwFlags现在也必须是0,或者是DONT_RESOLVE_DLL_REFERENCES、LOAD_LIBRARY_AS_DATAFILE和LOAD_WITH_ALTERED_SEARCH_PATH等标志的组合。

    1. DONT_RESOLVE_DLL_REFERENCES:说明系统将DLL映射到进程的地址空间中。通常DLL被加载的时候,DllMain函数会触发。然而当这个标志被设定后,DllMain函数的触发就被忽略了。
    2. LOAD_LIBRARY_AS_DATAFILE:和上面的DONT_RESOLVE_DLL_REFERENCES标志相似,由于数据文件的属性,系统在加载DLL的时候就不打算执行DLL中的源代码了。
    3. LOAD_WITH_ALTERED_SEARCH_PATH:用于改变查找特定DLL的时候用使用的搜索算法。如果使用了这个标志,函数就按照下面的顺序搜索文件:
      1. pszDLLPathName参数中设定的路径;
      2. 进程的当前路径;
      3. Windows系统目录;
      4. Windows目录;
      5. PATH环境变量中列出的目录;

    20.1.2 显示卸载DLL模块

    当进程不再需要DLL的时候,DLL可以从进程的地址空间中显式地卸载掉,使用下面的函数:

    BOOL FreeLibrary(HINSTANCE hinstDll);
    

    hinstDll参数必须被传递,用来表示将要卸载的DLL,它是调用LoadLibrary(Ex)的返回值。也可以用下面的函数卸载DLL:

    VOID FreeLibraryAndExitThread(
    	HINSTANCE hinstDll,
    	DWORD	dwExitCode);
    

    这个函数在Kernel32.dll中实现,方式并不稀奇:

    VOID FreeLibraryAndExitThread(HINSTANCE hinstDll,	DWORD	dwExitCode)
    {
      FreeLibrary(hinstDll);
      ExitThread(dwExtcode);
    }
    

    这其实并不高明,而可能会带来很严重的问题。假如有一个DLL,而它是第一次被映射到进程的地址空间中的时候,DLL就会启动了一个线程。线程做完工作后就调用FreeLibrary,卸载DLL,然后立即调用ExitThread。

    但是如果分开调用FreeLibrary和ExitThread,又会有别的问题:FreeLibrary被调用后,包含ExitThread函数的DLL就被卸载了,ExitThread的调用又从何说起呢?这会引起访问违规的。

    而如果调用的是FreeLibraryAndExitThread,那么即使DLL被正常卸载,下一个被执行的执行已经在Kernel32.dll中,而不是在刚刚被卸载的DLL里。这就意味着线程可以继续执行了。

    实际使用时,加载和卸载DLL的这四个函数如果各自被重复调用多次后,影响的是其相对的引用计数的增减。引用计数的规则在这里同样有效。

    如果想确定DLL是否已经被加载,可以使用下面的函数:

    HINSTANC GetModuleHandle(PCTSTR pszModuleName);
    

    有了DLL的句柄HINSTANCE,你甚至可以获得DLL的完整路径:

    DWORD GetModuleFileName(
    	HINSTANCE hinstModule,
    	PTSTR     pszPathName,
    	DWORD		  cchPath);
    

    20.1.3 显示地链接到一个输出符号

    获取DLL中的一个符号地址,调用下面的函数就行:

    FARPROC GetProcAddress(
    	HINSTANCE hinstDll,
    	PCSTR			pszSymbolName);
    

    hinstDll是LoadLibrary(Ex)或者GetModuleHandle函数的返回值。参数pszSymbolName有两种形式。

    1. 其一是以0结尾的字符串,代表输出符号的地址的名字,要注意的是这个字符串的原始是PCSTR,这意味着函数接收的是ANSI字符串,你不能将Unicode字符串加给它。
    2. 其二是指明输出符号的序号值。这种用法是假设你知道输出符号对应的序号。由于Microsoft不推荐,所以这个方式不常用。

    第一种方式比较慢,这是因为系统需要进行字符串比较,并搜索要传递的字符串。还有这个函数的返回值是FARPROC,如果失败是返回NULL的。

    20.2 DLL的进入点函数

    一个DLL拥有单独的入口函数,系统会在不同的情况下调用这个函数。下面会分析这样做的原因。进入点函数类似下面的情况:

    BOOL WINAPI DllMain(HINSTANC hinstDll, DWORD fdwReason, pvoid fimpLoad) {
      switch(fdwReason) {
        case DLL_PROCESS_ATTACH:
          // The DLL is being mapped into the process's address space.
          break;
        case DLL_PROCESS_DETACH:
          // The DLL is being unmapped from the process's address space.
          break;
        case DLL_THREAD_ATTACH:
          // A Thread is beding created.
          break;
        case DLL_THREAD_DETACH:
          // A thread is exiting cleanly.
          break;
      }
      return TRUE; // Used only if  DLL_PROCESS_ATTACH
    }
    

    注意函数名是区分大小写的。写错的话这个进入点函数就不会被调用,其中的一些初始化的工作也不能执行了。

    hinstDll参数是DLL的实例句柄。和WinMain的hinstExe参数相似,标志的是DLL文件被映射到进程地址空间的虚拟内存地址。fimpLoad参数是个标志,如果DLL是隐式加载的,这个参数是个非零值;如果DLL是显示加载的,那么这个值就是0。

    fdwReason参数说明系统为何调用这个函数,它可以是DLL_PROCESS_ATTACH、DLL_PROCESS_DETACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH这4个值中的任何一个。

    DLL中的函数同样能被其他DLL调用。但是无论如何都不可以在DllMain函数中出现LoadLibrary(Ex),这会让DLL很致命地被循环调用。

    你的DllMain应该仅仅做些简单的初始化工作(设置本地存储器,创建内核对象和打开文件等),而避免调用User、Shell、ODBC、COM、RPC、和套接字函数(以及调用它们的函数),因为它们的DLL同样有可能会还没有初始化。

    对于全局或者静态的C++对象,也可能有同样的问题。

    20.2.1 DLL_PROCESS_ATTACH通知

    只有当DLL第一次被映射到进程的地址空间时,这个通知会被触发。如果DLL重复被这样做,操作系统只是递增DLL的使用计数,不再触发。这个通知可以用于一些关键的初始化操作,例如管理堆栈。而如果初始化取得成功后,返回值就是TRUE,否则就是FALSE。此后再接收到DLL_PROCESS_DETACH、DLL_THREAD_ATTACH或者DLL_THREAD_DETACH的话就会被忽略。

    这样的话,进程启动的整个过程中涉及到的DLL如果带有DLL_PROCESS_ATTACH的DllMain函数,那么这些DLL的DLL_PROCESS_ATTACH就不能返回FALSE,否则就会失败,地址空间中的DLL映像也会被卸载。

    20.2.2 DLL_PROCESS_DETACH通知

    DLL从进程的地址空间中被卸载时,系统将调用 DLL的DllMain函数,给它传递fdwReason
    的值DLL_PROCESS_DETACH。当DLL处理这个值时,它应该执行任何与进程相关的清除操
    作。例如管理堆栈。

    注意,如果因为操作系统中的某个线程调用了TerminateProcess而使得进程终止运行,那么系统是不会去调用带有DLL_PROCESS_DETACH值的DllMain函数的。这意味着很多资源的回收工作就被跳过了,数据就会损失。只有在万不得已之时,才能使用TerminateProcess。

    20.2.3 DLL_THREAD_ATTACH通知

    当在进程中创建线程时,系统要查看映射到该进程的地址空间中的DLL,并查看它们是否接收DLL_THREAD_ATTACH这个通知,因为这涉及到DLL对于线程的初始化操作。只有当所有的DLL都有机会处理这个通知的时候,系统才允许新线程开始执行新线程的线程函数 。

    在DLL被引射到进程的地址空间之时,这个空间中可能已经有很多其他的DLL了。这样的话新的DLL被映射进来之时,旧的DLL的DllMain函数中的DLL_THREAD_ATTACH通知是不会被触发的,只有新的DLL里的通知会。

    另外,系统是不会为进程的主线程调用DllMain中的DLL_THREAD_ATTATCH通知的,而是DLL_PROCESS_DETACH。

    20.2.4 DLL_THREAD_DETACH通知

    让线程终止的最佳方式是让它的线程函数返回,这样系统就可以利用ExitThread来撤销线程。但是线程并不会立即被撤销,而是取出已经映射进去的所有DLL,通知这些DLL的DllMain函数中的DLL_THREAD_DETACH,使得这些DLL使用的资源得以回收。

    注意,DLL能够阻止进程终止进程终止运行。例如,当DllMain接收到DLL_PROCESS_DETACH通知时,它就会进入一个无限循环。只有当每个DLL都已经完成对DLL_PROCESS_DETACH通知的处理之时,进程才会被终止运行。

    如果因为系统调用了TerminateThread而终止了线程,那么线程的DLL中有DLL_THREAD_DETACH的DllMain函数就不会得到调用。这意味着数据的丢失,所以只有在万不得已的情况下才能使用这个手段。

    20.2.5 顺序调用DllMain

    假如一个进程中有两个线程A和B以及一个DLL(叫做some.dll),现在A和B都要各自创建新的线程C和D,2个新线程都要通知some.dll中DllMain函数中的DLL_THREAD_ATTACH所在的代码。在这种情况下,C和D对DllMain的调用就是有顺序的了,就是如果C先访问了DllMain,那么D的访问就被操作系统暂停,直到C的访问结束,才轮到D访问。

    而这个特性通常会被忽略,如果C对DllMain访问的过程中,出现了死循环或者WaitForXXX,就造成死锁。

    20.2.6 DllMain与CC++运行时库

    DllMain也是被别的函数调用的,和main函数相似。这是因为DllMain有可能会导出一个C++类的对象,而这个类的初始化操作是在另一个DLL中,也就是CC++的运行时库中。值得一提的是这个运行时库一定是被静态链接进来的。调用DllMain函数的函数实际上是_DllMainCRTStartup,此时就有条件初始化运行时库、创建C++对象了。然后函数_DllMainCRTStartup就开始调用DllMain,DLL的工作开始了。

    当DLL收到DLL_PROCESS_DETACH通知的时候,DllMain函数又被调用。在DllMain返回后,上面提到的导出对象就会被销毁,CC++运行时库也会被卸载。

    如果DllMain函数没有被实现,那么可以使用CC++运行时库实现的代码:

    BOOL WINAPI DllMain(HINSTANCE instDll, DWORD dwReason, PVOID fimpLoad) {
      if (dwReason == DLL_PROCESS_ATTACH) {
        DisableThreadLibraryCalls(hinstDll);
      }
      return TRUE;
    }
    

    如果你不实现DllMain的话,系统就会使用C++运行时库的代码,假设你不关心DLL_THREAD_ATTACH和DLL_THREAD_DETACH这2个通知。为了提高性能,DisableThreadLibraryCalls函数会被调用。

    20.3 延迟加载DLL

    顾名思义,这个特性是让DLL可以在需要其中的符号的时候再被加载。它的用处很大:

    1. 如果你的应用程序使用了很多DLL,那么初始化它们的时间就比较长,因为需要把它们都加载到地址空间中。有了这个特性,应用程序启动的速度就会提高很多。
    2. 如果要在旧版本的系统上使用应用程序时,有可能会造成DLL中的符号缺失的问题。这个时候延迟加载DLL的特性就会变得很有用。

    要实现这个特性,需要的步骤有:

    1. 像平常一样创建一个DLL和一个可执行模块。
    2. 修改两个链接程序开关:
      1. /Lib:DelayImp.lib :告诉链接程序将一个特殊的函数 --delayLoadHeaper嵌入你的可执行模块。
      2. /DelayLoad:MyDll.dll
        1. 从可执行模块的输入节中删除MyDll.dll。这样在进程被初始化的时候,就不会显式地加载这个DLL。
        2. 将新的延迟输入节(DelayImport,称为.didata)嵌入可执行模块,以指明哪些函数正在从MyDll.dll中输入。
        3. 通过转移对--delayLoadHeaper函数的调用,转换到对延迟加载函数的调用。

    应用程序对延迟加载函数的调用实际上是对 --delayLoadHeaper函数的调用,这个函数可以知道调用LoadLibrary和GetProcAddrees。这个操作完成之后,将来调用函数就会转向延迟加载的调用。

    应用程序的启动也需要很多DLL。当缺少了这些DLL的其中一个,应用程序将无法启动,并显示一条错误消息。如果缺少的是延迟加载的DLL,应用程序就不检查它是否存在了。只会在 --delayLoadHeaper函数中引发一个软件异常条件,可以用SEH来追踪。如果不追踪在这个异常,进程将停止运行。

    DLL找到了,应用程序需要的函数可能会没有(因为版本之类的问题)。这样也会发生一个异常,处理的方式和上面相同。

    我们有两个异常条件代码可以用,分别用来指明这个异常是缺少DLL(VcppException(ERROR_SEVERITY_ERROR、ERROR_MOD_NOT_FOUND)还是缺少函数(VcppException(ERROR_SEVERITY_ERROR、ERROR_PROC_NOT_FOUND))。下面的异常处理函数DelayLoadDllExceptionFilter用于查找这两个代码,如果没找到过滤函数就返回EXCEPTION_CONTINUE_SEARCH。但是,如果其中一个找到了,那么--delayLoadHeaper函数将提供一个(DelayLoadInfo)结构体的指针,结构体的定义如下:

    typedef struct _DelayLoadInfo {
      DWORD           cb;           // Size of Structure
      PCImgDelayDescr pidd;         // Raw data (everything is here)
      FARPROC *       ppfn;         // Point to address of function to load
      LPCSTR          szDll;        // Name of dll
      DelayLoadProc   dlp;          // Name or ordinal of procedure
      HMODULE         hmodCur;      // hInstance of loaded library
      FARPROC         pfnCur;       // Actual function that will be called 
      DWORD           dwLastError   // Error received;
    }DelayLoadInfo, *PDelayLoadInfo;
    

    这个函数是由--delayLoadHelper函数来分配和初始化。该函数动态加载DLL并获得被调用函数的地址的过程中,它将填写这个结构的各个成员。成员szDll指向要加载的DLL的名字,要查看的函数则在dlp中。pfnCur可以让你了解DLL被加载到的内存地址,想知道到底发生了什么错误可以使用dwLastError。不过异常已经告诉了你到底发生了什么问题,这就不必要了。pfnCur是需要的函数的地址,不过它总是NULL,因为--delayLoadHelper无法找到该函数的地址。cb用于确定版本,pidd指向嵌入模块中包含延迟加载的DLL和函数的节。ppfn则是函数找到后应该放的地址。

    若要卸载延迟加载的DLL,必须执行两项操作:

    1. 当创建可执行文件时,必须设定另一个链接程序开关(/delay:unload)
    2. 修改源代码,在你想要卸载DLL时调用--FUnloadDelayLoadedDLL函数。
    BOOL __FUnloadDelayLoadedDLL(PCSTR szDll);
    

    /delay:unload链接程序开关告诉链接程序将另一个节放入文件中。该节包含了你清除已经
    调用的函数时需要的信息,这样它们就可以再次调用 --delayLoadHelper函数。当调用函数--FUnloadDelayLoadedDLL的时候,将要卸载的延迟加载的 DLL的名字传递给它。该函数进入文件的未卸载节,并清除DLL中的所有函数地址,然后__FUnloadDelayLoadedDLL调用FreeLibrary,以便卸载DLL。

    还有一些问题:

    1. 不要自己调用FreeLibrary来卸载DLL,否则函数的地址将不会被清除,这样当下次试图访问DLL中的函数时就会发生访问违规。
    2. 当调用--FUnloadDelayLoadedDLL传递的名字不应该包含路径,名字的大小写和传递给链接开关(/delay:unload)的参数相同,否则--FUnloadDelayLoadedDLL将会失败。
    3. 如果不打算卸载延迟加载的DLL,那么请不要设定这个开关。这样可执行文件的长度就会比较小。
    4. 如果 --FUnloadDelayLoadedDll没有被调用,那么什么都不会发生,函数--FUnloadDelayLoadedDll只会返回FALSE。

    最后一个特性,当--delayLoadHeaper函数执行时,它可以调用你提供的挂钩函数。这些函数将接收--delayLoadHeaper函数的进度通知和错误通知。此外,这些函数可以重载DLL如何加载的方法以及如何获取函数的虚拟内存地址的方法。

    要实现这个行为特性,必须对你的源代码最2件事情:

    1. 提供DliHook框架中类似的挂钩函数。
    2. 启动DliHook函数,对它进行修改。
    3. 将函数的地址告诉--delayLoadHeaper。

    在DelayImp.lib静态库中定义了两个全局变量,pfnDliNotifyHookpfnDliFailureHook。它们的类型相同:

    typedef FARPROC (WINAPI *PFNDLIHOOK)(
    	unsigned dliNotify,
    	PDelayLoadInfo pdli);
    

    在DelayImp.lib文件中这2个变量被初始化为NULL,它告诉–delayLoadHelper不要调用任何挂钩函数。若要让你的函数被调用,必须将这2个函数中的一个设置为挂钩函数的地址。

    --delayLoadHeaper实际上是和两个函数一道运行的。它调用一个函数以便通知,调用另一个函数来报告失败情况。由于它们的原型相同,所以可以通过创建单个函数并将两个变量设置为指向我的一个函数,使工作简单一些。

    20.4 函数转发器

    这是DLL输出节中的一个项目,用于将对一个函数FA的调用转至另一个DLL中的另一个函数FB。当你的应用程序调用FA之时,应用程序就会链接FA所在的DLL。当应用程序被激活的时候,加载程序就会加载FA所在的DLL,并看到FA其实转发到了FB,系统中的任何地方都不存在FA。

    如果调用GetProcAddress,这个函数就会查看参数中的DLL的输出节,确定HeapAlloc是个转发函数,然后按递归方式调用GetProcAddress,查找NTDLL.DLL中的输出节中的RtlAllocateHeap函数。GetProceAddress(GetModuleHandle("Kernel32"), "HeapAlloc")

    也可以利用DLL模块中的函数转发器。最为容易的方式是使用一个pragma指令:

    // Function forward to function in DllWork
    #pragma comment(linker, "/export:SomeFunc=DllWork.SomeOtherFunc")
    

    这个操作告诉链接程序,被编译的DLL应该输出一个名叫SomeFunc的函数。但是SomeFunc函数的实现实际上位于另一个名叫SomeOtherFunc的函数中,该函数包含在名叫DllWork.dll的模块中。这样就可以为每一个转发函数创建一个单独的pragma代码行。

    20.5 已知的DLL

    有一些特殊的DLL,它们与其他的DLL并不不同,只是被操作系统做了特殊的处理,总是在同一个目录中查找这些DLL。这样可以便于对这些DLL进行加载操作。参考下面的注册表关键字:

    如你所见,这个关键字包含一组值的名字,这些名字是某些DLL的名字。每个值名字都有
    一个数据值,该值恰好与带有.DLLl文件扩展名的值名字相同。当LoadLibrary(Ex)被调用时,函数先产看是否传递了带有.DLL扩展名的DLL作为参数。如果没有传递,那么函数将使用常规的搜索规则来搜索DLL。

    如果设定了DLL的扩展名,那么函数将删除扩展名,然后搜索注册表关键字,查看是否存在相同的名字。如果没有找到匹配的名字,便使用通常的搜索规则。但是,如果搜索到了DLL的名字的值,系统将查找相关的值的数据,并设法用值的数据来加载DLL。系统也开始在注册表中的DllDirectory值数据指明的目录中搜索DLL。按照默认的设置,DllDirectory值的数据是%SystemRoot%/System32

    假设将下面的值添加给注册表关键字KnowDLL:

    Value name: SomeLib
    Value data: SomeOtherLib.dll
    

    若你调用LoadLibrary(“SomeLib”);,系统将使用通常的规则查找这个文件。但是如果调用的是LoadLibrary(“SomeLib.dll);,系统将删除扩展名,再查看是否有匹配的值名字。

    此时,系统将设法加载名字为SomeOtherLib.dll的文件,而不是SomeLib.dll。它先系统目录去%SystemRoot%/System32中查找SomeOtherLib.dll。如果找到,就加载这个DLL,否则函数LoadLibrary(Ex)就运行失败并返回NULL,同时对GetLastError的调用将返回2(ERROR_FILE_NOT_FOUND)。

    20.6 DLL转移

    算是微软应对版本变化的一个策略了。当Windows刚刚发展的时候,RAM和磁盘的空间都是十分宝贵的,所以CC++运行时类库和MFC的DLL为了实现所有应用程序共享,就优先将这些类库和DLL放在Windows的系统目录。

    但是随着版本的更迭,应用程序会遇到新旧版本的类库不匹配而导致无法正常运行的问题。现在硬盘的容量越来越大,RAM空间也有富余, 因此Windows就改变了这个策略,用一个新的特性,使得应用程序可以强制的首先在应用程序的当前目录加载文件模块。只有在当前目录下找不到的时候,才去别的目录搜索。

    而实现这个这个特性的方式将是在你的应用程序的目录放入一个文件。文件的内容可以忽略,但是内如必须是AppName.local。例如如果有一个可执行文件的名字是SuperApp.exe,那么转移文件的名字就是SuperApp.exe.local。

    在系统内部,LoadLibrary(Ex)函数已经被修改,会先查看是否存在这个文件。如果应用程序的目录中存在这个文件,该目录中的模块就已经被加载。如果不存在这个文件,LoadLibrary(Ex)函数将正常运行。

    这个特性对于已经注册的COM对象来说是非常有用的,它能使应用程序将它的COM对象DLL放入自己的目录。这样注册了相同COM对象的其他应用程序就不能干扰你的操作。

    20.7 改变模块的位置

    每个EXE或者DLL都有一个默认的加载基地址,EXE默认加载的基地址是0X00400000,而DLL的是0X00100000。但是如果所有模块都按照默认的基地址映射到RAM中,模块们就会发生重叠错乱了。

    既然Windows已经这么成功,这个问题必然是有解决方案的。简单的说就是为每个指定的文件调用函数ReBaseImage:

    BOOL ReBaseImage(
    	PSTR CurrentImageName,			// Pathname of file to be rebase
    	PSTR SymbolPath,					  // Symbol file path so debug info is accrate
    	BOOL fReBase,							 // TRUE to actually do the work;FALSE to pretend
      BOOL fReBaseSysFileOk,			// FALSE to not rebase image system images
      BOOL fGoingDown, 						// TRUE to rebase the image can grow to an address
    	ULONG CheckImageSize,				// Maximum size that image can grow to
      ULONG* pOldImageSize, 			// Receives original image size  
      ULONG* pOldImageBase,				// Receives original image base address
      ULONG* pNewImageSize, 			// Receives new image size 
      ULONG* pNewImageBase,				// Receives new image base address
      ULONG TimeStamp						  // New timestamp for image
    );
    

    当你执行Rebase程序,就会调用这个函数,为函数传递一组映像文件名时,它将执行如下的操作:

    1. 仿真创造一个进程的地址空间。
    2. 打开通常被加载到这个地址空间的所有模块。
    3. 仿真改变各个模块在仿真地址空间中的位置,这样各个模块就不会重叠。
    4. 对于已经移位的代码,它会分析该模块的移位节,并修改磁盘上的模块文件中的代码。
    5. 更新每个移位模块的头文件,以反映新的首选基地址。

    对于生产工具,非常推荐使用Rebase程序。连微软在销售Windows操作系统之前,对操作系统提高的所有文件都运行了Rebase程序,因此将它们映射到单个地址空间的话,所有的模块都不会重叠。

    20.8 绑定模块

    模块的移位可以提高系统的性能,但是这样的办法还有很多。可以将所有的模块绑定起来,使得应用程序可以尽可能快地初始化,尽可能使用少地使用存储器。绑定一个模块的时候,就可以为这个模块的输入节配备所有输入符号的虚拟BindImage();地址。为了缩短初始化的时间和使用少的存储器,这昂操作必须要在加载模块之前进行。

    VS的另一个程序Bind.exe就是完成这项工作的一个工具,它的使用可以参考PlatformSDK文档。和Rebase相似,Bind.exe为每个指定的文件重复调用BindImageEx函数:

    BOOL BindImageEx(
    	DWORD dwFlags;					// Flags giving fine control over the function
    	PSTR pszImageName,			// Pathname of file to be bound
    	PSTR pszDllPath,				// Search path used to locating image file
      PSTR pszSymbolPath,			//  Search path used to keep debug info accurate
      PIMAGEHELP_STATUS_ROUTINE StatusRoutine // Callback function
    );
    

    参数StatusRoutine是个回调函数,BindImageEx会定期调用它,用来尽快链接进程:

    BOOL WINAPI StatusRoutine(
    	IMAGEHLP_STATUS_REASON Reason,		// Module/procedure not found, etc
      PSTR pszImageName,			// Pathname of file being bound
    	PSTR pszDllPath,				// Pathname of DLL
      ULONG_PTR VA,						// Computed virtual address 
      ULONG_PTR Parameter			//  Additional info depending on Reason
    );
    

    当执行Bind程序,传递给它一个映像文件名时:

    1. 打开指定映像文件的输入节。
    2. 打开输入节中列出的每个DLL,查看它的头文件以确定它的首选地址。
    3. 查看DLL的输出节中的每个输入符号。
    4. 取出符号的RVA,并将模块的首选地址与它相加。将可能产生的输入符号的虚拟地址吸入映像文件的是输入节中。
    5. 将某些辅助信息添加到映像文件的输入节中,包括映像文件绑定到的所有DLL模块的名字和这些模块的时间戳。

    在进程执行的过程中,Bind程序有两个重要的假设:

    1. 当进程初始化的时候,它需要的所有DLL实际上都被加载到它们的首选基地址中。可以使用Rebase工具来确保这一点。
    2. 从绑定操作执行开始,DLL的输出节中引用的符号的位置上一直没有改变。加载程序通过将每个DLL的时间戳与上面第5个步骤中保存的时间戳比对,以核实这个情况。

    如果核实失败,加载程序就需要通过人工来修改可执行模块的输入节,就像它通常所做的那样。但是如果加载程序发现模块已经连接,需要的DLL已经加载到它们的首选基地址,而且时间戳也匹配,那么加载程序实际上不需要做什么,让应用程序只管运行即可。

    连来自系统的页文件的存储器都不必使用。

    看起来已经很完美。但是什么时候去连接模块呢?在开发的过程中,可以使用系统的DLL绑定,但是这些DLL不一定是用户会安装的。更有甚者,如果是双系统的情况下,Win7和Win10使用的DLL肯定有差别。

  • 相关阅读:
    Oracle報錯︰ORA-12638 證明資料擷取失敗
    tomcat启动startup.bat一闪而过[转]
    供应商术语
    如何在本地设置www.xxx.com,使其允许访问
    windows 安装包位置
    Windows Mobile系列学习四:Windows Mobile如何进行打包、设置打包显示图标
    Windows Mobile系列学习二:Windows Mobile 模拟器如何设置网络连线
    Windows Mobile的程式安装的手持机上如何设置开机自动启动功能?
    Java篇:树和Map
    JAVA篇:Java 多线程 (八)多线程下其他组件
  • 原文地址:https://www.cnblogs.com/leoTsou/p/13692611.html
Copyright © 2011-2022 走看看