免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 1891 | 回复: 1
打印 上一主题 下一主题

《Windows核心编程系列》谈谈DLL高级技术 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2011-12-08 20:59 |只看该作者 |倒序浏览
《Windows核心编程系列》谈谈DLL高级技术












本篇文章将介绍DLL显式链接的过程和模块基地址重定位及模块绑定的技术。

      第一种将DLL映射到进程地址空间的方式是直接在源代码中引用DLL中所包含的函数或是变量,DLL在程序运行后由加载程序隐式的载入,此种方式被称为隐式链接。

      第二种方式是在程序运行时,通过调用API显式的载入所需要的DLL,并显式的链接所想要链接的符号。换句话说,程序在运行时,其中的一个线程能够显式的将该DLL调用到进程地址空间中,并得到DLL中某函数的在进程地址空间的虚拟地址,然后调用该函数。此种方式被称为显式链接。

      注意:显式载入某DLL时,不需要该dll的Lib文件,且exe文件中并不包含该dll的导入表。

显示载入DLL模块的步骤:

    线程可以调用LoadLibrary将一个DLL映射到进程地址空间。
  1. view plaincopy to clipboardprint?HMODULE LoadLibrary(PCTSTR pszDLLPathName);  
  2.    HMODULE LoadLibrary(PCTSTR pszDLLPathName);
复制代码
该函数会试图对程序想载入的DLL进行定位,并试图将该DLL映射到调用进程的地址空间中。返回是DLL在调用进程的虚拟地址。即模块的句柄。如果无法将DLL载入到进程地址空间中返回值为NULL.

     与它类似的另一个函数
  1. view plaincopy to clipboardprint?HMODULE LoadLibraryEx(PCTSTR pszDLLPathName,HANDLE hFile,DWORD dwFlags);  
  2.       HMODULE LoadLibraryEx(PCTSTR pszDLLPathName,HANDLE hFile,DWORD dwFlags);
复制代码
也可以实现将DLL载入到进程地址空间的目的。具体请参考MSDN。

     加载后如果程序不再需要该DLL,可以调用FreeLibrary将DLL从进程地址空间中卸载:

  1. view plaincopy to clipboardprint?BOOL FreeLibrary( HMODULE  hInstDll );  
  2.     BOOL FreeLibrary( HMODULE  hInstDll );
复制代码
也可以调用FreeLibraryEx卸载某DLL。

    以下函数不仅具有从进程地址空间卸载某DLL的功能,还能退出调用线程:

  1. view plaincopy to clipboardprint?VOID FreeLibraryAndExitThread(HMODULE hInstDll,DWORD dwExitCode)  
  2.   
  3. {  
  4.   
  5. FreeLibrary(HMODULE hInstDll);  
  6.   
  7. ExitThread(dwExitCode);  
  8.   
  9. }  
  10.    VOID FreeLibraryAndExitThread(HMODULE hInstDll,DWORD dwExitCode)

  11.    {

  12.     FreeLibrary(HMODULE hInstDll);

  13.     ExitThread(dwExitCode);

  14.    }
复制代码
刚见到时或许你会觉得它很多余。考虑下面的情形:

       我们调用一个DLL,该DLL中的代码会创建一个线程,当此线程完成工作后,可以调用FreeLibrary和ExitThread将DLL从进程地址空间中卸载,并终止自己。由于线程是由DLL创建的,线程执行的代码也在DLL中,当线程调用FreeLibrary将它所在的DLL卸载的时候,它后续要执行的代码已不再进程地址空间中了,试图执行不存在的代码可能会导致访问违规,导致进程被终止。

       如果线程调用FreeLibraryAndExitThread,此函数在Kernel32.dll中,FreeLibraryAndExitThread函数调用FreeLibrary将线程函数所在的DLL卸载后,其所属DLL Kernel32.dll仍在进程地址空间内,FreeLibraryAndExitThread函数继续执行调用ExitThread,后续代码仍然存在,不会导致访问违规。

       每个DLL在进程中都有一个使用计数。LoadLibrary(Ex)会增加其计数,FreeLibrary(Ex)和FreeLibraryAndExitThread会递减其计数。例如:当程序第一次调用LoadLibrary来载入一个DLL时,系统会将此DLL映射到进程地址空间中,并将此DLL的使用计数加一。如果线程后来再次调用LoadLibrary(Ex)时,系统不会将此DLL再次映射到进程地址空间,仅仅递增此DLL的使用计数。为了从进程地址空间中撤销对该DLL的映射,线程必须调用FreeLibrary(Ex)两次。第一次是将此DLL的使用计数减为1,第二次减为0。当系统发现某DLL的使用计数已经为0时,会从进程地址空间卸载此DLL。此时如果线程试图显式调用DLL中的函数将会导致访问违规。

     系统会在每个进程中为DLL维护一个使用计数,在本进程调用LoadLibrary仅仅是增加DLL在本进程的使用计数。如果进程A中的一个线程执行了LoadLibrary("Mydll.dll");进程B的某一线程也调用LoadLibrary("Mydll.dll");那么该DLL会被映射到A,B两个进程空间中去,且在A和B进程的使用计数都为1。

