第19章 DLL基础
自从Microsoft公司推出第一个版本的Windows操作系统以来,动态链接库(DLL)一直是这个操作系统的基础。
Windows API
中的所有函数都包含在
DLL
中。3个最重要的DLL是:
Kernel32.dll
,它包含用于管理内存、进程和线程的各个函数;
User32.dll
,它包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数;
GDI32.dll
,它包含用于画图和显示文本的各个函数。
Windows还配有若干别的DLL,它们提供了用于执行一些特殊任务的函数。例如,
AdvAPI32.dll包含用于实现对象安全性、注册表操作和事件记录的函数;
ComDlg32.dll包含常用对话框(如File Open和File Save);
ComCtl32.DLL则支持所有的常用窗口控件。
本章将要介绍如何为应用程序创建DLL。下面是为什么要使用DLL的一些原因:
? 它们扩展了应用程序的特性。由于D L L能够动态地装入进程的地址空间,因此应用程序能够在运行时确定需要执行什么操作,然后装入相应的代码,以便根据需要执行这些操作。
? 它们可以用许多种编程语言来编写。可以选择手头拥有的最好的语言来编写D L L。也许你的应用程序的用户界面使用Microsoft Visual Basic编写得最好,但是用C + +来处理它的商用逻辑更好。系统允许Visual Basic程序加载C++ DLL、Cobol DLL和Fortran DLL等。
? 它们简化了软件项目的管理。如果在软件开发过程中不同的工作小组在不同的模块上工作,那么这个项目管理起来比较容易。
? 它们有助于节省内存。如果两个或多个应用程序使用同一个D L L,那么该D L L的页面只要放入R A M一次,所有的应用程序都可以共享它的各个页面。C / C + +运行期库就是个极好的例子。许多应用程序都使用这个库。如果所有的应用程序都链接到这个静态库,那么s p r i n t f、s t r c p y和m a l l o c等函数的代码就要多次存在于内存中。但是,如果所有这些应用程序链接到DLL C/C++运行期库,那么这些函数的代码就只需要放入内存一次,这意味着内存的使用将更加有效。
? 它们有助于资源的共享。D L L可以包含对话框模板、字符串、图标和位图等资源。多个应用程序能够使用D L L来共享这些资源。
? 它们有助于应用程序的本地化。应用程序常常使用D L L对自己进行本地化。例如,只包含代码而不包含用户界面组件的应用程序可以加载包含本地化用户界面组件的D L L。
? 它们有助于解决平台差异。不同版本的Widnows配有不同的函数。开发人员常常想要调了用新的函数(如果它们存在于主机的Windows版本上的话)。但是,如果你的源代码包含对一个新函数的调用,而你的应用程序将要在不能提供该函数的Windows版本上运行,那么操作系统的加载程序将拒绝运行你的进程。即使你实际上从不调用该函数,情况也是这样。如果将这些新函数保存在DLL中,那么应用程序就能够将它们加载到Windows的老版本上。当然,你仍然可以成功地调用该函数。
? 它们可以用于一些特殊的目的。Windows使得某些特性只能为DLL所用。例如,只有当DLL中包含某个挂钩通知函数的时候,才能安装某些挂钩(使用SetWindowsHookEx和SetWinEventHook来进行安装)。可以通过创建必须在DLL中生存的COM对象来扩展Windows Explorer的外壳程序。对于可以由Web浏览器加载的、用于创建内容丰富的Web页的Active X控件来说,情况也是一样。
19.1 DLL与进程的地址空间
创建DLL常常比创建应用程序更容易,因为
DLL
往往包含一组应用程序可以使用的自主函数。在
DLL
中通常没有用来处理消息循环或创建窗口的支持代码。DLL只是一组源代码模块,每个模块包含了应用程序(可执行文件)或另一个DLL将要调用的一组函数。当所有源代码文件编译后,它们就像应用程序的可执行文件那样被链接程序所链接。但是,对于一个DLL来说,你必须设定该连链程序的/DLL开关。这个开关使得链接程序能够向产生的DLL文件映像发出稍有不同的信息,这样,操作系统加载程序就能将该文件映像视为一个DLL而不是应用程序。
在应用程序(或另一个DLL)能够调用DLL中的函数之前,
DLL
文件映像必须被映射到调用进程的地址空间中。若要进行这项操作,可以使用两种方法中的一种,即加载时的隐含链接或运行期的显式链接。隐含链接将在本章的后面部分介绍,显式链接将在第2 0章中介绍。
一旦
DLL
的文件映像被映射到调用进程的地址空间中,
DLL
的函数就可以供进程中运行的所有线程使用。实际上,DLL几乎将失去它作为DLL的全部特征。对于进程中的线程来说,DLL的代码和数据看上去就像恰巧是在进程的地址空间中的额外代码和数据一样。当一个线程调用DLL函数时,该DLL函数要查看线程的堆栈,以便检索它传递的参数,并将线程的堆栈用于它需要的任何局部变量。此外,
DLL
中函数的代码创建的任何对象均由调用线程所拥有,而
DLL
本身从来不拥有任何东西
。
19.2 DLL的总体运行情况
一旦可执行模块和所有D L L模块被映射到进程的地址空间中,进程的主线程就可以启动运行,同时应用程序也可以启动运行。
19.3 创建D L L模块
************************************mylib.h*********************************
#ifdef
MYLIBAPI
#else
#define MYLIBAPI extern "C" __declspec(dllimport);
#endif
MYLIBAPI int g_nResult ;
MYLIBAPI int Add ( int nLeft, int nRight) ;
************************************mylibfile1.cpp**************************
#incldue <windows.h>
#define MYLIBAPI extern "C" __declspec (dllexport) ;
#include "mylib.h"
int
g_nResult ;
int
Add (int nLeft, int nRight)
{
g_nResult = nLeft + nRight ;
return (g_nResult) ;
}
*******************************************************************************
当上面的
DLL
源代码文件被编译时,在
MyLib.h
头文件的前面使用
__declspec(dllexport)
对
MYLIBAPI
进行定义。当编译器看到负责修改变量、函数或
C++
类的
__declspec(dllexport)
时
,
它就知道该变量、函数或
C++
类是从产生的
DLL
模块输出的。
注意,
MYLIBAPI
标志被置于头文件中要输出的变量的定义之前和要输出的函数之前。
另外,
在源代码文件
(MyLibFile1.cpp0)
中,
MYLIBAPI
标志并不出现在输出的变量和函数之前
MYLIBAPI
标志在这里是不必要的,因为编译器在分析头文件时能够记住要输出哪些变量或函数。
19.3.1 输出的真正含义是什么
上一节介绍的一个真正有意思的东西是__declspec(dllexport)修改符。当Microsoft的C/C++编译器看到变量、函数原型或C + +类之前的这个修改符的时候,它就将某些附加信息嵌入产生的.obj文件中。当链接DLL的所有.obj文件时,链接程序将对这些信息进行分析。
当DLL被链接时,链接程序要查找关于输出变量、函数或C++类的信息,并自动生成一个.lib文件。该.lib文件包含一个DLL输出的符号列表。当然,如果要链接引用该DLL的输出符号的任何可执行模块,该.lib文件是必不可少的。除了创建.lib文件外,链接程序还要将一个输出符号表嵌入产生的DLL文件。这个输出节包含一个输出变量、函数和类符号的列表(按字母顺序排列)。该链接程序还将能够指明在何处找到每个符号的相对虚拟地址( RVA)放入DLL模块。
19.3.2 创建用于非Visual C++工具的DLL
当你的函数使用__stdcall(WINAPI)调用规则时会出现这种问题。这种调用规则是最流行的一种类型。
当使用
__stdcall
将
C
函数输出时,
Microsoft
的编译器就会改变函数的名字,设置一个前导下划线,再加上一个
@
符号的前缀,后随一个数字,表示作为参数传递给函数的字节数。例如,下面的函数是作为DLL的输出节中的_MyFunc@8输出的:
__declspec(dllexport) LONG __stdcall
MyFunc(int a, int b) ;
如果用另一个供应商的工具创建了一个可执行模块,它将设法链接到一个名叫MyFunc的函数,该函数在Microsoft编译器已有的DLL中并不存在,因此链接将失败。
19.4 创建可执行模块
下面的代码段显示了一个可执行的源代码文件,它输入了DLL的输出符号,并且在代码中引用了这些符号。
**************************myexefile1.cpp*******************************
#include <windows.h>
#include"mylib.h"
int WINAPI WinMain (HINSTANCE hinstExe, HINSTANCE, LPTSTR pszCmdLine, int)
{
int nLeft = 10, nRight = 25;
TCHAR sz[100];
wsprintf(sz, TEXT("%d + %d = %d"), nLeft, nRight, Add(nLeft , nRight )) ;
MessageBox (NULL, sz, TEXT("Calculation"), MB_OK) ;
wsprintf(sz, TEXT("the result is:%d"), g_nResult) ;
MessageBox (NULL, sz, TEXT("Last Result"), MB_OK) ;
return (0) ;
}
当创建可执行源代码文件时,必须加上D L L的头文件。如果没有头文件,输入的符号将不会被定义,而且编译器将会发出许多警告和错误消息。
可执行源代码文件不应该定义DLL的头文件前面的MYLIBAPI。当上面显示的这个可执行源代码文件被编译时,MYLIBAPI由MyLib.h头文件使用__declspec(dllimport)进行定义。当
编译器看到修改变量、函数或
C++
类的
__declspec(dllimport)
时,它知道这个符号是从某个
DLL
模块输入的。它不知道是从哪个
DLL
模块输入的,并且它也不关心这个问题。编译器只想确保你用正确的方法访问这些输入的符号。现在你在源代码中可以引用输入的符号,一切都将能够正常工作。
接着,链接程序必须将所有.obj模块组合起来,创建产生的可执行模块。该
链接程序必须确定哪些
DLL
包含代码引用的所有输入符号的
DLL
。因此你必须将
DLL
的
.lib
文件传递给链接程序。如前所述,.lib文件只包含DLL模块输出的符号列表。链接程序只想知道是否存在引用的符号和哪个DLL模块包含该符号。如果连接程序转换了所有外部符号的引用,那么可执行模块就因此而产生了。
19.5 运行可执行模块
当一个可执行文件被启动时,操作系统加载程序将为该进程创建虚拟地址空间。然后,加载程序将可执行模块映射到进程的地址空间中。加载程序查看可执行模块的输入节,并设法找出任何需要的DLL,并将它们映射到进程的地址空间中。
由于该输入节只包含一个
DLL
名而没有它的路径名。因此加载程序必须搜索用户的磁盘驱动器,找出
DLL
。下面是加载程序的搜索顺序:
1)
包含可执行映像文件的目录。
2)
进程的当前目录。
3) Windows
系统目录。
4) Windows
目录。
5) PATH
环境变量中列出的各个目录。
应该知道其他的东西也会影响加载程序对一个DLL的搜索(详细说明参见第20章)。当DLL模块映射到进程的地址空间中时,加载程序要检查每个DLL的输入节。如果存在输入节(通常它确实是存在的),那么加载程序便继续将其他必要的DLL模块映射到进程的地址空间中。加载程序将保持对DLL模块的跟踪,使模块的加载和映射只进行一次(尽管多个模块需要该模块)。