内存管理
编辑
在Win32中,DLL文件按照片段(sections)进行组织。每个片段有它自己的属性,如可写或是只读、可执行(代码)或者不可执行(数据)等等。这些section可分为两种,一个是与绝对地址寻址无关的,所以能被多进程公用;另一个是与绝对地址寻址有关的,这个就必须由每个进程有自己的副本专用。sections的这种二分类,在编译DLL时就已经由编译器、链接器给标注好了。所以在装入DLL时,装入器知道哪些sections在内存物理地址空间只需要有一份,供多个进程共用(映射到各个进程的内存逻辑地址空间,所以逻辑地址可以不同); 哪些sections必须是进程使用自己的专用副本。
也可在程序编译时通过编译选项/section 的S (Shared)属性,显式指定哪个节是跨进程共享的。[1]默认情况下,DLL的数据节都是写时复制(COW)。
具体说,DLL装入时需考虑下述情形:
局部变量——每个线程都有自己的栈,DLL内部的局部变量随所在函数被执行而在各自线程的调用栈上开辟存储空间。
全局变量
DLL内部定义的全局变量
const全局变量——放入const节中,但不是各个进程共享;因为进程加载DLL时会初始化只读全局变量的值,这个值由可能是依赖于所在的进程,如DLL的函数在该进程中的逻辑地址。
非const全局变量——放入各个进程各自专用的data节中。即DLL装入时各个进程复制一份自己专用的DLL的data节。但是,对于一个进程内的多个线程并发访问这种进程空间全局变量,仍然存在线程安全问题。例如,在一个COM的DLL加载入一个进程的空间后,该进程的多个线程可能会并发访问该COM库的COM对象。为此,Windows与COM引入了线程“套间”(apartment)技术。一个进程内,应用程序与加载的各个DLL分属于不同的Module,如果DLL使用所在Module的全局变量,例如动态链接MFC的regular dll在访问自己的MFC全局变量时,应该明确声明。
访问DLL以外定义的全局变量——使用间址技术,在DLL的data节中用一个指针数据类型的内存空间来保存一个外部全局变量的地址。
函数调用
调用DLL内部定义的函数。这不是问题。
调用DLL外部定义的函数。例如,DLL内部调用一个外部函数foo()。这个foo函数在进程1中可能实现为“四舍五入”,在进程2中实现为“下取整”。所以调用外部函数是各个进程私用的事情。解决办法是使用间址技术,在data节中用一个“函数指针”数据类型的内存空间来保存这种外部函数的入口地址。
跳转指令
DLL内部跳转,不是问题
跳转到DLL外部,解决同上述3.2
DLL代码段通常被使用这个DLL的所有进程所共享。如果代码段所占据的物理内存被收回,它的内容就会被放弃,后面如果需要的话就直接从DLL文件重新加载。
与代码段不同,DLL的数据段通常是私有的;也就是说,每个使用DLL的进程都有自己的DLL数据副本。作为选择,数据段可以设置为共享,允许通过这个共享内存区域进行进程间通信。但是,因为用户权限不能应用到这个共享DLL内存,这将产生一个安全漏洞;也就是一个进程能够破坏共享数据,这将导致其它的共享进程异常。例如,一个使用访客账号的进程将可能通过这种方式破坏其它运行在特权账号的进程。这是在DLL中避免使用共享片段的一个重要原因。
当DLL被如UPX这样一个可执行的packer压缩时,它的所有代码段都标记为可以读写并且是非共享的。可以读写的代码段,类似于私有数据段,是每个进程私有的并且被页面文件备份。这样,压缩DLL将同时增加内存和磁盘空间消耗,所以共享DLL应当避免使用压缩DLL。
符号解析和绑定
编辑
DLL输出的每个函数都由一个数字序号唯一标识,也可以由可选的名字标识。同样,DLL引入的函数也可以由序号或者名字标识。对于内部函数来说,只输出序号的情形很常见。对于大多数视窗API函数来说名字是不同视窗版本之间保留不变的;序号有可能会发生变化。这样,我们不能根据序号引用视窗API函数。
按照序号引用函数并不一定比按照名字引用函数性能更好:DLL输出表是按照名字排列的,所以对半查找可以用来在在这个表中根据名字查找这个函数。另外一方面,只有线性查找才可以用于根据序号查找函数。
将一个可执行文件绑定到一个特定版本的DLL也是可能的,这也就是说,可以在编译时解析输入函数(imported functions)的地址。对于绑定的输入函数,连结工具保存了输入函数绑定的DLL的时间戳和校验和。在运行时Windows检查是否正在使用同样版本的函式庫,如果是的话,Windows将绕过处理输入函数;否则如果函式庫与绑定的函式庫不同,Windows将按照正常的方式处理输入函数。
绑定的可执行文件如果运行在与它们编译所用的环境一样,函数调用将会较快,如果是在一个不同的环境它们就等同于正常的调用,所以绑定输入函数没有任何的缺点。例如,所有的标准Windows应用程序都绑定到它们各自的Windows发布版本的系统DLL。将一个应用程序输入函数绑定到它的目的环境的好机会是在应用程序安装的过程。
运行时通知DLL进程/线程加载
编辑
进程/线程加载时,可以通过DllMain函数通知DLL相关信息,提供对应处理的机会。
BOOL WINAPI DLLMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID fImpLoad)
{
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
//当这个DLL第一次被映射到了这个进程的地址空间时。DLLMain函数的返回值为FALSE,说明DLL的初始化没有成功,系统就会终结整个进程,去掉所有文件映象,之后显示一个对话框告诉用户进程不能启动。
break;
case DLL_THREAD_ATTACH:
//一个线程被创建,新创建的线程负责执行这次的DllMain函数。系统不会让进程已经存在的线程以DLL_THREAD_ATTACH的值来调用DllMain函数。主线程永远不会以DLL_THREAD_ATTACH的值来调用DllMain函数。系统是顺序调用DllMain函数的,一个线程执行完DllMain函数才会让另外一个线程执行DllMain函数。
break;
case DLL_THREAD_DETACH:
//如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread)。线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
break;
case DLL_PROCESS_DETACH:
//这个DLL从进程的地址空间中解除映射。如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。
break;
}
return(TRUE);
}
动态链接库搜索顺序
编辑
对于Windows,加载动态链接库时:
如果内存中已经有同module名的DLL,除非是DLL redirection或manifest,否则直接就用内存中这个DLL而不再搜索。
如果DLL名字属于当前Windows版本的Known DLL,则必须用Known DLL。清单见 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs.
如果DLL有依赖DLL,操作系统按缺省标准规则根据module名字搜索依赖DLL。即使第一个DLL指定了全路径。
Windows Desktop应用程序的DLL标准搜索序:
应用程序所在目录;
系统目录。GetSystemDirectory函数返回该目录。
16比特系统目录;
Windows目录。使用GetWindowsDirectory函数返回该目录。
当前(工作)目录;
环境变量PATH中列出的目录。
如果SafeDllSearchMode被禁止,则当前目录成为第二个被搜索的目录。
Windows Desktop应用程序的DLL替代搜索序。这包括两种情况:
LoadLibraryEx函数使用参数LOAD_WITH_ALTERED_SEARCH_PATH:LoadLibraryEx函数不是从应用程序所在目录开始,而是从参数lpFileName(即要加载的dll)所在目录开始搜索所有依赖DLL。
用SetDllDirectory函数修改搜素序。
当所有DLL都完成搜索,要开始执行DLL的初始化时,替代搜索序结束,恢复标准搜索序。
运行时显式链接
编辑
对于一个进程,每個被加载的DLL在进程的PEB中有一個引用計數器,在当前进程中每多一次加载该计数器便加一。LoadLibrary 與 FreeLibrary 指令影響每一個行程內含的計數器;動態連結則不影響。因此藉由呼叫 FreeLibrary 而从記憶體卸载一 DLL 是很重要的。一個行程可以從它自己的 VAS 註銷此計數器。该计数器变为0时,这个DLL从当前进程中卸载,但并不意味着会从物理内存中卸载,因为其它进程可能使用这个DLL。
DLL 文件能够在运行时使用 LoadLibrary(或者 LoadLibraryEx)API 函数进行显式调用,这个的过程微软简单地称为运行时动态调用。如果在上述API的参数中指明了库文件的全路径,则不再搜索这个库文件。API 函数GetProcAddress 查找具有某名称的输出函数、FreeLibrary卸载 DLL(实际上是引用计数器减一)。这些函数类似于 POSIX 标准 API 中的 dlopen、dlsym、和 dlclose。
注意微软简单称为“运行时动态链接”的运行时隐式链接,如果不能找到链接的 DLL 文件,Windows 将提示一个错误消息并且调用应用程序失败(准确地说是不能创建进程)。应用程序开发人员不能通过编译链接来处理这种缺少 DLL 文件的隐式链接问题,而且在更改了实现后还必须重新编译链接整个程序——默认的增量链接模式会一直保留着那条缺少的函数引用,只有重新编译链接才能去掉。相反,虽然显式链接的代码量增多了,但开发人员有机会提供一个完善的出错处理机制。
运行时显式链接的过程在所有语言中都是相同的,因为它依赖于 Windows API 而不是语言结构。只要一种语言能够调用上述的 LoadLibrary 等函数,就能执行运行时显示链接。
Win16下的DLL
编辑
16位Windows,所有进程共用同一个内存地址空间。如果一个DLL被多个进程使用,它只被加载到内存中一次,只有一份数据节(data segment)。也就是说,DLL是系统全局而不是进程内的;进程不能得到DLL的一份独立的拷贝。每个DLL在内存中只有一份实例(instance)。[2]
如果一个EXE文件同时执行多次,那么在内存中只有该EXE的一套只读拷贝(如代码或资源),但这个EXE的每个进程都有自己的一套数据段,即有多份实例(instance)。实际上,进程的实例句柄就是data segment(存放进程的全局变量)的内存起始地址。
模块(module)是指一个硬盘文件,可被加载到内存中。模块句柄是一个数据结构,表示这个硬盘文件的各部分(section)出自哪里,是否已经加载到内存中。
所以进程只能用实例句柄标识,而不能用模块句柄标识。
Win32的进程使用自己专用的逻辑内存空间。进程的全局变量不再是跨进程边界可见的。实例句柄与模块句柄相同,都是指向模块加载后的内存基地址。这样规定实际上也兼容了Win16时实例句柄与模块句柄的含义。
资源加载
编辑
EXE和DLL都有其自己的资源(如对话框资源),而且这些资源的ID可能重复,默认使用EXE的资源。如果需要加载、使用DLL中的资源,需要通过DLL加载后的实例句柄(HINSTANCE)来找到DLL的资源。
应用程序进程本身及其调用的每个DLL模块都具有一个全局唯一的HINSTANCE句柄,它们代表了EXE或DLL模块在进程逻辑地址空间中的起始地址。进程本身的模块句柄一般为0x400000,而DLL模块默认加载地址为0x10000000。如果程序同时加载了多个DLL,则每个DLL模块都会有不同的HINSTANCE。
几种可行的办法:
方法1:
// in MFC DLL
void CDLL::ShowDlg(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框
dlg.DoModal();
}
AFX_MANAGE_STATE(AfxGetStaticModuleState());必须作为接口函数的第一条语句。功能是在栈上创建一个AFX_MAINTAIN_STATE2类的实例,利用其构造函数和析构函数对_afxThreadState.GetData()所指向内存存储块的模块状态(AFX_MODULE_STATE类型)设置现场(AfxGetModuleState函数返回的值)及恢复现场。
方法2:
// in MFC DLL
void CDLL::ShowDlg(void)
{
HINSTANCE save_hInstance = AfxGetResourceHandle(); //当前资源句柄
AfxSetResourceHandle(theApp.m_hInstance); //设置为当前DLL的实例句柄所对应的资源句柄
CDialog dlg(IDD_DLL_DIALOG); //打开指定ID的对话框
dlg.DoModal();
AfxSetResourceHandle(save_hInstance);
}
方法3:
// in EXE
void CEXE::OnButtonClick()
{
HINSTANCE exe_hInstance = GetModuleHandle(NULL);
HINSTANCE dll_hInstance = GetModuleHandle("SharedDll.dll");
AfxSetResourceHandle(dll_hInstance); //切换状态
ShowDlg();
AfxSetResourceHandle(exe_hInstance); //恢复状态
}