本文不是描述怎样编写dll程序,也不是说明dll在windows系统的意义。我们的目的是确认dll加载到进程空间的一些模糊的概念。
本文只能说是我结合文档和一些实验得出的一点猜测性质的结论,有些结论并没有官方Microsoft明确的说明,不保证完全正确,欢迎大家交流,共同学习。
一、程序加载dll的两种情况:
比如对于user32.dll,我们在程序中包含了头文件<windows.h>:
1.如果没有调用任何user32.dll的函数,那么user32.dll就不会自动加载,可以通过LoadLibrary来手动加载。比如如下代码:
#include "stdafx.h"
#include <Windows.h>
int _tmain(int argc, _TCHAR* argv[])
{
//HINSTANCE hInstance1 = LoadLibrary("user32.dll");
HMODULE hModule1 = GetModuleHandle("user32.dll");
if (!hModule1)
{
printf("Not auto load user32.dll!\n");
}
else
{
printf("OK,auto load user32.dll!\n");
}
//MessageBox(0,"System will auto load user32.dll because of the MessageBox function","tim",0);
getchar();
return 0;
}
运行结果告诉我们user32.dll没有被加载,而且exe程序的输入表中也没有user32.dll:
2.如果我们在程序中调用了dll中的函数,比如MessageBox,那么编译器会自动将user32.dll中的MessageBoxA(W)写到输入表中,这样user32.dll就会在进程启动时自动加载到地址空间。代码就是上面的,只是把MessageBox一句的注释去掉。其运行结果和输入表如下:
总结一下:dll模块可能因为exe的输入函数而自动加载,也可以在运行后手动加载。
二、从MSDN文档来研究操作dll的API
1、LoadLibrary
The LoadLibrary function maps the specified executable module into the address space of the calling process.
For additional load options, use the LoadLibraryEx function.
HMODULE LoadLibrary( LPCTSTR lpFileName );
这个函数的作用是把可执行模块(exe,dll)映射到调用进程的内存空间,同时增加该dll模块的引用计数(引用计数话题后面再讨论)。如果是重复调用LoadLibrary,则就是增加dll模块的引用计数,并返回模块句柄。
请特别注意下面这段话:Module handles are not global or inheritable. A call to LoadLibrary by one process does not produce a handle that another process can use — for example, in calling GetProcAddress. The other process must make its own call to LoadLibrary for the module before calling GetProcAddress.
利用LoadLibrary得到的模块句柄不是全局的,也不是可继承的。这只在本进程中有效!如果其他进程希望利用模块句柄来得到函数地址,必须自己调用LoadLibrary来获取句柄!
2、GetModuleHandle
The GetModuleHandle function retrieves a module handle for the specified module if the file has been mapped into the address space of the calling process.
To avoid the race conditions described in the Remarks section, use the GetModuleHandleEx function.
HMODULE GetModuleHandle( LPCTSTR lpModuleName );
利用这个函数可以得到指定模块的句柄。注意条件:模块文件已经被映射到了进程的地址空间!
If this parameter is NULL, GetModuleHandle returns a handle to the file used to create the calling process (.exe file).
如果参数为NULL,那么本函数就返回调用进程的句柄。也就是本exe程序的基地址,默认情况下,是400000h。
请注意下面这段话:The returned handle is not global or inheritable. It cannot be duplicated or used by another process.
返回的句柄不是全局的或可继承的,不能复制和跨进程使用!
If lpModuleName does not include a path and there is more than one loaded module with the same base name and extension, you cannot predict which module handle will be returned. To work around this problem, you could specify a path, use side-by-side assemblies, or use GetModuleHandleEx to specify a memory location rather than a DLL name.
The GetModuleHandle function returns a handle to a mapped module without incrementing its reference count. Therefore, use care when passing the handle to the FreeLibrary function, because doing so can cause a DLL module to be unmapped prematurely.
本函数返回模块句柄,但是不增加引用计数!
This function must be used carefully in a multithreaded application. There is no guarantee that the module handle remains valid between the time this function returns the handle and the time it is used. For example, a thread retrieves a module handle, but before it uses the handle, a second thread frees the module. If the system loads another module, it could reuse the module handle that was recently freed. Therefore, first thread would have a handle to a module different than the one intended.
在多线程环境下要小心使用得到的句柄,因为本模块在别的线程中释放了,而有重新加载了别的模块,并且恰恰复用了这个句柄值。这导致利用这个句柄值访问的不是期望的模块。
3、FreeLibrary
The FreeLibrary function decrements the reference count of the loaded dynamic-link library (DLL). When the reference count reaches zero, the module is unmapped from the address space of the calling process and the handle is no longer valid.
BOOL FreeLibrary( HMODULE hModule );
本函数减少加载的dll的引用计数。当引用计数被减到0时,这个模块就会被从进程地址空间卸载,并且句柄不再有效。
请注意下面这段话:
Each process maintains a reference count for each loaded library module. This reference count is incremented each time LoadLibrary is called and is decremented each time FreeLibrary is called. A DLL module loaded at process initialization due to load-time dynamic linking has a reference count of one. This count is incremented if the same module is loaded by a call to LoadLibrary.
每一个进程管理自己的每一个模块的引用计数。引用计数在每次调用LoadLibrary时递增,在调用FreeLibrary时递减。
Before unmapping a library module, the system enables the DLL to detach from the process by calling the DLL's DllMain function, if it has one, with the DLL_PROCESS_DETACH value. Doing so gives the DLL an opportunity to clean up resources allocated on behalf of the current process. After the entry-point function returns, the library module is removed from the address space of the current process.
It is not safe to call FreeLibrary from DllMain. For more information, see the Remarks section in DllMain.
Calling FreeLibrary does not affect other processes using the same library module.
调用FreeLibrary不会影响使用相同库模块的其他进程。
总结:
通过上面三个函数的说明,我们发现可以得到以下一些明确的概念:
1、模块的句柄HModule不是全局的,也不是可继承的。也就是说,只是保证对一个进程唯一的!不能在进程间复制和跨进程使用。
这也就是说,在不同的进程中,对同一个模块(dll等),得到的HMODULE不一定相同。系统并不保证这个值全局唯一。
2、模块的加载和卸载是由引用计数来判断的。LoadLibrary会递增引用计数,FreeLibrary会递减引用计数。当引用计数减少到0时,才会卸载该模块在进程空间的映射。(下文还有一个特殊情况要讨论)
3、模块的引用计数是进程自己管理的!所以一个进程中引用计数的变化不会影响其他的进程。
请记住模块的引用计数是进程自己管理的。你在一个进程中调用无限次的FreeLibrary也不会影响别的进程正常使用这个库模块。
4、如果只是要查看某个模块是否被映射,而得到其模块句柄时,请调用GetModuleHandle。本函数不会主动加载,也不会变更模块的引用计数。
对应疑惑:
1、我们经常发现,在很多不同的程序中,得到某个指定dll的HMODULE好像都是相同的,所以容易误以为这个值是全局唯一。但是MSDN告诉我们这是不可靠的,这个判断是错误的。
之所以很多情况下相同时,其实是涉及到PE加载和重定位的问题。对PE的映射,windows尽量将模块映射到PE指定的地址,来省却重定位的工作。大部分情况下,那些dll的地址是不冲突的,所以就映射成功,所以不同的程序得到的HMODULE值相同。但是这并不代表任何时候都条件满足,比如由于程序很大,用到了很多库,那么就可能导致其默认地址被占用,必须映射到别的地方,那么他的HMODULE就不同。
而且,对应“HMODULE值系统全局唯一”的观点,很容易就可以得出一个反例:对VS编译的exe程序,如果没有指定基地址选项,那么所有不同的进程调用GetModuleHandle(NULL)得到的都是400000h,很明显,对不同的模块,却有相同的值,所以可见该值不是全局的。
2、模块的引用计数是进程自己管理的。也就是你无论怎么调用LoadLibrary和FreeLibrary,都只会影响本进程的引用计数。不要误认为是系统维护的。
三、关于引用计数递减到0会导致模块卸载映射的问题
上文提到模块的引用计数递减到0时,就会将模块从进程的地址空间卸载,有一个特殊情况。这里就来讨论这个特殊情况。
第一部分我们说明了dll加载的两种情况,在这里正是派上用场的地方!
我们先看第一种情况,代码如下:
1#include "stdafx.h"
2#include <Windows.h>
3int _tmain(int argc, _TCHAR* argv[])
4{
5 HMODULE hModule1 = GetModuleHandle("user32.dll");
6 if (!hModule1)
7 {
8 printf("Not auto load user32.dll!\n");
9 }
10 else
11 {
12 printf("OK,auto load user32.dll!\n");
13 }
14
15 HINSTANCE hInstance1 = LoadLibrary("user32.dll");
16 hModule1 = GetModuleHandle("user32.dll");
17 if (hModule1)
18 {
19 printf("Have load user32.dll!\n");
20 }
21 else
22 {
23 printf("Not load user32.dll!\n");
24 }
25
26 int count=0;
27 while(hModule1)
28 {
29 FreeLibrary(hModule1);
30 hModule1 = GetModuleHandle("user32.dll");
31 ++count;
32 }
33
34 printf("After call FreeLibrary %d time(s).User32.dll has been Freed!\n",count);
35
36 //MessageBox(0,"System will auto load user32.dll because of the MessageBox function","tim",0);
37 getchar();
38 return 0;
39}下面是输出:
我们发现起初进程地址空间中没有user32.dll。然后通过调用LoadLibrary,user32.dll被成功的加载。然后通过调用FreeLibrary,我们也将user32.dll卸载了。这符合我们在第二部分的讨论。
不过一个有趣的现象是:我们在程序中明明只调用LoadLibrary一次,但是要调用4次FreeLibrary,才能释放该模块!这实在是有意思。。。
我们再来研究第二种情况,代码很简单,就是把上面的代码36行的MessageBox一句的注释去掉。这是的运行结果就有意思了:
之所以没有后面的,是因为程序就永远的陷入了while循环,永无出头之日了!也就是无论你怎么调用FreeLibrary,都无法将该模块从地址空间卸载!
结论:对于由输入表导入的模块,不管他是否采用引用计数机制,都无法希望利用FreeLibrary递减引用计数而卸载!
正因为这个特殊情况,也就是我专门写本文第一部分的原因。希望这样的编排不影响你的思考。
www.cppblog.com/Tim