Windows核心编程——》第二十章 DLL高级技巧 (DLL Advanced Techniques)

来源:互联网 发布:昆明网络推广哪家好 编辑:程序博客网 时间:2024/05/22 23:58

1.概览

 1.1动态加载DLL文件 LoadLibraryEx

                HMODULE LoadLibraryEx(

PCTSTR pszDLLPathName,

HANDLE hFile,

DWORD dwFlags);

              返回DLL加载到进程空间原首地址

              dwFlags 可以有以下几个值

              (1) DONT_RESOLVE_DLL_REFERENCES

                              建议永远不要使有这个值,它的存在仅仅是为了向后兼容、

                              更多内容请访问:http://blogs.msdn.com/oldnewthing/archive/2005/02/14/372266.aspx

              (2) LOAD_LIBRARY_AS_DATAFILE

                              把要加载的DLL文件以数据文件的形式加载到进程中

                              GetModuleHandleGetProcAddress返回NULL

              (3) LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE

                              与前者相同,不同的时独占打开,禁止其它进程访问和修改该DLL中的内容

              (4) LOAD_LIBRARY_AS_IMAGE_RESOURCE

                              不修改DLL中的RVAimage的形式加载到进程中。常与LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE一起使用。

              (5) LOAD_WITH_ALTERED_SEARCH_PATH

                              修改DLL的加载路径

 1.2 DLL的加载与卸载

              (1)加载

                              不要在同一进程中,同时使用LoadLIbraryLoadLibraryEx加载同一DLL文件。

                              DLL引用计数是以进程为单位的LoadLibrary会把DLL文件加载到内存,然后映射到进程空间中。

                              多次加载同一DLL只会增加引用计数而不会多次映射。当所有进程对DLL引用计数都为0时,系统会在内存中释放该DLL

              (2)卸载

                              FreeLibrary,FreeLibraryAndExitThread对当前进程的DLL的引用计数减1

              (3) GetProcAddress

                              取得函数地址。它只接受ANSI字符串。

2.DLL的入口函数

                2.1 DllMain

              BOOL WINAPI DllMain(

              HINSTANCE hInstDll, ""加载后在进程中的虚拟地址

              DWORD fdwReason, ""系统因何而调用该函数

              PVOID fImpLoad ""查看是隐工还是动态加载该DLL

 

              DLLsDllMain方法来初始化他们自已。DllMain中的代码应尽量简单,只做一些简单的初始化工作

              不要在DllMain中调用LoadLibrary,FreeLibraryShell, ODBC, COM, RPC,  socket函数,从而避免不可预期的错误。

 

                2.2 fdwReason的值

               (1)DLL_PROCESS_ATTACH

               系统在为每个进程第一次加载该DLL会,执行DLL_PROCESS_ATTACH后面的语句来初始化DLL,DllMain的返回值仅由它决定。

 系统会忽略DLL_THREAD_ATTACH等执行后DllMain的返回值。

               如果DllMain返回FALSE,系统会自动调用DLL_PROCESS_DETACH的代码并解除DLL文件中进程中的内存映射。

               

               (2)DLL_PROCESS_DETACH

                              如果DLL是因进程终止而卸载其在进程中的映射,那么负责调用ExitProcess的线程会调用DllMainDLL_PROCESS_DETACH所对应的代码。

                              如果DLLFreeLibraryFreeLibraryAndExitThread,而卸载其在进程中的映射

那么FreeLibraryFreeLibraryAndExitThread会负责调用DllMainDLL_PROCESS_DETACH所对应的代码。

                              如果DLL是因TerminateProcess而卸载其在进程中的映射,系统不会调用DllMainDLL_PROCESS_DETACH所对应的代码

              (3) DLL_THREAD_ATTACH

                              若进程是先加载的DLL,后创建的线程

                                              那么在进程中创建新线程时(主线程除外)系统会执行该进程已载的所有DLLDllMainDLL_THREAD_ATTACH对应的代码。

                              若进程是先创建的线程,后加载的DLL

                                              那么系统不会调用DLLDllMain中的代码

              (4) DLL_THREAD_DETACH

                              进程中的线程退出时,会先执行所有已加载DLLDllMainDLL_THREAD_DETACH所对应的代码。若该代码中有死循环,线程不会退出。

             

 2.3 同步化DllMain的调用

              同一时间只能有一个线程调用DllMain中的代码,所以下面的代码会导致死循环

BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) {

 

   HANDLE hThread;

   DWORD dwThreadId;

 

   switch (fdwReason) {

   case DLL_PROCESS_ATTACH:

      // The DLL is being mapped into the process' address space.

 

      // Create a thread to do some stuff.

      hThread = CreateThread(NULL, 0, SomeFunction, NULL,

         0, &dwThreadId);// CreateThread DLL_THREAD_ATTACH中的代码,但是由于当前线程并未执行完毕,

//所以DLL_THREAD_ATTACH 中的代码不会被执行,且CreateThread永无不会返回。

 

      // Suspend our thread until the new thread terminates.

      WaitForSingleObject(hThread, INFINITE);

 

      // We no longer need access to the new thread.

      CloseHandle(hThread);

      break;

 

   case DLL_THREAD_ATTACH:

      // A thread is being created.

      break;

 

   case DLL_THREAD_DETACH:

      // A thread is exiting cleanly.

      break;

 

   case DLL_PROCESS_DETACH:

      // The DLL is being unmapped from the process' address space.

      break;

   }

   return(TRUE);

}

 

