动态链接库一直是Windows的基础,WindowsAPI的所有函数都包含在DLL中。三个最重要的DLL是Kernel32.dll(管理内存、进程和线程的函数)、User32.dll(包含用于执行UI任务的函数)和GDI32.dll(包含画图和显示文本的各个函数)。还有别的DLL,用来做特殊任务的DLL,如AdvAPI.dll(用于实现对象安全性、注册表操作和事件记录的函数)、ComDlg32.dll(常用对话框)、ComCtrl32.dll则支持所有的常用窗口控件。
动态链接库之所以重要的原因:
创建DLL相对更容易,因为DLL往往包含的是可以供外部程序使用的函数,没有消息循环也不用创建窗口。在DLL代码被编译之后,链接的方式有/DLL开关,链接程序就会输出一个DLL。
DLL需要加载进程的地址空间才能被使用,至于加载的方式则有隐式和显式2种。后面将提到这点。而当DLL被加载之后,DLL中的代码和数据就像是原本就在进程中一样,而DLL已经失去了它作为DLL的特征。当DLL中的函数被调用的时候,线程会先去查看其堆栈,取出其中可能需要的参数,并传递给函数。还有DLL中创建的任何对象都归线程所有,DLL本身不拥有任何资源。
根据可执行文件的特性,它的全局变量和静态变量是不能被这种文件的不同实例所共享的。Win2K保证这一点的方式是使用所谓的写时拷贝(copy-on-write)机制。对于DLL也遵循同样的特性。
现在有一个问题,假如一个exe加载了一个dll,dll中有一个函数,返回函数中malloc出来的内存mem,而exe接收了这块内存。现在问题来了,谁应该来释放mem呢?exe方并不清楚这块内存是通过malloc还是new产生的,只有dll知道。只能是dll提供释放的操作。
下面将介绍DLL被隐式链接的过程,这是最常用的方式。在DLL被链接成功之后,DLL中的所有变量、函数、对象等信息都会被嵌入到进程空间中。
创造DLL:
创造EXE:
运行应用程序:
如果想要输出函数和变量,那么就要有个DLL模块,然后就可以创建可执行模块。而创建一个DLL模块则有下面的步骤:
一旦可执行模块和相关的DLL被映射到进程的地址空间中,进程的主线程就可以启动运行,同时应用程序也可以启动运行。下面会更加详细地介绍这个进程的运行情况。
DLL中应该避免输出的数据有2种,变量和类。
一般来说,一个DLL需要同时有一个头文件,供DLL创建程序和可执行模块的创建程序使用,就可以大大简化维护工作。下面的代码说明了如何对一个头文件进行编码:
/**********************************************************************************Module: MyLib.h**********************************************************************************/#ifdef MYLIBAPI// MYLIBAPI should be defined in all of the DLL;s source// code modules before this file is include#else // This header file is include by an EXE source code module.// Indicate that all function/variables are being imported.#ifdef MYLIBAPI extern "C" __declspec(dllexport)// Include the export data structures, symbols, functions, and variables.#include "MyLib.h"
当DLL源代码文件被编译时,在头文件的前面使用__declspec(dllexport)
对MYLIBAPI
进行定义,这个定义使得让编译器知道被定义的变量、函数或者C++类是从DLL中输出的。要注意的是```MYLIBAPI``需要被放在要出的变量和函数之前。
当编写C++代码时,extern “C”
修饰符是很必要的。这是因为C和C++编译器对代码进行命名粉碎的方式不同,链接器会抱怨有些可执行模块引用的符号不存在。
DLL这边如何使用头文件就是这样,EXE这边的情况又是怎么样呢?这边的头文件就需要将上面的MYLIBAPI
重新定义成__declspec(dllimport)
,编译器看到这个定义就能从DLL中输入函数和变量。
而编写基础的Windows源代码之时,往往也会只导入一个头文件,如WinBase.h
,就能使用头文件中提到的API和变量了。
上面提到,是修饰符declspec(dllexport)
,使得DLL中的函数和变量得以输出。当Microsoft的C/C++编译器看到这个修饰符之时,一些附加信息就被嵌入到生成的DLL中。在链接程序链接DLL的全部lib文件时,链接程序将对这些信息进行分析。
当D L L被链接时,链接程序要查找关于输出变量、函数或 C++类的信息,并自动生成一
个.lib文件。该.lib文件包含一个DLL输出的符号列表。除了创建.lib文件外,链接程序还要将一个输
出符号表嵌入产生的DLL文件。这个输出节包含一个输出变量、函数和类符号的列表(按字母
顺序排列)。该链接程序还将能够指明在何处找到每个符号的相对虚拟地址(RVA)放入DLL
模块。
通过Visual Studio的DumpBin.exe程序(带-export)开关,就能开到DLL 的输出节的内容。内容中的符号按字母排列,RVA这列的数字用于指明在DLL文件映像的什么位置能找到输出符号的位移量。hint提示码可供系统用来改进代码的运行性能,但是由于不同版本的提示码不同,在这里就不重要了。
如果使用Visual C++ 创建的DLL是要链接到任何供应商的工具创建的可执行模块,那么必须做一些额外的工作。
根据上面提到的extern “C”
的重要性,以及不同供应商的编译器对于C++类做命名粉碎的差异。比如函数的调用规则是__stdcall(也就是WINAPI)
,那么Microsoft的C编译器编译时会改变函数的名字:一个下划线、一个@符号和一个数字。这个数字表示传递给函数的参数的字节数。比如下面的代码:
__declspec(dllexport) LONG __stdcall MyFun(int a, int b);
如果另一个供应商编译了一个EXE,它将设法链接到函数MyFun
,而这个函数的名字已经被改成_@8MyFun了,链接就会失败。为了避免这种情况,必须想办法让Microsoft的编译器输出改变前的函数名。有两种方法:
为编程项目建立一个def文件,并在def文件中加上类似的节:
EXPORTS MyFun
这样当Microsoft的链接程序分析这个def文件时,发现_MyFun@8
和_MyFun
都被输出。由于这两个名字是匹配的,因此链接程序输出的是MyFun的def文件来输出这个函数,而不是__MyFun@8。这样Microsoft的链接程序也会进行正确的操作,链接名字为MyFun的函数了。
在DLL的源代码中添加一些代码,也可以达到和上面一样的效果:
#pragma commet(linker, "/export:MyFun=_MyFun@8")
这条代码会告诉一个名叫MyFun的函数将被输出,其进入点和名称为_MyFun@8的函数的进入点相同。第二种方式麻烦了一些,要自己截断函数名,而且需要两个符号。
下面的代码显示了一个可执行的源代码文件,它输入DLL的输出符号,并在代码中引用这些符号。
/*************************************************************************************Module: MyExeFile1.cpp*************************************************************************************/// Include the standard Windows and C-Runtime header files here.#include <windows.h>// Include the datastructures, symbols, functions and variables.#include "MyLib\MyLib.h"//////////////////////////////////////////////////////////////////////////////////////int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPTSTR pszCmdLine, int) { int nLeft = 10, nRight; TCHAR szBuf[100]; wsprintf(szBuf, TEXT("%d + %d = %d"), nLeft, nRight, Add(nLeft,nRight)); MessageBox(NULL, szBuf, TEXT("Calculation"), MB_OK); wsprintf(szBuf, TEXT("The result from the last ad"))}//////////////////////////////End of file///////////////////////////////////////////
现在exe这边就需要DLL的头文件了,如果没有,输入的符号将不会被定义,而且编译器会发出许多警号和错误消息。
现在MYLIBAPI在EXE这边已经被定义成__declspec(dllimport)了,就在DLL所带的头文件中。编译器看到这个定义,就知道这个符号是从某个DLL模块输入了。编译器不关心是从哪个DLL模块输入的,只确保输入的方式正确。
接着,链接程序必须将所有obj模块组合起来,创建产生的可执行模块。该链接程序必须确定哪些DLL包含代码引用的所有输入符号的DLL。
只要你对calc.exe文件运行dumpbin程序,再加上-import,也就是dumpin -import calc.exe
,输出的内容里有很多DLL,例如shell32.dll、msvcrt.dll、advapi32.dll、kernel32.dll、gdi32.dll和user32.dll,又在列举出了每一个dll输出的函数的名称,比如calc.exe就有Kernel32.dll这个文件,Kernel32.dll下则列举出了:lstrcpyW、LocalAlloc、GetCommandLineW和GetProfileIntW等。
当一个可执行模块被执行时,系统就会为新进程创建地址空间。然后加载程序将可执行模块映射到进程的地址空间中。加载程序查看执行模块的输入节,遍历出所有需要的DLL,再将DLL们也映射到新的地址空间中。
由于输入节只有DLL的名称而不是路径,因此加载程序必须搜索用于的磁盘驱动器,找出DLL,下面是搜索的目录的先后顺序:
在DLL模块被映射到进程的地址空间后,加载程序还要检查每一个DLL的输入节。如果输入节已经存在,加载程序就继续将其他需要的DLL映射进地址空间中。加载程序将保持对DLL模块的跟踪,使得模块的加载和映射只进行一次。
如果加载程序无法找到需要的DLL,那么就会弹出一个提示错误的MessageBoxW,标题就告诉你加载程序无法找到DLL,内容则是详细说明需要找的DLL名和在哪些目录查找过DLL。
找到DLL后,加载程序还要去确认DLL中是否所有的符号都有效。如果某些符号无效,那么就会弹出一个提示错误的MessageBoxW,标题就提示你整个应用程序都出错了,原因则是说无法在DLL中的函数对应的地址上找到这个函数。
如果这个符号存在,那么加载程序将要检索该符号的RVA,并添加到符号在进程的地址空间的位置。然后加载程序将虚拟地址保存在可执行模块的输入节中。此时,当代码引用了一个输入的符号,它将查看调用模块的输入节,并找到这个符号的地址。这样可执行程序就能成功地访问输入的变量、函数或者C++类的成员函数了。动态链接完成,进程的主线程开始执行,应用程序终于也开始执行了!