Windows核心编程笔记(十七) DLL高级技术

来源:互联网 发布:手机淘宝开店没反应 编辑:程序博客网 时间:2024/06/05 02:13

20.1 DLL模块的显式载入和符号链接

20.1.1 显式载入DLL模块

(1)构建DLL时,如果至少导出一个函数/变量,那么链接器会同时生成一个.lib文件,但这个文件只是在隐式链接DLL时使用(显示链接时并没有用到这文件)

(2)显式载入DLL的函数:LoadLibrary(Ex)

  参数

含义

pCTSTR pszDllPathName

LoadLibrary只有这个参数。函数会根据第19章介绍的搜索算法在用户的计算机中对DLL文件进行定位,并映射到进程的地址空间。

HANDLE hFile

该参数为将来扩充所保留的,这里必须为NULL

DWORD dwFlags

可为0或下列标志的组合

①DONT_RESOLVE_DLL_REFERENCES:只将该DLL映射到进程地址空间,但不调用DllMain函数及不检查该DLL导入段中的其他额外DLL,这也意味着不自动载入额外的DLL。(一般应避免使用该标志,因为代码所依赖的DLL可能尚未被载入!)

②LOAD_LIBRARY_AS_DATAFILE:将DLL作为数据文件映射到进程。(一般用在一个DLL只包含资源而没有函数时或想用一个EXE文件中包含的资源时可用这个标志,而且载入EXE时必须使用这个标志

③LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE:与②标志相似,唯一不同的是以独占方式来打开这个DLL文件,以防止其他程序对其修改。

④LOAD_LIBRARY_AS_IMAGE_RESOURCE:与②标志相似,但不同之处在于当系统载入DLL的时候,会对相对虚拟地址(RVA)进行修复。这样RVA就可以直接使用,而不必再根据DLL载入的内存地址来转换了。(当需要对DLL进行遍历其PE段时,这个标志特别有用)

⑤LOAD_IGNORE_CODE_AUTHZ_LEVEL:用来关闭UAC对代码在执行过程中可以拥有的特权加以控制。

⑥LOAD_WITH_ALTERED_SEARCH_PATH:用来改变LoadLibrary对DLL文件进行定位所使用的搜索算法。

 A、如果pszDllPathName不包含“\”字符,会使用标准搜索路径算法

 B、如果pszDllPathName包含“\”会因全路径(网络共享路径)或相对路径而有所不同。(见课本P557)

 C、可以调用SetDllDirectory改变搜索算法,搜索指定的目录路径。(具体顺序为:EXE所在目录→SetDlldirectory设置的文件夹→Windows系统目录→16位Windows系统目录→Windows目录→PATH列出的目录。

返回值

HMODULE类型,等价于HINSTANCE,表示DLL被映射到的虚拟内存地址。当返回NULL,表示映射失败,可进一步调用GetLastError

(3)混用LoadLibrary和LoadLibraryEx加载同一个DLL可能带来的问题

【情况1】:不会出现问题,此时hDll1=hDll2=hDll3

HMODULE hDll1 = LoadLibrary(TEXT("MyLibrary.dll"));HMODULE hDll2 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,LOAD_LIBRARY_AS_IMAGE_RESOURCE);HMODULE hDll3 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,LOAD_LIBRARY_AS_DATAFILE);

【情况2】:将上面的调用顺序改变一下,则hDll1≠hDll2≠hDll3,说明DLL被多次映射到进程的地址空间。

HMODULE hDll1 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,LOAD_LIBRARY_AS_DATAFILE);HMODULE hDll2 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,LOAD_LIBRARY_AS_IMAGE_RESOURCE);HMODULE hDll3 = LoadLibrary(TEXT("MyLibrary.dll"));

【分析原因】当LoadLibraryEx时(使用LOAD_LIBRARY_AS_DATAFILE, LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE, or LOAD_LIBRARY_AS_IMAGE_RESOURCE标志),系统会检测该DLL是否被LoadLibrary(或未使用上述标志LoadLibraryEx)进来,如果己经被载入过,那么函数会返回空间中DLL原先被映射的地址。如果DLL未被载入,那么DLL会将这个DLL载入,但会认为是个未完全载入的DLL,如果这时再载入时会被多次的映射到进程的地址空间,从而产生不同的地址。

20.1.2 显式卸载DLL模块

(1)BOOL FreeLibrary(HMODULE hInstDll);

