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

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

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

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

显示载入DLL模块的步骤:

线程可以调用LoadLibrary将一个DLL映射到进程地址空间。

  1. HMODULE LoadLibrary(PCTSTR pszDLLPathName);

该函数会试图对程序想载入的DLL进行定位,并试图将该DLL映射到调用进程的地址空间中。返回是DLL在调用进程的虚拟地址。即模块的句柄。如果无法将DLL载入到进程地址空间中返回值为NULL.

与它类似的另一个函数

  1. HMODULE LoadLibraryEx(PCTSTR pszDLLPathName,HANDLE hFile,DWORD dwFlags);

也可以实现将DLL载入到进程地址空间的目的。具体请参考MSDN。

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

  1. BOOL FreeLibrary( HMODULE  hInstDll );

也可以调用FreeLibraryEx卸载某DLL。

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

  1. VOID FreeLibraryAndExitThread(HMODULE hInstDll,DWORD dwExitCode)
  2. {
  3. FreeLibrary(HMODULE hInstDll);
  4. ExitThread(dwExitCode);
  5. }

刚见到时或许你会觉得它很多余。考虑下面的情形:

我们调用一个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. HMODULE  GetModuleHandle(PCTSTR pszModuleName);

该函数可以用来检测某DLL是否被映射到了进程地址空间。如果返回值为NULL,则此DLL未被载入。

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

显式链接导出符号

显式载入某个DLL后,线程可以通过调用以下函数来得到它要引用的符号的地址。

  1. 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. typedef void (CALLBACK *PFN_DUM_MOUDLE)(MODULE hModule);

它是与void DynamicDumpModule(HMODULE hModule)函数相对应的函数相同。

动态调用某DLL导出函数的例子:

  1. <span style="font-size:18px;"> PFN_DUMPMODULE pfnDumpModule=(PFN_DUMPMODULE)GetProcAddress(hDll,"DumpModule");
  2. If(pfnDumpModule!=NULL)
  3. {
  4. pfnDumpModule(hDll);
  5. }
  6. pan>

DLL的入口点函数

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

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

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

  1. <span style="font-size:18px;">Bool WINAPI DllMain(HINSTANCE hInsDll,DWORD fdwReason,PVOID fImpLoad)
  2. {
  3. Swith(fdwReason)
  4. {
  5. Case DLL_PROCESS_ATTACH:
  6. //DLL被映射到进程地址空间是,执行此处代码。
  7. Break;
  8. Case DLL_THREAD_ATTACH:
  9. //线程被创建的时候执行。
  10. Break;
  11. Case DLL_THREAD_DETACH:
  12. //线程终止运行时执行。
  13. Break;
  14. Case DLL_PROCESS_DETACH:
  15. //DLL被卸载的时候执行。
  16. Break;
  17. }
  18. }
  19. </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)函数内。

DLL_PROCESS_ATTACH通知

当一个DLL从进程的地址空间中撤销的时候,会调用该DLL的DllMain函数,并在fdwReason传入DLL_PROCESS_DETACH。该case语句内一般是用来执行与进程相关的清理工作。如调用HeapDestroy清理堆。

注意:当DLL刚被映射到进程地址空间,执行DllMain并传入DLL_PROCESS_ATTACH时的返回值为false时,所有DLL将会被撤销映射,此时并不会调用DllMain并传入DLL_PROCESS_DETACH。

下面谈谈调用DllMain并传入DLL_PROCESS_DETACH的时机:

1:当进程又由于某线程调用ExitProcess而终止时,映射到该进程的所有DLL都会被撤销。调用 ExitProcess的线程将负责执行DllMain。一般情况下,此线程就是主线程。

2:如果DLL被撤销的原因是因为进程中的线程调用了FreeLibrary或是FreeLibraryAndExitThread,那么执行上述函数的线程将负责对DllMain的调用。调用完成后线程返回,继续执行其他代码。

