介绍一点重要的背景知识:所有的Win32API函数都包含在DLL中。三个最重要的DLL是:KERNEL32.DLL(它由管理内存、进程和线程的函数组成),USER32.DLL(它由执行用户界面任务(如创建窗口和发送消息)的函数组成),GDI32.DLL(它由绘图和显示文本的函数组成)。另外还有一些执行专门功能的DLL,例如:ADVAPI32.DLL(包含有关对象安全、注册表管理和事件记录的函数);COMDLG32.DLL(包含了通用对话框,FileOpen、FileSave等);LZ32.DLL(包含文件解压缩函数)
13.1 创建动态链接库
DLL中通常没有处理消息循环的代码和创建窗口的代码。
应用程序要调用DLL中的函数,必须先把DLL的文件映射到进程的地址空间里。
由DLL中代码创建的对象都归调用线程或进程所有,DLL在Win32中什么也不拥有。例如:DLL中的函数调用了VirtualAlloc,保留的地址空间区域在调用的线程的进程的地址空间中。如果后来DLL从程序中释放了,可是由DLL保留的这块物理存储并不释放,只有当程序某个线程调用了VirtualFree或是进程终结时,该保留区域才从内存中释放。
13.1.1将DLL映射到进程的地址空间:可以使用两种方法将DLL映射到进程的地址空间,一种是隐式连接,另一种是显式装入。
1.隐式链接:
采用这种方法时,系统寻找DLL文件时,查找以下这几处:
(1)包含EXE文件的目录
(2)进程的当前目录
(3)Windows系统目录
(4)Windows目录
(5)列在PATH环境变量中的目录
如果这五处都找不到DLL,那么系统会显示一个消息框,之后终止整个进程。
采用这种方法映射的DLL,DLL在进程终结时才会被解除映射,也就是说在进程终结时才会被释放。
2.显式链接:
当进程中的线程调用LoadLibrary或LoadLibraryEx时可以显示的映射DLL文件到进程的地址空间。显式加载DLL后,可以在任意时刻用FreeLibrary来释放DLL在进程地址空间中的映射。如果同一进程中的线程使用LoadLibrary或LoadLibraryEx加载了相同的DLL两次以上,那么在第一次加载DLL到进程的地址空间后,系统并不会再重复加载DLL,而只是增加对DLL的使用计数,同理在使用FreeLibrary时,只是减少对DLL的使用计数,并一定要从进程的地址空间中释放DLL。当系统看到一个DLL的使用计数为0时,就自动把该DLL从进程的地址空间中释放。如果是两个不同的进程(进程A和进程B)显示载入了相同的DLL,那么进程A在载入DLL时,系统同时为该DLL的使用计数设置为1,同理B的也是1,在进程A释放DLL时,进程B中的DLL并不受任何影响,进程B中对DLL的使用计数还是1,而进程A中对该DLL的使用计数变成0。注:显示链接时,系统寻找DLL文件同隐式链接,不过函数LoadLibraryEx可以改变寻找的方式。
线程可以调用GetModuleHandle函数来判断一个DLL是否被载入了进程的地址空间,
HINSTANCE GetModuleHandle(LPCTSTR lpszModuleName);
例子:
HINSTANCE hinstDLL;
hinstDLL = GetModuleHandle(“SomeDLL.dll”);
If (hinstDLL == NULL){
hinstDLL = LoadLibrary(“SomeDLL.dll”);
}
如果有了DLL的HINSTANCE值就可以使用GetModuleFileName来得到DLL的全路径名,
DWORD GetModuleFileName(HINSTANCE hinstModule,
LPTSTR lpszPath,DWORD cchPath);
参数说明:
hinstModule是DLL或EXE的HINSTANCE值,lpszPath是返回的DLL的全路径名,cchPath指定缓冲区字符大小。
13.2 DLL的进入/退出函数
这些函数常常被DLL用来执行线程级或进程级的初始化或清理工作。
如果DLL中有这个进入/退出函数,那么这个函数必须是下面这个形式的:
BOOL WINAPI DLLMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID fImpLoad){
Switch(fdwReason){
Case DLL_PROCESS_ATTACH:
//当这个DLL被映射到了进程的地址空间时
break;
case DLL_THREAD_ATTACH:
//一个线程正在被创建
break;
case DLL_THREAD_DETACH:
//线程终结
break;
case DLL_PROCESS_DETACH:
//这个DLL从进程的地址空间中解除映射
break;
}
return(TRUE);
}
参数说明:
hinstDLL包含DLL句柄。该值表示DLL被映射到进程地址空间内的虚拟地址。
fImpLoad当隐式加载时该参数非零,当DLL被显示加载时为零。
fdwReason:该参数指出系统为什么调用该函数。该参数有四个可能值:DLL_PROCESS_ATTACH、DLL_PROCESS_DETACH、DLL_THREAD_ATTACH、DLL_THREAD_DETACH。下面分别说明这四个可能值的作用:
DLL_PROCESS_ATTACH:当一个DLL被首次载入进程地址空间时,系统会调用该DLL的DLLMain函数,传递的参数fdwReason为DLL_PROCESS_ATTACH。这种情况只有在首次映射DLL时才发生。当DLLMain处理DLL_PROCESS_ATTACH时,DLLMain函数的返回值表示DLL的初始化是否成功。成功返回TRUE,否则返回FALSE。举一个在DLL_PROCESS_ATTACH通知中简单的初始化例子:使用HeapCreate来创建一个DLL要使用的堆,当然这个堆是在进程的地址空间上的。(现在描述一下隐式载入DLL时都发生了什么:当新建一个进程时,系统为该进程分配了地址空间,之后将EXE文件和所需要的DLL文件都映射到进程的地址空间。之后系统创建了进程的主线程,并使用这个主线程来调用每一个DLL中的DLLMain函数,传递给DLLMain函数的参数fdwReason的值是DLL_PROCESS_ATTACH。在所有的DLL都响应了DLL_PROCESS_ATTACH通知后,系统让进程的主线程执行EXE的C运行时启动代码,而后调用EXE文件的WinMain函数。如果有一个DLL的DLLMain函数的返回值为FALSE,说明DLL的初始化没有成功,系统就会终结整个进程,去掉所有文件映象,之后显示一个对话框告诉用户进程不能启动。再说明一下显示载入DLL时都发生了什么:首先线程(A)调用LoadLibrary或LoadLibraryEx来载入一个DLL,之后系统让线程A来调用DLL中的DLLMain函数,并传递参数fdwReason值为DLL_PROCESS_ATTACH,当DLL中的DLLMain处理完DLL_PROCESS_ATTACH通知后,线程就会从LoadLibrary返回,继续执行线程中LoadLibrary下面的代码。如果DLL中的DLLMain返回FALSE,说明初始化不成功,系统将DLL自动解除映射,使用对LoadLibrary或LoadLibraryEx的调用返回NULL。)
DLL_PROCESS_DETACH:当DLL从进程的地址空间解除映射时,参数fdwReason被传递的值为DLL_PROCESS_DETACH。当DLL处理DLL_PROCESS_DETACH时,DLL应该处理与进程相关的清理操作。举个例子:可以在DLL_PROCESS_DETACH阶段使用HeapDestroy来释放在DLL_PROCESS_DETACH阶段创建的堆。
如果进程的终结是因为系统中有某个线程调用了TerminateProcess来终结的,那么系统就不会用DLL_PROCESS_DETACH来调用DLL中的DLLMain函数来执行进程的清理工作。这样就会造成数据丢失。所以万不得以不要使用TerminateProcess来终结进程。
DLL_THREAD_ATTACH:该通知告诉所有的DLL执行线程的初始化。当进程创建一个新线程时,系统会查看进程地址空间中所有的DLL文件映射,之后用DLL_THREAD_ATTACH来调用DLL中的DLLMain函数。要注意:系统不会为进程的主线程使用值DLL_THREAD_ATTACH来调用DLL中的DLLMain函数。
DLL_THREAD_DETACH:该通知告诉所有的DLL执行线程的清理工作。注意如果线程的终结是使用TerminateThread来完成的,那么系统将不会使用值DLL_THREAD_DETACH来执行线程的清理工作,这也就是说可能会造成数据丢失,所以万不得已不要使用TerminateThread来终结线程。
注:在编写DLL时不必一定实现DLLMain函数,当DLL源码中没有DLLMain函数时,C运行时库有它自己的一个DLLMain函数。
13.3 从DLL中输出函数和变量
在C中输出函数和变量的方法:
举例子说明:如何输出函数add和变量g_nUsageCount,
_declspec(dllexport) int Add(){
//函数Add的函数体
}
_declspec(dllexport) int g_nUsageCount = 0;
下面说明一下从DLL中输出函数和变量时链接器所要做的一些事:首先在链接器链接DLL时,链接器检查到了关于输出函数和变量的信息,之后链接器就自动产生了一个包含DLL输出符号的LIB文件,同时还在生成的DLL文件中嵌入了一个输出符号表(这个表中包含DLL中要输出的函数和变量的名字和在DLL中对应的地址)。
13.4 在EXE中使用DLL输出的函数和变量
下面举例子说明如何在EXE中调用从DLL中输出的函数Add和变量g_nUsageCount,
_declspec(dllimport) int Add();
_declspec(dllimport) int g_nUsageCount;
线程可以使用GetProcAddress函数来得到从DLL中输出的函数或变量的地址,
FARPROC GetProcAddress(HINSTANCE hinstDLL,LPCSTR lpszProc);
参数说明:hinstDLL是DLL的句柄,lpszProc可以是DLL中函数或变量的名字,也可以是对应DLL中函数或变量的序号。
举例子:GetProcAddress(hinst,”SomeDLLName”);//直接使用函数名
GetProcAddress(hinst,MakeIntResource(2));//使用函数的序号
13.5 在EXE或DLL的多个实例中共享数据
13.5.1 首先说明一下EXE或DLL中的节:
每一个EXE或DLL文件都是由节的集合组成的,每个节由“.”开始,例如:编译器把所有代码都放在.text节中。如下表所示为EXE或DLL中的常用节:
节名 |
内容 |
.text |
应用程序或DLL的代码 |
.bss |
未初始化的数据 |
.rdata |
只读的运行时数据 |
.rsrc |
资源 |
.edata |
输出名字表 |
.data |
初始化的数据 |
.xdata |
异常处理表 |
.idata |
引入名字表 |
.CRT |
只读的C运行时数据 |
.reloc |
修正表信息 |
.debug |
调试信息 |
.tls |
线程局部存储 |
每个节都有如下访问权限:
属性 |
含义 |
READ |
该节中的字节可读取 |
WRITE |
该节中的字节可写入 |
EXECUTE |
该节中的字节可执行 |
SHARED |
该节中的字节可被多个实例共享 |
在代码中使用节的方法举例:
#pragma comment(linker,”/SECTION:SHARED,RWS”);//使节SHARED可以读、写、可共享。