调用FreeLibrary("Mydll.dll");也仅仅是递减DLL在本进程内的使用计数。

  1. view plaincopy to clipboardprint?HMODULE  GetModuleHandle(PCTSTR pszModuleName);  
  2.     HMODULE  GetModuleHandle(PCTSTR pszModuleName);
复制代码
该函数可以用来检测某DLL是否被映射到了进程地址空间。如果返回值为NULL,则此DLL未被载入。

     当给pszModuleName传NULL时,函数会返回应用程序可执行文件的句柄。

显式链接导出符号

       显式载入某个DLL后,线程可以通过调用以下函数来得到它要引用的符号的地址。
  1. view plaincopy to clipboardprint?FARPROC GetProcAddr(HMODULE hInstDll, PCSTR pszSymbolName);  
  2.     FARPROC GetProcAddr(HMODULE hInstDll, PCSTR pszSymbolName);
复制代码
hInstDll标识导出符号所在的DLL的句柄。它是LoadLibrary(Ex),或是GetModuleHandle所返回的句柄。

     pszSymbolName用于标识导出符号。

         pszSymbolName可以有两种形式:

         第一种:用符号名来指定我们想要得到哪个符号的地址。

        如:FARPROC pfn=GetProcAddress(hInstDll,"MyProc");

        它是以0结尾的字符串。要注意此字符串是ANSI类型的。因为编译器、链接器始终都是将符号的名称以ANSI字符串的形式保存在DLL的导出段。

         第二种:用序号来指定我们想要那个符号的地址。

         如:FARPROC pfn=GetProcAddress(hInstDll,MAKERESOURCE(2));

         这种方法假定我们知道某个导出符号在某DLL中的序号为2。应该明确的是Microsoft强烈反对使用序号。

使用序号的形式要比使用字符串速度慢,因为系统需要对一字符串标识的符号名进行字符串比较。使用第二种方法即使该序号并没有与任何导出函数相对应,GetProcAddress也会返回非NULL值。其实这个地址是无效的,访问此地址可能会导致访问违规。

      注意:使用GetProcAddress返回的函数指针来调用函数之前,需要将它转换成与函数签名相匹配的类型。

     例如:
  1. view plaincopy to clipboardprint?typedef void (CALLBACK *PFN_DUM_MOUDLE)(MODULE hModule);  
  2. typedef void (CALLBACK *PFN_DUM_MOUDLE)(MODULE hModule);
复制代码
它是与void DynamicDumpModule(HMODULE hModule)函数相对应的函数相同。

      动态调用某DLL导出函数的例子:
  1. view plaincopy to clipboardprint?<span style="font-size:18px;"> PFN_DUMPMODULE pfnDumpModule=(PFN_DUMPMODULE)GetProcAddress(hDll,"DumpModule");  
  2.   
  3. If(pfnDumpModule!=NULL)  
  4. {  
  5.     pfnDumpModule(hDll);  
  6. }  
  7.   
  8. ;/span>  
  9.    <span style="font-size:18px;"> PFN_DUMPMODULE pfnDumpModule=(PFN_DUMPMODULE)GetProcAddress(hDll,"DumpModule");

  10.    If(pfnDumpModule!=NULL)
  11.     {
  12.        pfnDumpModule(hDll);
  13.     }

  14. </span>