注意:如果进程终止是因为某个线程调用TerminateProcess,此时DllMain并不会被调用。这意味着在进程终止前,已经映射到进程的任何DLL将没有任何机会执行清理工作,这可能导致数据丢失或是已被该进程占用的信号量不能得到释放。因此不到万不得已,应该避免使用TerminageProcess。

DLL_THREAD_DEATTACH通知

当进程创建一线程的时候,系统会检查已映射到此进程的所有DLL,并用DLL_THREAD_ATTACH调用每个DLL的DllMain。一般在此时执行与线程有关的初始化。DllMain的代码是由新创建的线程执行。当该线程完成了所有DllMain之后,才会执行它的线程函数。

注意:仅仅是让新建的线程执行已经被映射到进程地址空间的DLL的DllMain函数。而不会让已经存在的线程调用DllMain。当系统的主线程被创建的时候,并不会调用DllMain并传入DLL_PROCESS_ATTACH。它已经在进程被创建的时候调用DllMain并传入DLL_PROCESS_ATTACH。

DLL_THREAD_DETACH通知

当线程调用ExitThread将要终止的时候,系统会让该线程用DLL_THREAD_DETACH调用所有已映射到进程地址空间的所有DLL的DllMain函数。这一般被用来执行与线程相关的清理工作。

注意:如果线程终止是因为其他线程调用TerminateThread而终止的话,系统不会用DLL_THREAD_DETACH让线程调用各DLL的DllMain。因此与TerminateProcess一样,除非万不得以,应避免使用TerminateThread函数。

下面来总结下调用DllMain的过程:

进程中的一个线程调用LoadLibrary来映射一个DLL,系统使该线程用DLL_PROCESS_ATTCH调用该DLL的DllMain函数(该线程不会得到DLL_THREAD_ATTACH)通知。当此线程退出时,系统让此线程再次调用所有DLL的DllMain函数,但此次传入的是DLL_THREAD_DETACH。虽然在该DLL映射的时候,不会向该DLL发送DLL_THREAD_ATTACH通知。但是当该线程退出时,会向DLL发送DLL_THREAD_DETACH通知。

之所以不发送DLL_PROCESS_DETACH通知,是因为DLL仍在进程中。只有当DLL被卸载时,才会发送此通知。

前面我们提到过DllMain函数并不是必须的。在链接DLL的时候,如果链接器无法在obj文件中发现DllMain函数,它会链接C/C++运行库的DllMain函数。如果我们不提供DllMain函数,C/C++运行库会认为我们不关系DLL的各种通知。它会调用DisableThreadLibraryCalls函数。

  1. <span style="font-size:18px;">     BOOL DisableThreadLibraryCalls(HMODULE hInstDll);
  2. </span>

该函数告诉系统  我们不想让系统向某个指定的DLL发送DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知。

C/C++运行库中实现的DllMain函数如下所示:

  1. <span style="font-size:18px;">    BOOL WINAPI DllMain(HINSTANCE hInstDll,DWORD fdwReason,PVOID fImpLoad)
  2. {
  3. if(fdwReason==DLL_PROCESS_ATTACH)
  4. DisableThreadLibraryCalls(hInstDll);
  5. return true;
  6. }
  7. </span>

延迟载入DLL。

所谓延迟载入DLL,就是在进程运行后加载程序加载各种DLL时,并不载入已经被设为延迟加载的DLL。直到该DLL中的某个导出函数被调用的时候,此DLL才会被加载到进程的地址空间中。该DLL是隐式链接的 。

这项特性非常有用,主要应用与以下各种情况下:

1:某进程使用了很多DLL,由于初始化时加载程序必须将所有DLL都映射到进程地址空间中,这会导致加载速度比较慢。如果使用延迟加载,某些DLL直到其导出符号被引用到的时候,该DLL才会被隐式加载到进程地址空间,这缩短了初始化时间。

