windows下DLL基础

本文详细介绍了Windows动态链接库(DLL)的加载机制、使用优势及地址空间管理。探讨了LoadLibrary和FreeLibrary函数如何控制DLL的加载与卸载,以及DLLMain函数在进程和线程生命周期中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

动态链接库

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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值