Basic DLL Theory

来源:互联网 发布:ubuntu php5 安装路径 编辑:程序博客网 时间:2024/05/23 02:04

现在,随着.NET的普及和完善,Visual C++MFC正在谈出主流。越来越多的人在使用其他的语言开发Windows程序,但是从技术角度而言,Visual C++依然是最强大的编程语言(工具)之一,特别是对于开发Windows程序,而MFC作为应用程序框架类库,已经成为了Windows程序设计的C++封装典范。潘爱民老师曾说过,“学语言应该学习C++,要开发Windows应用程序应该看看MFC”。C++是程序语言的宝库,MFC是开发Windows应用程序的技术宝库。但是由于MFC非常庞大,如何有效地学习它是大多数初学者感到困惑的问题。而我认为最好的方法应该是通过,学习兴趣+一本好的教材+积累总结。如果我们不是天才,那么学习任何东西都需要时间的积累,我始终相信天道酬勤这句话,与大家共勉!以前虽然学过动态连接库方面的知识,却不是很系统,想起以前老师总是教导我们说,“好记性不如烂笔头”,我相信只有通过不断的总结,才能够更好地掌握一门知识。

 

From:  Visual C++ 技术内幕(第四版) David Kruglinski 1992年第一版

PS:

u      作者Dave,是一位杰出的程序设计者同时也是旅行家和户外活动爱好者,不幸的是,在1997417,他在华盛顿州的一个峡谷飞行时不幸遇难,终年49岁。

u      第四版主要针对Visual C++ 5.0版本,以Windows NT 4.0Windows 95或更高版本的32WindowsOS平台,以MFC 4.21为基础,全面介绍了各种MFC类库应用程序的开发过程。

 

如果我们要编写一个标准的模块软件的话,一定会对DLL感兴趣。C++的类就是一种模块,但是,类是创建时(build-time)的模块,而DLL是运行时的模块。我们在编写庞大的EXE程序时,每次作了修改都要重新编译并测试,而现在我们可以编写小的DLL模块,然后单独测试。例如,我们可以把C++类放到一个DLL里,在编译连接后可能只有12KB大小。客户程序在运行时,可以快速装载并连接到DLL上。MS Windows本身的一些主要的功能都使用了DLL。而现在编写DLL也很容易,Win32已经大大简化了编程模型,而且,AppWizardMFC库对DLL也有了更多的支持。

基本DLL理论

Win32是如何把DLL结合到进程里去的呢?记住,进程是一个程序的运行主体,而程序是从磁盘上的EXE文件开始启动的。首先,DLL是磁盘上的一个文件(通常带DLL扩展名),包含全局数据、编译过的函数和资源,它们是进程的一部分。DLL经编译后,装入到一个预置的基地址,如果跟其他的DLL没有冲突的话,文件就被映射到进程中相同的虚拟地址上。DLL有各种导出函数,客户程序(首先装入DLL的程序)导入这些函数。Windows在装入DLL时会对导入和导出作匹配。

说明:Win32 DLL允许导出全局变量,就像导出函数一样。

Win32中,每一个进程对DLL的可读写全局变量都有自己的私有拷贝。如果我们想在进程间共享内存,我们或者可以使用内存映射文件,或者可以声明一个共享数据区(具体见Jeffrey Richter)。只要DLL申请堆内存,它就从客户进程的堆中进行内存分配。

1.1  导入如何与导出相匹配

DLL包含一个导出函数表,我们可以通过函数的符号化的名字和(可选)称为序号的整数识别这些函数。函数表也包含了函数在DLL内的地址。当客户程序首先装入DLL时,它并不知道它将要调用的函数的地址,但它知道符号名或序号。动态连接的进程然后建立一张表,把客户的调用与DLL里函数的地址连接起来。如果我们编辑并重建了DLL,我们并不需要重建客户程序,除非我们改变了函数名或参数序列。

说明:在简单的情况下,只有一个EXE文件从一个或多个DLL导入函数;而在实际情况下,许多DLL调用了其他DLL里的函数。因此,一个特殊的DLL可以同时有导入和导出。这样做当然没有问题,因为动态连接进程可以控制交叉关联(cross-dependency)

DLL代码中,我们必须显式声明导出函数,类似这样:

(另一种办法是在模块定义[DEF]文件中列出所有的导出函数,但这通常很麻烦。)在客户方面,我们需要声明对应的导入函数,类似这样:

如果我们使用了C++,则编译器会为MyFunction产生一个其他语言不能使用的修饰名。这些修饰名很长,编译器根据类名、函数名和参数类型产生修饰名,它们在工程的MAP文件中被列出。如果我们希望使用普通名MyFunction,则必须用下面这种方式书写函数声明:


    说明:默认情况下,编译器用__cdecl参数传送约定,这就意味着,调用程序应从栈里弹出参数。有些客户程序可能要求__stdcall约定(它代表了Pascal调用约定),这就意味着,被调用的函数将直接从栈里弹出。因此,我们在DLL导出声明里可能必须使用__stdcall修饰符。

 

要使客户连接到一个DLL,仅仅有导入声明还不够。客户工程必须为连接器指定导入库(LIB),而且客户程序必须实际调用了DLL的导出函数中的至少一个函数。调用语句必须在程序的可执行路径里。

1.2  隐式连接和显示连接