2:当应用程序在代码上使用了一个新的函数,运行在不提供此函数的老版本的系统上时,如果该函数所在的DLL不使用延迟加载机制,加载程序会报告一个错误:无法找到该函数。接着便会终止该应用程序的执行。如果我们使用延迟加载技术,当程序检测到此时是运行在老的系统中,程序就不会调用此函数,转而使用可以在老的系统上使用的其它函数。程序仍然可以继续运行。由于不会在程序中引用在老系统中不支持的函数,该函数所在的DLL就不会被加载。

当然任何方法都有适用范围,延迟加载不适用于以下几种情况:

1:导出全局变量的DLL是无法延迟加载的。

2:Kernel32.dll是无法延迟加载的,LoadLibrary和GetProcAddress都在该模块中。必须加载该模块才可以调用它们。

3:不应该在DllMain中代用延迟加载函数,这样会导致程序崩溃。

要让延迟加载能够正常工作,首先要指定两个链接器开关。

/Lib:DelayImp.dl

/DelayLoad:要延迟加载的DLL名字。

它们不可以在代码中通过#pragma comment(linker,"")来设定。而要通过Configuration Properities属性页来设定。

/Lib:DelayImp.dll是通过Linker/Advanced/DelayLoadDLL开关来指定。它告诉链接器将函数_delay_LoadHelper2嵌入到我们的可执行文件中。

/DelayLoad开关可以通过Linker /input/DelayLoadDLLs开关来指定。要延迟载入的函数所在的DLL在该项的右侧指定。可以指定多个延迟载入DLL。

该开关告诉链接器::

1:将用户要延迟载入的DLL从可执行文件的导入段中去除,这样当进程初始化时该DLL就不会被隐式的载入。

2:在可执行文件中嵌入一个延迟载入段,来表示要从用户要延迟载入的DLL导入哪些函数。

3:当程序调用延迟载入DLL中的函数时,对该函数的调用会转到_delayLoadHelper2函数,来完成对延迟载入函数的解析。也就是说对延迟载入段中的函数的调用,实际上会调用_delayLoadHelper2函数。此函数会引用延迟载入段,然后调用LoadLibrary和GetProcAddress得到延迟载入函数的地址。一旦得到延迟载入函数的地址_delayLoadHelper2就会修复对该函数的调用(Windows核心编程的原话,至于如何修复不清楚。2011年12月8日注)。今后的调用将直接调用该延迟载入函数。注意:同一个DLL的其它函数仍然必须在第一次被调用的时候修复。对同一DLL中某一延迟函数的调用并不会对其他延迟函数的调用进行修复

关于延迟载入函数暂时介绍这么多。感兴趣的话可以参考其他文献。

《参考自windows核心编程》第五版第四部分。以上仅仅是个人总结,如有纰漏请不吝赐教!

