Basic DLL Theory

现在,随着 .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 ,是一位杰出的程序设计者同时也是旅行家和户外活动爱好者,不幸的是,在 1997 4 17 ,他在华盛顿州的一个峡谷飞行时不幸遇难,终年 49 岁。

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

 

如果我们要编写一个标准的模块软件的话,一定会对 DLL 感兴趣。 C++ 的类就是一种模块,但是,类是创建时 (build-time) 的模块,而 DLL 是运行时的模块。我们在编写庞大的 EXE 程序时,每次作了修改都要重新编译并测试,而现在我们可以编写小的 DLL 模块,然后单独测试。例如,我们可以把 C++ 类放到一个 DLL 里,在编译连接后可能只有 12KB 大小。客户程序在运行时,可以快速装载并连接到 DLL 上。 MS Windows 本身的一些主要的功能都使用了 DLL 。而现在编写 DLL 也很容易, Win32 已经大大简化了编程模型,而且, AppWizard MFC 库对 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++ 中使用。对于显示连接,我们不需要导入文件,而是调用 Win32 LoadLibrary 函数,指定 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 都被一个唯一的 32 HINSTANCE 值所标识。此外,进程本身有一个 HINSTANCE 值。所有这些实例句柄只有在进程内部才有效,它们代表了 DLL EXE 的起始虚拟地址。在 Win32 里, HINSTANCE HMODULE 值是相同的,两种类型可相互转换。进程 (EXE) 实例句柄几乎总是 0x400000 ,而装入在默认基地址的 DLL 的句柄是 0x10000000 。如果程序使用了多个 DLL ,则每个都有不同的 HINSTANCE 值,这或者是因为 DLL 有不同的基地址 ( 基地址在创建时被指定 ) ,或者是因为装载器把 DLL 代码作了拷贝并进行了重定位。

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

如何获得实例句柄呢?

如果想获得 EXE 的句柄,我们可以用 NULL 参数调用 Win32 GetModuleHandle 函数;如果想获得 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( 扩展的和正规的 )


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值