前面部分基本上介绍的是隐式连接,它是C++程序员为了使用DLL而经常使用的一种方法。当我们创建DLL时,连接器产生一个附加的导入LIB文件,其中包含了每个DLL的导出符号和(可选)序号,但没有代码。LIB文件是DLL的一个代理,它被加到客户程序的工程中。当创建客户(静态连接)时,导入的符号被匹配到LIB文件的导出符号,这样符号(或序号)被绑定进EXE文件里。LIB文件也包含了DLL文件名(但不是全路径名),文件名也被保存到EXE文件中。当客户装载后,Windows找到DLL并进行装载,然后根据符号或序号动态连接。

显示连接对于解释语言(Microsoft Visual Basic)更为合适,但如果需要的话,我们也可以在C++中使用。对于显示连接,我们不需要导入文件,而是调用Win32LoadLibrary函数,指定DLL的路径名作为参数。LoadLibrary返回一个HINSTANCE参数,我们可以在GetProcAddress调用中使用该参数,该调用把一个符号(或序号)转换到DLL中的地址。假定我们有一个DLL导出这样的一个函数:

下面是客户显示连接到函数的一个例子:

  

对于隐式连接,所有的DLL都在客户被装载的时候被装载,但在显示连接的情况下,我们可以决定什么时候装载和卸除。显示连接允许我们在运行时决定装载哪个DLL,例如,我们有一个DLL带英文字符串资源,另一个DLL带西班牙文字字符串资源,那么应用程序可以在用户选择了一种语言后选择装载适当的DLL

1.3  符号连接和序号连接

Win16里,序号连接更为有效,而且也是人们乐意采用的一种方式;在Win32里,符号连接效率有了改进,Microsoft现在推荐这种方式超过了序号连接。然而,MFC库的DLL版本使用了序号连接。一个典型的MFC程序可能会连接MFC DLL中的上百个函数,而序号连接可以使程序的EXE文件很小,因为它没有包含导入函数的长长的符号名。如果我们创建自己的DLL时使用了序号连接,则必须在工程的DEF文件里指定序号。在Win32环境里,DEF文件没有其他太多的用途。

1.4  DLL入口点——DllMain

默认情况下,连接器为DLL指定主入口点 _DllMainCRTStartup。当Windows加载DLL时,它调用该函数,该函数首先调用全局对象的构造函数,然后调用全局函数DllMain(它假设我们编写了DllMain函数)DllMain不仅在DLL被连接到进程时被调用,而且在断开进程的连接和其他相应的时候也被调用。下面是DllMain函数的框架:

如果我们没有为DLL编写DllMain函数,则会从运行库里导进一个什么也不做的函数版本。

DllMain函数也在独立线程被启动和终止时被调用,其中的参数dwReason指出了调用的原因。Ritcher的书介绍了所有这些我们该知道的主题。

1.5  实例句柄——装载资源

进程中的每一个DLL都被一个唯一的32HINSTANCE值所标识。此外,进程本身有一个HINSTANCE值。所有这些实例句柄只有在进程内部才有效,它们代表了DLLEXE的起始虚拟地址。在Win32里,HINSTANCEHMODULE值是相同的,两种类型可相互转换。进程(EXE)实例句柄几乎总是0x400000,而装入在默认基地址的DLL的句柄是0x10000000。如果程序使用了多个DLL,则每个都有不同的HINSTANCE值,这或者是因为DLL有不同的基地址(基地址在创建时被指定),或者是因为装载器把DLL代码作了拷贝并进行了重定位。

实例句柄对装载资源特别重要。Win32函数FindResource带一个HINSTANCE参数。EXEDLL可以拥有各自的资源。如果我们从DLL中获取资源,则必须指定DLL的实例句柄;如果我们从EXE文件中获取资源,则必须指定EXE的实例句柄。

如何获得实例句柄呢?

如果想获得EXE的句柄,我们可以用NULL参数调用Win32GetModuleHandle函数;如果想获得DLL的句柄,我们可以DLL的名字作为参数调用GetModuleHandle函数。后面我们将看到,MFC库通过顺序查找各个模块来进行资源装载。

1.6  客户程序如何找到DLL

如果用LoadLibrary显示连接DLL的话,我们可以指定DLL的全路径名。如果我们没有指定路径名,或者使用了隐式连接,则Windows将使用下面的搜索序列定位DLL

1.         包含EXE文件的目录

2.         进程的当前目录

3.         Windows的系统目录

4.         Windows目录

5.         Path环境变量里列出的目录

这里有一个很容易掉入的陷阱。我们创建一个DLL工程,然后把DLL文件拷贝到系统目录下,再从一个客户程序运行DLL。这样做当然很好。下一次,我们做了一些修改并重建了DLL,但忘记把DLL文件拷贝到系统目录下。这样,当再次运行客户程序时,它装载的仍是原来的DLL版本。一定要小心!

1.7  调试DLL

Developer Studio使调试DLL很容易,只要从DLL工程启动调试器即可。第一次这样做的时候,调试器会请求给出客户EXE文件的路径。之后,每次从调试器“运行”DLL时,调试器会装入EXE,而EXE用搜索序列找到DLL。这就意味着,我们必须或者设置Path环境变量以指向DLL,或者把DLL拷贝到搜索序列中的目录下。

 

Note: wcdj 于2010-1-5   下一次介绍和总结MFC DLL(扩展的和正规的)