3.延时加载DLL

(1)延时加载DLL的限制

              延时加载是指当程序在运行时用到DLL中的函数时自动会自动加载DLL函数,它与动态加载不同。

              http://msdn2.microsoft.com/en-us/library/yx1x886y(VS.80).aspx

 

4.已知的DLL(Known DLLs)

              位置:HKEY_LOCAL_MACHINE"SYSTEM"CurrentControlSet"Control"Session Manager"KnownDLLs

              LoadLibrary在查找DLL会先去该位置查找有无相应的键值与DLL要对应,若有则根据链值去%SystemRoot%"System32加载键值对应的DLL

              若无则根据默认规去寻找DLL

 

5.Bind and Rebase Module

              它可以程序启动的速度。ReBaseImage







1.         加载一个DLL,系统至少会干几件事:(1)将不同段的分页分别映射并赋予不同的保护属性。(2)检查DLL依赖的其他DLL依次加载。(3)执行DllMain

2.         LoadLibraryEx:dwFlags参数-DON’T_RESOLVE_DLL_REFERENCES-DLL映射到内存后,对于条款1中的三件事,只做按段分配保护属性这件。LOAD_LIBRARY_AS_DATAFILE-比起上个标志,连三件事中仅剩的一件也省了,只是映射文件,用做数据文件。可以加载EXE然后读取其中的资源。LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE-以独占方式映射数据文件。LOAD_LIBRARY_IMAGE_SOURCE-AS_DATAFILE的基础上,将导出段的所有RVA转换成VALOAD_WITH_ALTERED_SEARCH_PATH-可以调整DLL路径的搜索方式。LOAD_IGNORE_CODE_AUTHZ_LEVEL-安全相关,该安全方案被后来的UAC取代。

3.         SetDllDirectory:设置加载DLL时的搜索路径,DLL在搜索进程的当前路径过后就会搜索这里。当路径为空串(””\0”)的时候,表示搜索的时候跳过当前路径,当路径为NULL的时候恢复默认搜索方式。

4.         FreeLibraryAndExitThread适用于一个场合:要调用FreeLibrary的代码正是位于DLL中。

5.         LoadLibrary和LoadLibraryEx返回的地址不等价,不能混用。如先以LOAD_LIBRARY_AS_DATA_FILE做参数调用LoadLibraryEx,再用LoadLibrary加载同一个DLL,返回值是不同的。

6.         GetProcAddress。

7.         名为DllMain的函数不存在的时候,系统会使用默认入口。

8.         DllMain的fdwReason参数:DLL_PROCESS_ATTACH-DLL第一次被加载的时候传入,对于隐式加载的DLL是主线程执行,而显式加载的DLLLoadLibrary线程执行,用于执行DLL初始化操作。返回FALSE,程序会报错表示加载DLL失败。DLL_PROCESS_DETACH-隐式卸载的时候由主线程执行,显示卸载的时候由FreeLibrary线程执行,负责清理资源。DLL_THREAD_ATTACH-线程在创建时,会检查进程已经加载的DLL,然后依次通知每个DLLDllMain函数。进程启动时会先创建主线程,再加载各个DLL,因此这时主线程调用DllMain只会传入DLL_PROCESS_ATTACH而不是DLL_THREAD_ATTACHDLL_THREAD_DETACH-线程退出的时候检测所有已经加载的DLL依次调用DllMain

9.         DisableThreadLibraryCalls:声明线程在创建和退出的时候不用通知指定DLLDllMain函数。

10.     所有DLLDllMain的调用被加载锁(Loader Lock,进程唯一的)序列化了。避免同时创建多个线程以DLL_THREAD_ATTACH调用DllMain时产生竞争。