复制代码
DLL的入口点函数

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

       如果不需要执行这些操作,可以不必再源代码中不实现此函数。

       如果需要DLL接受这些通知,就应该按照如下的格式来实现该函数。


  1. view plaincopy to clipboardprint?<span style="font-size:18px;">Bool WINAPI DllMain(HINSTANCE hInsDll,DWORD fdwReason,PVOID fImpLoad)  
  2.   
  3. {  
  4.   
  5.      Swith(fdwReason)  
  6.   
  7.     {  
  8.   
  9.         Case DLL_PROCESS_ATTACH:  
  10.   
  11.    
  12.   
  13.              //DLL被映射到进程地址空间是,执行此处代码。   
  14.   
  15.                 Break;  
  16.   
  17.          Case DLL_THREAD_ATTACH:  
  18.   
  19.              //线程被创建的时候执行。   
  20.   
  21.                  Break;  
  22.   
  23.          Case DLL_THREAD_DETACH:  
  24.   
  25.                //线程终止运行时执行。   
  26.   
  27.                    Break;  
  28.   
  29.          Case DLL_PROCESS_DETACH:  
  30.   
  31.                 //DLL被卸载的时候执行。   
  32.   
  33.                     Break;  
  34.   
  35.        }  
  36. }  
  37.   
  38. </span>  
  39. <span style="font-size:18px;">Bool WINAPI DllMain(HINSTANCE hInsDll,DWORD fdwReason,PVOID fImpLoad)

  40. {

  41.      Swith(fdwReason)

  42.     {

  43.         Case DLL_PROCESS_ATTACH:

  44.   

  45.              //DLL被映射到进程地址空间是,执行此处代码。

  46.                 Break;

  47.          Case DLL_THREAD_ATTACH:

  48.              //线程被创建的时候执行。

  49.                  Break;

  50.          Case DLL_THREAD_DETACH:

  51.                //线程终止运行时执行。

  52.                    Break;

  53.          Case DLL_PROCESS_DETACH:

  54.                 //DLL被卸载的时候执行。

  55.                     Break;

  56.        }
  57. }

  58. </span>
复制代码
hInstDll是该DLL实例的句柄。它是DLL文件被映射到进程地址空间的虚拟地址。通常将这个参数保存在全局变量中。这样在DLL的其他导出函数中就可以使用。

        如果DLL是被隐式载入的,fImpLoad为非零值,显式的话fImportLoad为0。

        fdwReason表示系统调用入口点函数的原因。它是switch语句的参数。可以是上述四个值。分别表示四种情况。后续将会详细介绍每一种情况。

          注意:DLL使用DllMain对自己进行初始化。DllMain执行的时候,其他DLL的可能还未被初始化。这意味着我们应该避免在DllMain中调用从其他DLL中导出的函数。

DLL_PROCESS_ATTACH通知

         当系统第一次将一个DLL映射到进程地址空间是,会调用DllMain函数,并给fdwReason传入DLL_PROCESS_ATTACH。注意:只有在该DLL是第一次被调用到进程地址空间中时,才会调用DllMain。如果以后再次调用LoadLibrary(Ex)时,OS仅仅是递增该DLL在此进程的使用计数,并不会再次调用DllMain。

         当DLL在处理DLL_PROCESS_ATTACH时,应该根据需要执行与进程相关的初始化。如DLL中包含一些函数,需要使用自己的堆,可以在进程加载时执行一些堆的初始化工作。

处理DLL_PROCESS_ATTACH时,DllMain的返回值表示DLL的初始化是否成功。如初始化成功,应返回TRUE,否则应返回false。

       下面来看看DllMain调用的时机:

        创建新进程时,系统为该进程分配地址空间,并将exe可执行文件和所需要的DLL映射到进程地址空间。然后创建主线程,并用主线程来调用每个DLL的DllMain函数,同时传入DLL_PROCESS_ATTACH。当所有已映射的DLL完成对该通知的处理后,系统会让主进程执行可执行模块的C/C++运行库的启动代码。然后执行可执行模块的入口点函数(_tmain或_tWinMain)。如果任意一个DLL的DllMain返回false,就说明初始化失败,系统会将所有文件映像从地址空间中清除,向用户显示错误信息。

显式载入DLL的过程:

         进程调用LoadLibrary(Ex),该函数对DLL进行定位,并将该DLL映射到进程地址空间。然后会让调用LoadLibrary(Ex)的线程调用DllMain函数,并传入DLL_PROCESS_ATTACH。当DLL的DllMain函数完成了对通知的处理后,系统会让LoadLibrary返回。这样线程就可以继续执行。

         注意:DllMain是在进程调用LoadLibrary(Ex)的时候调用的。它返回到LoadLibrary(Ex)函数内。

论坛徽章:
0
2 [报告]
发表于 2011-12-22 18:58 |只看该作者
学习鸟  谢谢分享
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP