Windows核心编程(十)

第十九章 DLL基础

动态链接库(dynamic-link library,DLL)一直以来都是Windows操作系统的基石。Windows应用程序编程接口(application programming inferface,API)提供的所有函数都包含在DLL中。其中三个最重要的DLL分别是:Kernel32.dll,包含的函数用来管理内存、进程及线程;User32.dll,包含的函数用来执行与用户界面相关的任务,如创建窗口和发送消息;GDI32.dll,包含的函数用来绘制图像和显示文字。

Windows还提花了其他一些DLL,用来执行更加专门的任务。例如,AdvAPI32.dll包含的函数与对象的安全性、注册表的操控以及事件日志有关。ComDlg32.dll包含了一些常用的对话框(如打开文件和保存文件),ComCtl32.dll支持所有常用的窗口控件。

一旦系统将一个DLL的文件映像映射到调用进程的地址空间中之后,进程中的所有线程就可以调用该DLL中的函数了。事实上,该DLL几乎完全丧失了它的DLL身份:对进程中的线程来说,该DLL中的代码和数据就像是一些附加的代码和数据,碰巧被放在进程地址空间中。当线程调用DLL中的一个函数的时候,该函数会在线程栈中取得传给它的参数,并使用线程栈来存放它需要的局部变量。此外,该DLL中的函数创建的任何对象都为调用线程或调用进程所拥有——DLL绝对不拥有任何对象。例如,如果DLL中的一个函数调用了VirtualAlloc,系统会从调用进程的地址空间中预订地址空间区域。如果稍后从进程的地址空间中撤销对DLL的映射,那么这块地址空间区域仍将保持被预订状态,因为虽然该区域事实是由DLL中的函数所预订的,但系统并不会对此进行记录。被预订的区域为进程所有,只有当线程调用了VirtualFree函数或者当进程终止的时候,该区域才会被释放。



启动一个可执行模块的时候,操作系统的加载程序会先为进程创建虚拟地址空间,接着把可执行模块映射到进程的地址空间中。之后加载程序会检查可执行模块的段,试图对所需的DLL进行定位并将它们映射到进程的地址空间中。由于导入段只包含DLL的名称,不包含DLL的路径。因此加载程序必须在用户的磁盘上搜索DLL。下面是加载程序的搜索顺序:
1)包含可执行文件的目录。

2)Windows的系统目录,该目录可以通过GetSystemDirectory得到。

3)16位系统目录,即Windows目录中的System子目录。

4)Windows目录,该目录可以通过GetWindowsDirectory得到。

5)进程的当前目录。

6)PATH环境变量中所列出的目录。

注意:对应用程序当前目录的搜索位于Windows目录之后,这个改变始于Windows XP SP2,其目的是为了防止加载程序在应用程序的当前目录找到伪造的系统DLL并将它们载入。


二十、DLL高级技术

1 显式地加载

在任何时候,进程中的一个线程可以调用下面两个函数来将一个DLL映射到进程的地址空间中:

HMODULE WINAPI LoadLibrary(
    LPCTSTR lpFileName
);
HMODULE WINAPI LoadLibraryEx(
    LPCTSTR lpFileName,
    HANDLE hFile,
    DWORD dwFlags
);
两个函数返回的HMODULE表示文件被映射到的虚拟内存地址。

LoadLibraryEx函数有两个额外的参数:hFile和dwFlags。参数hFile是为将来扩充保留的,现在必须将它设为NULL。参数dwFlags可以被设为0,或下列标志的组合:

1)DONT_RESOLVE_DLL_REFERENCES标志

告诉系统只需将DLL映射到调用进程的地址空间,不调用DllMain(DLL初始化函数),并且不载入该DLL中包含的其它的额外的DLL。

2)LOAD_LIBRARY_AS_DATAFILE标志

告诉系统将DLL作为数据文件映射到进程的地址空间中。系统不会花费额外的时间来准备执行文件中的任何代码。所以不能对这个DLL进行GetModuleHandle或GetProcAddress。通常在只是为了提取DLL中的资源或信息时而加载DLL时使用这个标志。由于.exe文件没有DllMain函数,使用LoadLibraryEx来载入.exe的时候,必须指定这个标志。

3)LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE标志

和LOAD_LIBRARY_AS_DATAFILE类似,不同之处在于DLL文件是以独占访问模式打开的,从而禁止任何其他应用程序在当前应用程序使用该DLL文件的时候对其进行修改。

4)LOAD_LIBRARY_AS_IMAGE_RESOURCE标志

与LOAD_LIBRARY_AS_DATAFILE相似,不同之处在于:当系统载入DLL的时候,会对相对虚拟地址(relative virtual address,RVA)进行修复,这样,RVA就可以直接使用,而不必根据DLL载入到的内存地址来对它们进行转换了。

5)LOAD_WITH_ALTERED_SEARCH_PATH标志

用来改变LoadLibraryEx在对指定的DLL进行定位时所使用的搜索算法。

6)LOAD_IGNORE_CODE_AUTHZ_LEVEL标志

用来关闭WinSafer所提供的验证,它是在Windows XP中引入的,其设计目的是为了对代码在执行过程中可以拥有的特权加以控制。Windows Vista中用户账户控制(UAC)已经取代了这项特性。


2 显式地卸载

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

BOOL FreeLibrary( HMODULE hLibModule);

VOID FreeLibraryAndExitThread( HMODULE hModule, DWORD dwExitCode);

第二个函数在Kernel32.dll中被实现,是为了应对以下情形:DLL在映射到地址空间中,这个DLL创建了一个线程,当线程完成了它的工作后,可以先后调用FreeLibrary和ExitThread,来从进程的地址空间中撤销对DLL映射并终止线程。但是,FreeLibrary会立即销毁对DLL的映射,等它返回的时候,调用ExitThread的代码已经不复存在了,线程试图调用不存在的代码,将引发违规,并导致整个进程被终止。而这个函数要执行的下一条指令仍然在Kernel32.dll中,而不是在已经被撤销映射的DLL中。

系统会在每个进程中为每个DLL维护一个使用计数,进程之间的引用计数是无关的。当进程第一次调用LoadLibrary来载入一个DLL的时候,系统会将DLL的文件映射到调用进程的地址空间中,并将DLL的使用计数设为1。如果同一个进程中的一个线程后来再调用LoadLibrary载入同一个DLL文件映像的时候,系统不会再次将DLL的文件映像映射到进程的地址空间中。它只是将进程中与该DLL对应的使用计数递增。

线程可以通过GetModuleHandle函数来检测一个DLL是否已经被映射到了进程的地址空间中。

混用LoadLibrary和LoadLibraryEx可能会导致同一个DLL映射到同一个地址空间中的不同位置。

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

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

参数pszSymbolName在函数原型中的类型为LPCSTR,意味着只能接受ANSI字符串。它也能接受序号来指定想要得到哪个符号的地址,并强烈不推荐。


3 DLL的入口函数

一个DLL可以有一个入口点函数。系统会在不同的时候调用这个入口点函数(进程或线程初始化或终止的时候,或调用LoadLibrary或FreeLibrary的时候),这些调用是通知性质的,通常被DLL用来执行一些与进程或线程有关的初始化和清理工作。如果DLL不需要这些通知,可以不必在源代码中实现这个入口点函数。如果想要在DLL中接收通知,那么我们可以像下面这样实现入口点函数:

BOOL WINAPI DllMain( HANDLE hinstDLL, DWORD dwReason, LPVOID lpvReserved);

第二个参数dwReason表示系统调用入口点函数的原因可能是下列4个值之一:DLL_PROCESS_ATTACH、DLL_THREAD_ATTACH 、DLL_THREAD_DETACH 或 DLL_PROCESS_DETACH。

 

当系统第一次将一个DLL映射到进程的地址空间中时,会调用DllMain函数,并传入DLL_PROCESS_ATTACH。创建新的进程时,主线程被创建之后系统会用这个线程调用每个DLL函数的DllMain函数,同时传入DLL_PROCESS_ATTACH。当所有已映射的DLL完成了对该通知的处理后,系统才会让进程的主线程开始执行可执行模块的C/C++运行时启动代码,然后执行可执行模块的入口点函数(_tmain或_tWinMain)。

当系统将一个DLL从进程的地址空间中撤销对象时,会调用DLL的DllMain函数,并传入DLL_PROCESS_DETACH。

当进程创建一个线程的时候,系统会检查当前映射到该进程的地址空间中的所有DLL文件映像,并用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数。这告诉DLL需要执行与线程相关的初始化。新创建的线程负责执行所有的DLL的DllMain函数中的代码。当系统将一个新的DLL映射到进程的地址空间中时,进程中已有的线程不会用DLL_THREAD_ATTACH调用该DLL的DllMain。新线程创建时,DLL还没有被映射到DLL进程的地址空间中,也不会用DLL_THREAD_ATTACH调用DllMain函数。系统不会让进程的主线程用DLL_THREAD_ATTACH值来调用DllMain函数,在进程创建的时候被映射进程地址空间中的任何DLL会收到DLL_PROCESS_ATTACH通知,但不会收到DLL_THREAD_ATTACH通知。

线程运行结束后会使得系统调用ExitThread来终止线程,但系统不会立即终止线程,而会让这个即将终止的线程用DLL_THREAD_DETACH来调用所有已映射DLL的DllMain函数。

如果线程A调用CreateThread来创建线程C的时候,系统会调用DLL_THREAD_ATTACH来调用某个DLL的DllMain函数。在C执行DllMain函数中的代码时,线程B调用CreateThread来创建线程D。系统必须再次用DLL_THEAD_ATTACH来调用DllMain,但这次是线程D来执行其中的代码。此时系统会对DllMain的调用序列化,它会将线程D挂起,直到线程C执行完DllMain中的代码并返回为止(其实就是将代码进行了线程同步)。


4 函数转发器

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

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

这个pragma告诉链接器,正在编译的DLL应该输出一个名为SomeFunc的函数,但实际实际SomeFunc的是另一个名为SomeOtherFunc的函数,该函数被包含在另一个名为DllWork.dll的模块中。必须为每个想要转发的函数单独创建一行pragma。

5 已知的DLL

系统对操作系统提供的某些DLL进行了特殊处理,这些DLL被称为已知的DLL(known DLL).除了操作系统在载入它们的时候总是在同一个目录中查找之外,它们与其他的DLL并没有不同。注册表中有一个注册表项:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs

这个注册表项包含了一组值名,这些值名是一些DLL的名称。当LoadLibrary或LoadLibraryEx被调用的时候,函数首先会检查我们传入的DLL的名字,如果名字中包含了.dll扩展名,那么这两个函数会先将扩展名去掉,然后再在KnownDLLs注册表中搜索,如果没有继续按照正常的搜索规则,否则就使用与值名相对应的数据,并试图用该数据来载入DLL。系统还会从这个注册表项的DllDirectory值所表示的目录中开始搜索DLL。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值