11.     对于C++编写的DLL,实质上系统通知的是__DllMainCRTStartup,当fdwReasonDLL_PROCESS_ATTACHDLL_PROCESS_DETACH时,它会调用全局变量的构造或析构函数,然后再调用DllMain

12.     延迟载入是指直到使用DLL导出的函数时系统才加载DLL和查找函数。优点:加速进程启动、让为高版本系统设计的程序在低版本系统中也能使用部分功能、特殊的设计用途等。部分DLL不能延迟加载:导出了数据的DLL(因为延迟加载利用的是GetProcAddress等功能)、Kernal32.dll。另外在DllMain中也不该使用延迟载入的DLL函数。

13.     延迟加载的使用:在Linker-Input-Delay Loaded Dlls中指定要延迟载入的DLL。如果要Hook延迟加载过程以及停用延迟加载的DLL,需要再导入DelayImp库和开启Linker-Advanced-Delay Loaded DllSupport Unload

14.     延迟加载的细节:模块引用的DLL要延迟加载的话,会删除该DLLidata段,改为包含didata段,对延迟加载函数的调用会跳转到__delayLoadHelper2函数中,该函数会确保该DLL已经被加载,然后检查didata中对应函数的表项是否非空,为空的话用GetProcAddress查找并填充didata项,下次使用就不用再查找。用__FUnloadDelayLoadedDLL2卸载延迟加载DLL,以便之后再次使用延迟函数能够保证正常,该函数会清空didata中已经填充的各项。__pfnDliNotifyHook2__pfnDilFailureHook2是延迟加载过程的Hook函数指针。

15.     函数转发器:#pragma comment(linker, “/export:SomeFunc=DllA.SomeOtherFunc”)

16.     HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs包括一些影响LoadLibrary路径查找的信息。

17.     关于模块基地址重定位:DLL中的代码访问DLL中的全局变量时用的是绝对地址,同时会增加一个reloc段(重定位段)记录所有引用绝对地址的代码,如果DLL最终加载的位置不是默认基址,之前使用的绝对地址需要根据reloc的记录被修正,这就是重定位过程。可见如果进程加载的时候,多个DLL基址发生冲突,需要被重定位,修复绝对地址的操作增加了加载时间,同时也会因为修改Image内存页造成写拷贝,增加了系统的虚拟内存占用。最理想的情况下,所有使用DLL的进程都不需要重定位,这就需要安排合理的基址,可以使用Rebase.exe工具或者ReBaseImage函数。使用dumpbin.exe /headers命令可以查看包括基址的信息。使用/FIXED开关删除reloc段,禁用重定位。

18.     关于模块的绑定:默认情况下,模块的引入段,会在进程加载模块后被填入导入函数的绝对地址,因此包含引入段的模块会发生内存页的写拷贝。使用Bind.exe工具,可以在映像文件中的idata段填入绝对地址和对应DLL的时间戳,当进程加载时,发现被依赖DLL没有被重定位(即基址和默认基址相同)且时间戳和绑定的DLL相同,那么idata段就可以不用修改直接使用绑定值,避免了写拷贝。可以使用Bind.exe工具或BindImageEx函数来绑定模块。绑定操作应该在软件每次升级后执行。

19.     综合讨论重定位和绑定:一个DLL中,引入段最终包含所依赖的DLL的函数地址,如果所依赖的DLL没有被重定位,那引入段不用被修改避免了写拷贝;DLL内部的全局变量是用绝对地址访问,如果DLL本身没有被重定位,这些绝对地址不用被修改也避免了写拷贝。因此用ReBase.exe工具合理安排所有DLL的基址,然后在用Bind.exe工具写入导入函数地址,能提升性能和减少内存占用。






显式的载入DLL模块

HMODULE WINAPI LoadLibrary(

  __in          LPCTSTR lpFileName

);

 

HMODULE WINAPI LoadLibraryEx(
  __in          LPCTSTR lpFileName,
                 HANDLE    hFile,
  __in          DWORD dwFlags
);

hFile参数是为将来扩充所保留的,现在必须将它设为NULL

dwFlags 参数可以被设置为0,或者下列标志的组合:DONT_RESOLVE_DLL_REFERENCES

LOAD_IGNORE_CODE_AUTHZ_LEVEL LOAD_LIBRARY_AS_DATAFILE LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVELOAD_LIBRARY_AS_IMAGE_RESOURCE LOAD_WITH_ALTERED_SEARCH_PATH

这些标志有可以改变DLL文件的默认搜索路径等功能,具体参见MSDN.

 