《Windows核心编程系列》二十谈谈DLL高级技术的更多相关文章

  1. 《windows核心编程系列》十七谈谈dll

    DLL全称dynamic linking library.即动态链接库.广泛应用与windows及其他系统中.因此对dll的深刻了解,对计算机软件开发专业人员来说非常重要. windows中所有API ...

  2. 《windows核心编程系列》十九谈谈使用远程线程来注入DLL。

    windows内的各个进程有各自的地址空间.它们相互独立互不干扰保证了系统的安全性.但是windows也为调试器或是其他工具设计了一些函数,这些函数可以让一个进程对另一个进程进行操作.虽然他们是为调试 ...

  3. 《windows核心编程系列》十八谈谈windows钩子

    windows应用程序是基于消息驱动的.各种应用程序对各种消息作出响应从而实现各种功能. windows钩子是windows消息处理机制的一个监视点,通过安装钩子能够达到监视指定窗体某种类型的消息的功 ...

  4. 《Windows核心编程系列》十四谈谈默认堆和自定义堆

    堆 前面我们说过堆非常适合分配大量的小型数据.使用堆可以让程序员专心解决手头的问题,而不必理会分配粒度和页面边界之类的事情.因此堆是管理链表和数的最佳方式.但是堆进行内存分配和释放时的速度比其他方式都 ...

  5. 《windows核心编程系列》十六谈谈内存映射文件

    内存映射文件允许开发人员预订一块地址空间并为该区域调拨物理存储器,与虚拟内存不同的是,内存映射文件的物理存储器来自磁盘中的文件,而非系统的页交换文件.将文件映射到内存中后,我们就可以在内存中操作他们了 ...

  6. 《Windows核心编程系列》十二谈谈Windows内存体系结构

    Windows内存体系结构 理解Windows内存体系结构是每一个励志成为优秀的Windows程序员所必须的. 进程虚拟地址空间 每个进程都有自己的虚拟地址空间.对于32位操作系统来说,它的地址空间是 ...

  7. 《windows核心编程系列》十五谈谈windows线程栈

    谈谈windows线程栈. 当系统创建线程时会为线程预订一块地址空间区域,注意仅仅是预订.默认情况下预定的这块区域的大小是1MB,虽然预订这么多,但是系统并不会给全部区域调拨物理存储器.默认情况下,仅 ...

  8. 《windows核心编程系列》七谈谈用户模式下的线程同步

    用户模式下的线程同步 系统中的线程必须访问系统资源,如堆.串口.文件.窗口以及其他资源.如果一个线程独占了对某个资源的访问,其他线程就无法完成工作.我们也必须限制线程在任何时刻都能访问任何资源.比如在 ...

  9. 《windows核心编程系列 》六谈谈线程调度、优先级和关联性

    线程调度.优先级和关联性 每个线程都有一个CONTEXT结构,保存在线程内核对象中.大约每隔20ms windows就会查看所有当前存在的线程内核对象.并在可调度的线程内核对象中选择一个,将其保存在C ...

随机推荐

  1. Spring的Web MVC框架

    以下内容引用自http://wiki.jikexueyuan.com/project/spring/web-mvc-framework.html: Spring web MVC框架提供了模型-视图-控 ...

  2. Maven创建项目时出现Generating project in Interactive mode就一直卡住的解决方案

    使用maven命令在创建项目的时候出现 Generating project in Interactive mode 然后就一直卡住 网上搜做了很多解决方案 有说各种方案的,最后找到了一种.实验成功 ...

  3. Defcon 23最新开源工具NetRipper代码分析与利用

    0×01 研究背景 在分析了俄罗斯人被曝光的几个银行木马的源码后,发现其大多均存在通过劫持浏览器数据包来获取用户个人信息的模块,通过截获浏览器内存中加密前或解密后的数据包来得到数据包的明文数据.在De ...

  4. Win8系统如何关闭用户账户控制UAC

    按WIN+S,屏幕右侧出现搜索框,在搜索框中输入UAC,然后单击"更改用户账户控制设置"   然后把弹出的窗口改成"从不通知"就可以了  

  5. 微信小程序之 Index(仿淘宝分类入口)

    1.逻辑层 index.js //index.js //获取应用实例 const app = getApp() Page({ /** * 页面的初始数据 */ data: { menu: { imgU ...

  6. MyBatis -- sql映射文件具体解释

    MyBatis 真正的力量是在映射语句中. 和对等功能的jdbc来比价,映射文件节省非常多的代码量. MyBatis的构建就是聚焦于sql的. sql映射文件有例如以下几个顶级元素:(按顺序) cac ...

  7. url加密并计算时间

    将URL地址参数进行加密传输提高网站安全性 加密算法,直接调用就好 function keyED($txt,$encrypt_key){ $encrypt_key = md5($encrypt_key ...

  8. 嵌入式开发之davinci--- 8148/8168/8127 中的添加算饭scd 场景检测 文档简介

    Osd Scd (1)     Introduction over view a)         scene change detection block diagram a)         gr ...

  9. SQL Server中一些有用的日期sql语句

    SQL Server中一些有用的日期sql语句 1.一个月第一天的 SELECT DATEADD(mm, DATEDIFF(mm,0,getdate()), 0) 2.本周的星期一 SELECT DA ...

  10. HTML form表单的默认提交方式

    默认为Get,亲测.. key值为控件name属性值,如果没有 url中就没有此值 aspx中默认Form表单提交方式为post