(2)VOID FreeLibraryAndExitThread(hInstDll,dwExitCode);

  ①函数的内部实现(在Kernel32.dll中):

VOID FreeLibraryAndExitThread(HMODULE hInstDll,DWORD dwExitCode){     FreeLibrary(hInstDll);     ExitThread(dwExitCode);//调用该行指令在kernel32.dll}

  ②为什么需要FreeLibraryAndExitThread函数?

  A、假设在A这个DLL里创建一个线程,当该线程完成一些工作后,先调用FreeLibrary再调ExitThread来撤销对DLL的映射并终止线程,这里会出现一个严重的问题。因为FreeLibrary会立即从进程的地址空间撤销对DLL的映射。当FreeLibrary返回时,线程会试图调用ExitThread,而这行代码本来在DLL里的,这个DLL己经不存在了,这时线程会试图执行不存在的代码,将引发访问违规,并导致整个进程被终止。

  B、但如果调用FreeLibraryAndExitThread,由于函数内部会调用FreeLibrary和ExitThread,而这两个函数是在Kernel32.dll的内部调用(而不是A这个DLL)。所以当撤销了A这个DLL后,这个线程可以继续执行ExitThread,只不过当ExitThread时线程不再返回A这个DLL里了,所以也不会出错。

(3)DLL的使用计数问题

  ①当LoadLibrary(Ex)时使用计数递增,第一个Load时使用计数为1.如果同一个进程的一个线程再调用LoadLibrary时,系统不会再次进行映射而是将使用计数递增。

  ②FreeLibrary或FreeLibraryAndExitThread使计数递减,但计数递减到0时,系统会这个DLL从进程的地址空间中撤销映射。

  ③系统为每个进程的每个DLL维护一个使用计数。如进程A和B都加载了MyLib.dll,那么这个DLL会被映射进两个进程的地址空间,但该DLL在进程A和B中的使用计数都是1如果后来进程B的一个线程再次LoadLibrary这个DLL,则进程B中这个DLL的使用计数为2,但进程A中仍为1。

(4)检测DLL的两个函数

  ①检测DLL是否被映射:HMODULE GetModuleHandle(PCTSTR pszModuleName);

    A、返回NULL时表示未被映射。

    B、如果传NULL参数时,会返回应用程序的EXE文件的句柄。

  ②获得DLl/EXE的全路径名:GetModuleFileName(hInstModule,pszPathName,cchPath);其中的hInstModule为DLL或EXE的句柄。

20.1.3 显示链接到导出符号——获得函数地址:GetProcAddress

  参数

描述

HMODULE hInstDll

DLL的句柄,即先前调用LoadLibrary(Ex)或GetModuleHandle时的返回值。

PCSTR pszSymbolName

函数的名称或序号,注意这里的类型是PCSTR,而不是PCTSTR,说明这个函数只接受ANSI的字符串。

返回值

FARPROC,要获得的函数的地址,须转为该函数原型的指针。一般用typedef来声明要获取的函数原型的指针

20.2 DLL的入口点函数

复制代码
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) {   switch (fdwReason) {      case DLL_PROCESS_ATTACH:         // DLL第1次被映射到进程的地址空间时         break;      case DLL_THREAD_ATTACH:         // 创建一个线程时         break;      case DLL_THREAD_DETACH:         // 线程终止时         break;      case DLL_PROCESS_DETACH:         // 进程撤销一个DLL映射时         break;   }   return(TRUE); // 该返回值只在 DLL_PROCESS_ATTACH通知时有用,用来表示DLL的初始化是否成功,如果return FALSE表示加载DLL失败,如果系统会终止整个进程或撤销对该DLL的映射。其他通知时,系统将忽略这个返回值。}
复制代码

20.2.1 DLL_PROCESS_ATTACH通知

 

(1)只有当DLL第1次被映射到进程地址空间时,才会发送该通知。如果以后一个线程再调用LoadLibrary(Ex)来载入这个DLL,则只会递增该DLL的使用计数,但不会再发该通知。

(2)在该通知里,一般用来执行与进程相关的初始化,如创建DLL中一般函数要使用的堆。

(3)当DllMain处理DLL_PROCESS_ATTACH通知时,返回值用来表示DLL的初始化是否成功。如果return FALSE表示加载DLL失败,系统会终止整个进程(这种情况发生在刚创建进程时)撤销对该DLL的映射(这种情况发生在显式调用LoadLibrary(Ex)时)

(4)这个通知由进程中的某个线程来调用的如果是刚创建新的进程时,则由主线程调用如果某个线程调用LoadLibrary第1次显式载入这个DLL时,则由这个线程来调用执行这个通知,然后线程继续正常执行其他任务,如果return FALSE表示初始化失败,系统会撤销对DLL的映射并让LoadLibrary(Ex)返回NULL。

20.2.2 DLL_PROCESS_DETACH通知

 

(1)当系统将一个DLL从进程的地址空间中撤销映射时,发送该通知。(注意:在处理DLL_PROCESS_ATTACH时如果的返回FALSE时,那么就不会收到DLL_PROCESS_DETACH通知。)

(2)如果在处理DLL_PROCESS_ATTACH时返回FALSE,则DllMain就不会收到DLL_PROCESS_DETACH通知。

(3)如果是因调用ExitProcess而导致撤销DLL映射,则调用ExitProcess函数的线程负责执行DllMain函数的代码

(4)如果是因线程调用了FreeLibrary(或FreeLibraryAndExitThread)而撤销Dll映射,该线程将执行DllMain函数中的代码。该线程会直到处理完DLL_PROCESS_DETACH完才从FreeLibrary中返回。因此,如果该通知时死循环,则阻碍线程的终止,只有当每个DLL都处理完该通知后,操作系统才会真正地终止进程。

(5)如果某个线程调用TerminateProcess来终止进程,则系统不会发送DLL_PORCESS_DETACH通知。这意味着Dll没有机会执行一些清理代码的操作。因此,不到万不得己,应避免使用TerminateProcess函数。

20.2.3 DLL_THREAD_ATTACH通知

(1)当进程创建一个线程时,系统会向当前映射到该进程地址空间中的所有DLL发送DLL_THREAD_ATTACH通知。告诉这些DLL执行一些与线程相关的初始化。新创建的线程负责执行所有DLL中DllMain函数中相关的代码。只有当所有DLL完成了对该通知的处理后,新线程才会开始执行它的线程函数。

(2)当一个新的DLL映射到进程地址空间时,进程中己经有的线程不会是不会收到DLL_THREAD_ATTACH通知的。(即只有在创建新线程时,己经被映射到进程地址空间中的DLL才会收到这个通知

(3)因创建进程时,何任被映射到进程地址空间中的DLL都会收到DLL_PROCESS_ATTACH通知,并由主线程负责执行,这里就可以执行一些相关的初始化工作,所以系统不会让主线程用DLL_THREAD_ATTACH来调用DllMain函数即主线程只接收DLL_PROCESS_ATTACH通知,而不接收DLL_THREAD_ATTACH通知

20.2.4 DLL_THREAD_DETACH通知

(1)当线程函数返回后,系统会调用ExitThread来终止线程,但在终止前,这个线程会用DLL_THREAD_DETACH去调用所有己映射DLL的DllMain函数。告诉DLL执行与线程相关的清理操作(如C/C++运行库在这里可释放多线程应用程序的数据块)。

(2)如果该通知里有死循环,将妨碍线程的终止。只有当每个DLL都处理完DLL_THREAD_DETACH通知后,操作系统才会真正的终止线程。

(3)如果某个线程调用了TerminateThread来终止线程,那会系统将不会发送DLL_THREAD_DETACH通知给线程。这意味着DLL没有机会执行任何清理操作。

(4)如果在撤销一个DLL映射时,还有其他线程(正在运行),系统不会发送DLL_THREAD_DETACH给这些线程。(即这些线程不会用DLL_THREAD_DETACH来调用这个DLL的DllMain)

【注意】上面的规则可能会出现一个情况:当进程中的一个线程调用LoadLibrary来载入一个DLL时,系统会用DLL_PROCESS_ATTACH来调用该DLL的DllMain(但该线程不会得到DLL_THREAD_ATTACH通知)。接着,这个载入DLL的线程退出,这时该线程会收到DLL_THREAD_DETACH通知。由于这个原因,当进行与线程相关的清理里必须极为小心。一般调用LoadLibrary与调用FreeLibrary的线程应该是同一个线程。





20.3 延迟载入DLL

20.3.1延迟载入的目的

(1)如果应用程序使用了多个DLL,那么它的初始化可能比慢,因为加载程序要将所有必需的DLL映射到进程的地址空间。→利用延迟加载可将载入过程延伸到执行过程时

(2)如果我们的代码调用的操作系统的一个新函数,但程序又试图在老版本的操作系统运行。这时程序会被终止。这时可以有两种解决方法:一种是判断利用GetVersionEx操作系统,在老系统中使用旧函数而不使用新函数。另一种是通过延迟载入,通过SEH来捕获异

20.3.2延迟载入技术

(1)延迟载入是针对隐式链接DLL的

(2)一个导出了字段(如即全局变量)的DLL是无法延迟载入的

(3)Kernel32.dll模块是无法延迟载入的,因为必须载入该模块才能调用LoadLibrary和GetProcAddress。

(4)不应在DllMain入口函数中调用一个延迟载入的函数,这可能导致程序崩溃

20.3.3 使用方法及相关说明

 (1)使用方法

  ①常规建立DLL和可执行模块

  ②链接可执行模块时,添加延迟加载开关

  A.为了延迟加载Dll,还需要在解决方案的该项目“属性”->“配置属性”->“链接器”->“输入”->“延迟加载的Dll”中输入MyDll.dll(注意/DelayLoad:MyDll.dll这个开关不能用#pragma comment(linker, "/DelayLoad:MyDll.dll")来设置。

  B.增加/Lib:DelayImp.lib开关:这可以用#include <delayimp.h>和#pragma comment(lib, "Delayimp.lib")。这个开关告诉链接器将delayimp中的__delayLoadHelper2函数嵌入到我们的可执行文件中。

  C.如果需要手动卸载Dll,则需在可选“链接器”→“高级”中指定“卸载延迟加载的DLL”中输入“MyDll.dll”。但要注意两点:一是卸载时只能调用__FUnloadDelayLoadedDll2(PCSTR szDll)函数,而不能调用FreeLibrary。二是该卸载操作是可选的,不是必需的,只有在需要手动卸载Dll时才设置。

 (2)/Lib:DelayImp.lib此时链接器将执行下列的事项

  ①将MyDll.dll从.exe的导入段去除,这样操作系统就不会隐式载入该DLL

  ②在.exe中嵌 入一个新的延迟载入段(Delay Import Section,称为.didata)表示要从MyDll.dll中导入哪些函数。

  ③对延迟载入函数的调用会跳转到__delayLoadHelper2函数,来完成对延迟载入函数的解析。

(3)其他说明

  ①应用程序对延迟载入函数的调用实际上会调用__delayLoadHelper2函数,该函数会引用那个特殊的延迟载入段,并用LoadLibrary和GetProcAddress得到延迟载入函数的地址,然后修改对该函数的调用,这样以后将直接调用该延迟载入函数。

  ②同一个DLL中的其他函数仍然必须在第一次被调用的时修复,即其他函数第1次调用时仍然会LoadLibrary+GetProcAddess并修复函数地址。

 【Export/ImportDelay程序】演示延迟载入Dll



20.4 函数转发器

(1)函数转发器原理(下图是利用Dependency Walker打开Kernel32.dll得到)

 

  ①图中CloseThreadpool*等4个函数转发到NTDLL中相应的函数中去了,但我们调用CloseThreadpool*等函数时,exe会被动态地链接到Kernel32.dll。当执行exe时,加载程序会发现被转发的函数实际上在NTDLL.dll中,然后它会将NTDLL.dll模块一并载入。

  ②当我们调用CloseThreadpool*函数时,那么调用GetProcAddress先在Kernel32的导出段是查找,并发现CloseThreadpool*是一个转发器函数,于是它会递归调用GetProcessAddress,在NTDLL  的导出段中查找相应的函数。

(2)实现自己的函数转发器

   #pragma comment(linker,"/export:MyFunc =OtherDll.OtherFunc")

   //即正在编译的Dll输出一个名为MyFunc的函数,但实际上这个函数在另一个叫OtherDll.dll模块中,函数名为OtherFunc.

20.5 己知的DLL

(1)操作系统对某些DLL进行了特殊处理,这些Dll被称为己知的Dll。在载入它们的时候,总是从"%SystemRoot%\System32"目录下查找。这些Dll被记录在注册表中如下的位置

 

(2)当LoadLibrary(TEXT("A_Dll"))时,系统会用正常的搜索规则来定位这个Dll。但如果调用LoadLibrary(TEXT("A_Dll.dll"))时,系统会先将扩展名.dll去掉,然后在注册表查找名称为"A_Dll"的一项,并将载入该项后面“数据”项里指向的Dll(注意,这个Dll会在"%SystemRoot%\System32"目录里查找),如果载入不成功,会返回NULL,GetLastError将返回(ERROR_FILE_NOT_FOUND).

 20.6 DLL重定向

(1)早期Windows为了文件共享,将多个应用程序共享的所有模块都放在Windows系统目录中,但会出现一个严重问题,因为安装程序会用老版本的文件覆盖这个目录中的文件,从而妨碍其他应用程序的正常运行。

(2)从Windows2000开始,新增了DLL重定向特性,使得应用程序首先从应用程序的目录中载入模块,只有当加载程序无法找到这个文件时,才会在其他目录中搜索。

(3)为了强制加载程序先检查应用程序的目录,可以在应用程序目录中,建一个文件名为AppName.local的文件(此处的AppName如MyApp.exe),内容无关紧要。

(4)LoadLibrary(Ex)在内部做了修改,来检查这个文件是否存在,如果应用程序目录中存在这个文件,便载入这个目录中的模块。如果不存在该.local文件,则工作方式与以往相同。

(5)由于安全性缘故,该特性默认是关闭的,因为它会使系统从应用程序的文件夹中载为伪造的系统DLL,而不是从Windows的系统文件夹中载入真正的系统DLL。为打开这个特性,可以HKLM\Software\Microsoft\WindowsNT\CurrentVesion\Image File Execution Options注册表项中增一个项名为DevOverrideEnable的DWORD型项,并将值设为1。

20.7 模块的基地址重定位

(1)基址重定位的原因

int g_x;void Func(){    g_x = 5;  //该行很重要,当编译和链接该行成会生成Mov [0xXXXXXXXX],5之类的代码}

  ①一般EXE的首选基地址为0x00400000,DLL为0x1000000。当运行exe时,加载程序为进程创建一个虚拟地址空间,并将exe映射到0x00400000处,g_x变量的变成0x00414540之类的固定地址。而当该这些代码是位于DLL模块时,这个地址将被固定为类似0x10014540之类的地址(前提是该DLL被加载到首选地址处)。

  ②对于EXE来说,会被加载到首选基地址处而不会造成冲突。但DLL则不一样,如果有两个DLL的首选基地址都是0x10000000。加载程序会将第1个DLL加载到首选地址上,但会对第2个DLL模块进行重定位(如加载到0x20000000),则g_x的地址会被修改为0x20014540,即汇编代码为Mov [0x20014540],5。

(2)EXE或(Dll)重定位的缺点

  ①当链接器在构建模块时,会将重定位段(relocation section)嵌入在生成的文件中。重定位表中的有用数据是那些需要重定位机器码所使用到的内存地址的偏移量。

  ②如果加载程序无法将模块载入到它的首选基地址,那么系统会遍历重定位段中的所有条目,对每一个条目,加载程序会先找到机器指令的那个存储页面。然后将模块的首选基地址减去实际映射地址,将这个差值加到机器指令使用的地址上。因为加载程序必须遍历重定位段并修改模块中的大量代码,这个过程会牺牲程序的启动时间。

  ③当加载程序写入模块的代码页面中时,系统的写时复制会强制这些页面以系统的页交换文件为后备存储器。因为页交换文件是所有模块(如EXE或DLL)的代码页面的后备存储器,所以这也会损害性能,减少可供系统中所有进程使用的存储器的数量。

(3)指定DLL基地址的方法

  ①方法1:在“配置属性”→“链接器”→“高级”→“基址”输入如0x20000000之类。同时“固定基址”这项选项“是(/Fixed)”。注意,为了少地址空间碎片,应该总是先从高内存地址开始载入DLL,然后再到低内存地址。

  ②方法2:

  A.创建一个文件文件,在其中按照下述语法指定每个DLL的基地址和大小(大小可选)

       key  address [size]  ;comment

      其中key是一个字母数字组成的字符串,不区分大小写;通常是Dll的名称,但不必非是;只要和在/BASE中指定一样就行。Address是十六进制或十进制表示的基址。Size是可选的,一般为最大DLL的size。

  B.将这个文本文件放到链接器可以搜到的地方

  C.在动态库项目的基址选项中指定@filename,key格式的命令,其中@是固定前缀,filename就是刚才的文本文件,可以指定完整路径名,key就是文本文件中指定的key

【CustomDLL程序】演示修改写DLL入口点函数及用方法2指定基地址

 


0 0
原创粉丝点击