上面这两个函数会在用户的系统中对DLL的文件映像进行定位,并试图将该文件映像映射到调用进程的地址空间中。两个函数返回的HMODULE表示文件映像被映 射到的虚拟内存地址。DllMain入口点所接收的HINSTANCE参数也同样是文件映像被映射到的虚拟内存地址。

 

 

显式的卸载DLL模块

当进程不再需要引用dll中的符号时,我们应该调用下面的函数来显式的将DLL从进程的地址空间中卸载:

BOOL WINAPI FreeLibrary(
  __in          HMODULE hModule
);

 

我们还可以调用下面这个函数来将一个DLL模块从进程的地址空间中卸载:

VOID WINAPI FreeLibraryAndExitThread(
  __in          HMODULE hModule,
  __in          DWORD dwExitCode
);

这个函数适用的情形是:当我们编写了一个DLL,在一开始被映射到进程的地址空间中时,这个DLL会创建一个线程。当线程完成了它的工作后,我们先后调用FreeLibraryExitThread,来从进程的地址空间中撤销对DLL的映射并且终止进程。

但是如果线程分别调用FreeLibraryExitThread,那么会出现一个严重的错误。这个问题就是FreeLibrary会立即从进程的地址空间中撤销对DLL的映射。当FreeLibrary返回的时候,调用ExitThread的代码已经不复存在了,线程试图执行的是不存在的代码。

这将导致访问违规,并导致整个进程被终止。

此时,我们可以调用上述函数,就可以了。

 

检测一个DLL文件是否已经被映射到了进程的地址空间

HMODULE WINAPI GetModuleHandle(
  __in          LPCTSTR lpModuleName
);

如果传NULLGetModuleHandle,那么函数会返回应用程序的可执行文件的句柄。

例如:

HMODULE h = GetModuleHandle("Mylib.dll");

if (h == NULL)

