动态链接库
windows应用程序编程接口(application programming interface,API)所提供的所有函数都包含在DLL中。
DLL | 描述 |
---|---|
kernal32.dll | 包含的函数用来管理内存、进程以及线程 |
User32.dll | 包含的函数用来执行与用户界面相关的任务 |
GDI32.dll | 包含的函数用来绘制图像和显示文字 |
AdvAPI32.dll | 包含的函数与对象的安全性、注册表的操控以及事件日志有关 |
ComDlg32.dll | 包含了一些常用的对话框 |
ComCtl32.dll | 支持所有常用的窗口控件 |
使用DLL的优势
- 扩展了应用程序的特性
- 简化了项目管理
- 有助于节省内存
- 促进了资源的共享
- 促进本地化
- 有助于解决平台间的差异
- 用于特殊目的
DLL和进程的地址空间
DLL通常由一组可供任何应用程序使用的独立函数组成。在DLL中,通常没有用来处理消息循环或创建窗口的代码。
在应用程序调用DLL之前,必须将该DLL的文件映像隐射到进程的地址空间中。隐式加载时链接(implicit load-time linking)或显示加载时链接(explicit run-time linking)
一旦系统将一个DLL的文件映射到调用进程的地址空间之后,进程中的所有线程就可以调用该DLL中的函数了。
事实上,该DLL几乎完全丧失了它的DLL身份:对进程中的线程来说,该DLL中的代码和数据就像是一些附加的代码和数据,碰巧被加载到进程地址空间中。当线程调用DLL中的一个函数的时候,该函数会在线程栈中取得传给它的参数,并使用线程栈来存放它需要的局部变量。此外,该DLL中的函数创建的任何对象都为调用线程或调用进程所拥有–DLL绝对不会拥有任何对象。
如果运行同一个可执行文件的多个实例,这些实例将不会共享可执行文件中的全局变量和静态变量。windows通过写时复制机制保证。
DLL中的全局变量和局部变量也是如此。一个进程将一个DLL映像文件映射到自己的地址空间中时,系统也会为全局变量和静态变量创建新的实例。
DLL加载方式
显示加载
HMODULE LoadLibrary(PCTSTR pszDllPathName);
HMODULE LoadLibraryEx(PCTSTR pszDllPathName,
HANDLE hFile,
DWORD dwFlags)
返回值HMOUDLE等价于HINSTANCE,LoadLibraryEx中提供了额外的两个参数,hFile是保留值,需要设定为NULL。参数dwFlags可以被设置为0,或者以下标识:
dwFlags | 描述 |
---|---|
DONT+RESOLVE_DLL_REFERENCES | 让系统只映射文件映像,但不调用DLLMain |
LOAD_LIBRARY_AS_DATAFILE | 无法调用DLL中的函数,加载资源DLL,或者加载EXE中的资源 |
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE | 禁止其他应用程序在当前应用程序使用该DLL文件的时候,对其进行修改 |
LOAD_LIBRARY_AS_IMAGE_RESOURCE | 会对虚拟地址进行修改 |
LOAD_WITH_ALTERED_SEARCH_PATH | 用来改变LoadLibraryEx在对指定的DLL进行定位时所使用的搜索算法 |
LOAD_IGNORE_CODE_AUTHZ_LEVEL | 关闭WinSafer所提供的验证 |
显示卸载DLL模块
VOID FreeLibrary(HMODULE hInstDLL);
VOID FreeLibraryAndExitThread(HMODULE hInstDll, DWORD dwExitCode){
FreeLibrary(hInstDll);
ExitThread(dwExitCode);
}
FreeLibraryAndExitThread调用情景:在一开始被映射到进程的地址空间时,这个DLL会创建一个线程。当线程完成了它的工作后,可以先后调用FreeLibrary和ExitThread,来从进程的地址空间中撤销对DLL的映射并终止线程。
但是如果分别调用FreeLibrary和ExitThread函数,会出现一个严重的问题,就是FreeLibrary会立即从进程的地址空间中撤销对DLL的映射。就是说,当FreeLibrary返回时,调用ExitThread的代码已经不复存在了,线程试图执行的是不存在的代码。
FreeLibraryAndExitThread函数会先调用FreeLibrary,这使得对DLL的映射会立即被撤销,要执行的下一条指令仍在Kernel32.dll中,而不是在已经被撤销映射的DLL中了。
DLL引用
当我们第一次调用LoadLibrary来载入一个DLL的时候,系统会将DLL的文件映像映射到调用进程的地址空间中,并将DLL的使用计数设为1。如果同一个进程中的一个线程后来再调用LoadLibrary来载入同一个DLL文件映像的时候,系统不会再次将DLL的文件映射到进程的地址空间中。它只是将进程中该DLL对应的引用计数递增。
同理,FreeLibrary调用会撤销对该DLL文件映像的映射。当系统发现DLL的引用计数递减为0时,会从进程的地址空间中撤销对该DLL文件映像的映射。如果任何线程再试图调用该DLL中的函数,那将引发访问违规。
线程可以调用GetModuleName函数来检测一个DLL是否已经被映射到了进程的地址空间中。
HMODULE GetModuleHandle(PCTSTR pszModuleName);
HMODULE GetModuleFileName(HMODULE hInstModule, PTSTR pszPathName, DWORD cchPath);
混用LoadLibrary和LoadLibraryEx可能会导致将同一个DLL映射到同一个地址空间中的不同位置!!!
DLL入口点函数
一个DLL可以有一个入口点函数。系统会在不同的时候调用这个函数。这些调用是通知性质的,通常被DLL用来执行一些与进程或线程有关的初始化或清理工作。
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, PVOID fimpLoad){
switch(fdwReason){
case DLL_PROCESS_ATTACH:
// the DLL is being mapped into the process' address space.
break;
case DLL_THREAD_ATTACH:
// A thread is being created.
break;
case DLL_THRAD_DETACH:
// A thread is exiting cleanly.
break;
case DLL_PROCESS_DETACH:
// the DLL is being unmapped from the process' address space.
break;
}
return TRUE; // Used only for DLL_PROCESS_ATTACH
}
HInstance表示一个虚拟内存地址,DLL的文件映像就被映射到进程地址空间中的这个位置。如果DLL是隐式载入,参数fImpLoad的值将不为零,如果DLL是显示载入,那么fImpLoad的值将为0。
DLL使用DllMain函数来对自己进行初始化,DllMain函数执行的时候,同一地址空间中的其他DLL可能还没有执行它们的DllMain,因此在DllMain中避免调用其他DLL中导入的函数。
在DllMain函数中只应该执行简单的初始化,比如设置线程局部存储区,穿件内核对象,打开文件等等。避免调用User,Shell,ODBC,COM,RPC以及套接字函数。这是因为包含这些函数的DLL可能尚未初始化完毕,或者内部调用导致循环依赖。
DLL_PROCESS_ATTACH通知
当系统第一次将一个DLL映射到进程的地址空间时,DllMain中传入DLL_PROCESS_ATTACH,之后的DLL载入只会增加引用计数。
举个例子:进程调用LoadLibrary(Ex)的时候,系统会对指定的DLL进行定位,并将该DLL映射到进程的地址空间中。然后系统会调用LoadLibrary(Ex)的线程来调用DLL的DllMain函数,并传入DLL_PROCESS_ATTACH值。当DLL的DllMain函数完成了对通知的处理后,系统会让LoadLibrary(Ex)调用返回,这样线程就可以继续正常执行。
DLL_PROCESS_DETACH通知
当系统将一个DLL从进程的地址空间撤销映射时,会调用DllMain函数,并在fdwReason参数中传入DLL_PROCESS_DETACH。当DLL处理这个通知的时候,应该执行与进程相关的清理操作。
如果撤销映射的原因是因为进程要终止,那么调用ExitProcess函数的线程将负责执行DllMain函数的代码。这个线程就是应用程序的主线程。当我们的入口点函数返回到C/C++运行时的启动代码后,启动代码会显示地调用ExitProcess来终止进程;
如果撤销映射的原因是因为进程中的一个线程调用了FreeLibrary或FreeLibraryAndExitThread,那么发出调用的线程将执行DllMain函数中的代码,如果调用的是FreeLibrary,那么在DllMain处理完DLL_PROCESS_DETACH通知之前,线程是不会从该调用中返回的。
DLL_THREAD_ATTACH
当进程创建一个线程的时候,系统会检查当前映射该进程的地址空间的所有DLL文件映像,并用DLL_THREAD_ATTACH来调用每个DLL的DllMain函数。新创建的线程负责执行所有DLL的DllMain函数的代码。
只有当所有DLL都完成了对该通知的处理之后,系统才会让新线程开始执行它的线程函数。如果在已经有的线程后,加载DLL,那么系统不会让任何已有线程用DLL_THREAD_ATTACH来调用该DLL的DllMain函数。
DLL_THRAD_DETACH
同上述,即将终止的线程,会通知所有加载的DLL来处理线程退出函数。C/C++会在这个时候释放那些用来管理多线程应用程序的数据块。
DllMain与C/C++运行库
C/C++运行库,需要确保DLL中全局对象,在DllMain中能够被调用。
在链接DLL的时候,链接器会将DLL的入口点函数的地址嵌入到生成的DLL文件映像中。MSVC中指定了/DLL开关,那么链接器会认为入口点函数的函数名为_DllMainCRTStartup,这个函数包含在C/C++运行库中,在链接DLL的时候会被静态地链接到DLL的文件映像。(即便用的是C/C++运行库的DLL版本,对这个函数的链接仍然会是静态的)
系统将DLL的文件映像映射到进程的地址空间时,实际调用的是_DllMainCRTStartup函数,而不是我们的DllMain函数。在将所有的通知都转发到_DllMainCRTStartup函数之前,为了支持/GS开关提供的安全特性,_DllMainCRTStartup函数会对DLL_PROCESS_ATTACH通知进行处理。_DllMainCRTStartup函数会初始化C/C++运行库,并确保在_DllMainCRTStartup收到DLL_PROCESS_ATTACH通知的时候,所有的全局或静态C++对象都已经构造完毕。
在C/C++运行时的初始化完成后,_DllMainCRTStartup函数会调用我们的DllMain函数。
当DLL收到Dll_PROCESS_DETACH通知,系统会再次调用_DllMainCRTStartup函数。这次函数会调用我们的DllMain函数,当DllMain返回的时候,_DllMainCRTStartup会调用DLL中所有的全局或静态C++对象的析构函数。当接收到DLL_THREAD_ATTACH或者DLL_THREAD_DETACH通知的时候,_DllMainCRTStartup不会做任何的特殊处理。
C/C++库实现了DllMain函数,在我们不实现DllMain函数的时候,运行库会自己创建自己的库
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad){
if(fdwReason == DLL_PRCESS_ATTACH)
DisableThreadLibraryCalls(hInstDll);
return TRUE;