{

         h = LoadLibrary(TEXT(“Mylib.dll”);

}

Else

{

         printf("Mylib.dll已经载入");

}

        

得到DLL的全路径

DWORD WINAPI GetModuleFileName(
  __in          HMODULE hModule,
  __out         LPTSTR lpFilename,
  __in          DWORD nSize
);

如果传NULL给第一个参数,那么函数会在lpFilename中返回当前正在运行的应用程序的可执行文件的文件名。

 

显式的链接到导出符号

一旦显式的载入了一个DLL模块,线程必须通过调用下面的函数来得到它想要引用的符号的地址:

FARPROC WINAPI GetProcAddress(
  __in          HMODULE hModule,
  __in          LPCSTR lpProcName
);

hModule参数是用来指定包含符号的DLL的句柄。它是先前调用LoadLibrary(Ex)GetModuleHandle所返回的值。

参数lpProcName是我们想要得到的符号名。

 

用法:

在能够调用GetProcAddress返回的函数指针来调用函数之前,我们需要将它转型为与函数原型相匹配的正确类型。

例如:Typedef void (CALLBACK *PFN_DUMPMODULE)(HMODULE hModule);

是与void DynamicDumpModule(HMODULE hModule)函数相对应的回调函数的类型签名。

 

例如:

PFN_DUMPMODULE pfnDumpModule =

(PFN_DUMPMODULE)GetProcAddress(hDll, “DynamicDumpModule”);

If(pfnDumpModule != NULL)

{

         PfnDumpModule(hDll);

}

 

DLL的入口点函数

一个DLL可以有一个入口点函数,系统会在不同时候调用这个入口点函数。这些调用是通用性质的,通常被DLL用来执行一些与进程或线程有关的初始化和清理工作。

如果DLL不需要这些通知,那么我们可以不必在源代码中实现这个入口点函数。

例如,如果要创建一个只包含资源的DLL,那么我们就不需要实现这个函数。

 

入口点函数:

BOOL APIENTRY DllMain( HANDLE hModule,

                     DWORD  ul_reason_for_call,

                     LPVOID lpReserved)

{

    switch (ul_reason_for_call)

         {

                   case DLL_PROCESS_ATTACH:

                   case DLL_THREAD_ATTACH:

                   case DLL_THREAD_DETACH:

                   case DLL_PROCESS_DETACH:

                            break;

    }

    return TRUE;

}

 

hModule参数包含该DLL实例的句柄。这个值表示一个虚拟内存地址,DLL的文件映像就被映射到进程地址空间的这个位置。通常我们将这个参数保存在一个全局变量中,这样在调用资源载入函数(比如DialogBoxLoadString)的时候,就可以使用它。

lpReserved如果DLL是隐式载入的,那么这个参数的值将不为0;如果DLL是显示载入的,那么这个参数的值将为0

 

函数转发器

函数转发器是DLL输出段中的一个条目,用来将一个函数调用转发到另一个DLL中的另一个函数。

我们可以在自己的DLL模块中使用函数转发器。最简单的方法是使用pragma指示符,如下所示:

 

#pragma comment(linker, “/export:SomeFunc = DllWork.SomeOtherFunc”)

 

这个pragma告诉链接器,正在编译的DLL应该输出一个名为SomeFunc的函数,但实际实现SomeFunc的是另一个名为SomeOtherFunc的函数,该函数被包含在另一个名为DllWork.dll

的模块中。

我们必须为每个想要转发的函数单独创建一行pragma

 

DLL重定向

MicrosoftWindows 2000开始新增了一项DLL重定向的特性。这个特性强制操作系统的加载程序首先从应用程序的目录中载入模块。只有当加载程序无法找到要找的文件时,才会在其他目录中搜索。

为了强制加载程序总是先检查应用程序的目录,我们所要做的就是将一个文件放到应用程序的目录中。这个文件的内容无关紧要,但他的文件名必须是AppName.local

举个例子,如果我们的程序为SuperApp.exe,那么重定向文件的名称必须是SuperApp.exe.local

对已注册的COM对象来说,这项特性极其有用。它允许应用程序将它的COM对象DLL放在自己的目录中,这样注册了同一个COM对象的其他应用程序就不会妨碍到我们的应用程序。

         注意,为了安全性的缘故,Windows Vista中这项特性默认是关闭的,因为它可能会使系统从应用程序的文件夹中载入伪造的系统DLL,而不是从Windows的系统文件夹下载入真正的系统DLL。为了打开这项特性,我们必须在HKLM\Software\Microsoft\WindowsNT\CurrentVersion\Image File Execution Options注册表项中增加一个条目DWORD DevOverrideEnable,并将它的值设为1.

 

模块的基地址重定位

每个可执行文件和DLL模块都有一个首选基地址,它表示在将模块映射到进程的地址空间中时的最佳内存地址。当我们在构建一个可执行模块的时候,链接器会将模块的首选基地址设为0x00400000。对DLL模块来说,链接器会将首选基地址设为0x10000000.

         假设有3个模块,一个user.exe,另外两个是A.dllB.dll。在编译链接各个模块时,我利用VS默认的base address,这样user.exe的默认基地址是0x00400000,AB的基地址是0x10000000。这样,当加载器加载 User.exe(它同时隐式链接A,B)。这样,A,B就会有一个被迫改变默认的基地址;从而导致映像文件里的机器代码指令(包含的硬编码地址)与加载 后的不一样,从而需要调整(效率就会降低)


         
当一个模块无法被加载到他的首选基地址的时候,存在以下两个主要缺点:

1.       加载程序必须遍历重定位段并修改模块中大量的代码。这个过程不仅是一大性能杀手,而且也确实会损害应用程序的初始化时间。

2.       当加载程序写入到模块的代码页面中时,系统的写时复制机制会强制这些页面以系统的页交换文件为后备存储器。由于页交换文件是所有模块的代码页面的后备存储器,因此这会减少可供系统中所有进程使用的存储器的数量。

 

为了要将多个模块载入到同一个地址空间中,那么我们必须给每个模块指定不同的首选基地址。

Visual studio提供了一个名为Rebase.exe的工具。如果在执行Rebase工具的时候传给它一组映像文件名,那么它会执行下面的操作。

1.  它会模拟创建一个进程地址空间。

2.  它会打开应该被载入到这个地址空间中的所有模块,并得到每个模块的大小以及他们的首选基地址。

3.  它会在模拟的地址空间中对模块重定位的过程进行模拟,使各模块之间没有交叠。

4.  对每个重定位过的模块,它会解析该模块的重定位段,并修改模块在磁盘文件中代码。

5.  为了反映新的首选基地址,它会跟新每个重定位过的模块的文件头。

 

用法:

Rebase.exe -b 0x400000 user.exe A.dll  B.dll

注意:我们应该在自己的构建过程的后期,等应用程序所有的模块都已经构建完成后运行它。






http://www.cnblogs.com/lgxqf/archive/2009/03/18/1415379.html


http://www.cnblogs.com/cbscan/archive/2011/09/30/2196424.html


http://mzf2008.blog.163.com/blog/#m=0&t=1&c=fks_084066086081086068083094080095087083084074093081094069


http://hi.baidu.com/hypkb/archive/tag/windows核心编程?page=2







0 0
原创粉丝点击