Win32下DLL编程

 
  
  比较大应用程序都由很多模块组成,这些模块分别完成相对独立的功能,它们彼此协作来完成整个软件系统的工作。其中可能存在一些模块的功能较为通用,在构造其它软件系统时仍会被使用。在构造软件系统时,如果将所有模块的源代码都静态编译到整个应用程序 EXE 文件中,会产生一些问题:一个缺点是增加了应用程序的大小,它会占用更多的磁盘空间,程序运行时也会消耗较大的内存空间,造成系统资源的浪费;另一个缺点是,在编写大的 EXE 程序时,在每次修改重建时都必须调整编译所有源代码,增加了编译过程的复杂性,也不利于阶段性的单元测试。
   Windows 系统平台上提供了一种完全不同的较有效的编程和运行环境,你可以将独立的程序模块创建为较小的 DLL(Dynamic Linkable Library) 文件,并可对它们单独编译和测试。在运行时,只有当 EXE 程序确实要调用这些 DLL 模块的情况下,系统才会将它们装载到内存空间中。这种方式不仅减少了 EXE 文件的大小和对内存空间的需求,而且使这些 DLL 模块可以同时被多个应用程序使用。 Microsoft Windows 自己就将一些主要的系统功能以 DLL 模块的形式实现。例如 IE 中的一些基本功能就是由 DLL 文件实现的,它可以被其它应用程序调用和集成。
  一般来说, DLL 是一种磁盘文件(通常带有 DLL 扩展名),它由全局数据、服务函数和资源组成,在运行时被系统加载到进程的虚拟空间中,成为调用进程的一部分。如果与其它 DLL 之间没有冲突,该文件通常映射到进程虚拟空间的同一地址上。 DLL 模块中包含各种导出函数,用于向外界提供服务。 Windows 在加载 DLL 模块时将进程函数调用与 DLL 文件的导出函数相匹配。
  在 Win32 环境中,每个进程都复制了自己的读 / 写全局变量。如果想要与其它进程共享内存,必须使用内存映射文件或者声明一个共享数据段。 DLL 模块需要的堆栈内存都是从运行进程的堆栈中分配出来的。
   DLL 现在越来越容易编写。 Win32 已经大大简化了其编程模式,并有许多来自 AppWizard MFC 类库的支持。
  一、导出和导入函数的匹配
   DLL 文件中包含一个导出函数表。这些导出函数由它们的符号名和称为标识号的整数与外界联系起来。函数表中还包含了 DLL 中函数的地址。当应用程序加载 DLL 模块时时,它并不知道调用函数的实际地址,但它知道函数的符号名和标识号。动态链接过程在加载的 DLL 模块时动态建立一个函数调用与函数地址的对应表。如果重新编译和重建 DLL 文件,并不需要修改应用程序,除非你改变了导出函数的符号名和参数序列。
  简单的 DLL 文件只为应用程序提供导出函数,比较复杂的 DLL 文件除了提供导出函数以外,还调用其它 DLL 文件中的函数。这样,一个特殊的 DLL 可以既有导入函数,又有导入函数。这并不是一个问题,因为动态链接过程可以处理交叉相关的情况。
  在 DLL 代码中,必须像下面这样明确声明导出函数:
   __declspec(dllexport) int MyFunction(int n);
  但也可以在模块定义 (DEF) 文件中列出导出函数,不过这样做常常引起更多的麻烦。在应用程序方面,要求像下面这样明确声明相应的输入函数:
   __declspec(dllimport) int MyFuncition(int n);
  仅有导入和导出声明并不能使应用程序内部的函数调用链接到相应的 DLL 文件上。应用程序的项目必须为链接程序指定所需的输入库( LIB 文件)。而且应用程序事实上必须至少包含一个对 DLL 函数的调用。
  二、与 DLL 模块建立链接
  应用程序导入函数与 DLL 文件中的导出函数进行链接有两种方式:隐式链接和显式链接。所谓的隐式链接是指在应用程序中不需指明 DLL 文件的实际存储路径,程序员不需关心 DLL 文件的实际装载。而显式链接与此相反。
  采用隐式链接方式,程序员在建立一个 DLL 文件时,链接程序会自动生成一个与之对应的 LIB 导入文件。该文件包含了每一个 DLL 导出函数的符号名和可选的标识号,但是并不含有实际的代码。 LIB 文件作为 DLL 的替代文件被编译到应用程序项目中。当程序员通过静态链接方式编译生成应用程序时,应用程序中的调用函数与 LIB 文件中导出符号相匹配,这些符号或标识号进入到生成的 EXE 文件中。 LIB 文件中也包含了对应的 DLL 文件名(但不是完全的路径名),链接程序将其存储在 EXE 文件内部。当应用程序运行过程中需要加载 DLL 文件时, Windows 根据这些信息发现并加载 DLL ,然后通过符号名或标识号实现对 DLL 函数的动态链接。
  显式链接方式对于集成化的开发语言(例如 VB )比较适合。有了显式链接,程序员就不必再使用导入文件,而是直接调用 Win32 LoadLibary 函数,并指定 DLL 的路径作为参数。 LoadLibary 返回 HINSTANCE 参数,应用程序在调用 GetProcAddress 函数时使用这一参数。 GetProcAddress 函数将符号名或标识号转换为 DLL 内部的地址。假设有一个导出如下函数的 DLL 文件:
   extern "C" __declspec(dllexport) double SquareRoot(double d);
  下面是应用程序对该导出函数的显式链接的例子:
   typedef double(SQRTPROC)(double);
   HINSTANCE hInstance;
   SQRTPROC* pFunction;
   VERIFY(hInstance=::LoadLibrary("c://winnt//system32//mydll.dll"));
   VERIFY(pFunction=(SQRTPROC*)::GetProcAddress(hInstance,"SquareRoot"));
   double d=(*pFunction)(81.0);// 调用该 DLL 函数
  在隐式链接方式中,所有被应用程序调用的 DLL 文件都会在应用程序 EXE 文件加载时被加载在到内存中;但如果采用显式链接方式,程序员可以决定 DLL 文件何时加载或不加载。显式链接在运行时决定加载哪个 DLL 文件。例如,可以将一个带有字符串资源的 DLL 模块以英语加载,而另一个以西班牙语加载。应用程序在用户选择了合适的语种后再加载与之对应的 DLL 文件。
  三、使用符号名链接与标识号链接
  在 Win16 环境中,符号名链接效率较低,所有那时标识号链接是主要的链接方式。在 Win32 环境中,符号名链接的效率得到了改善。 Microsoft 现在推荐使用符号名链接。但在 MFC 库中的 DLL 版本仍然采用的是标识号链接。一个典型的 MFC 程序可能会链接到数百个 MFC DLL 函数上。采用标识号链接的应用程序的 EXE 文件体相对较小,因为它不必包含导入函数的长字符串符号名。
  四、编写 DllMain 函数
   DllMain 函数是 DLL 模块的默认入口点。当 Windows 加载 DLL 模块时调用这一函数。系统首先调用全局对象的构造函数,然后调用全局函数 DLLMain DLLMain 函数不仅在将 DLL 链接加载到进程时被调用,在 DLL 模块与进程分离时(以及其它时候)也被调用。下面是一个框架 DLLMain 函数的例子。
   HINSTANCE g_hInstance;
   extern "C" int APIENTRY DllMain(HINSTANCE hInstance,DWORD dwReason,LPVOID lpReserved)
   {
   if(dwReason==DLL_PROCESS_ATTACH)
   {
   TRACE0("EX22A.DLL Initializing!/n");
   // 在这里进行初始化
   }
   else if(dwReason=DLL_PROCESS_DETACH)
   {
   TRACE0("EX22A.DLL Terminating!/n");
   // 在这里进行清除工作
   }
   return 1;// 成功
   }
  如果程序员没有为 DLL 模块编写一个 DLLMain 函数,系统会从其它运行库中引入一个不做任何操作的缺省 DLLMain 函数版本。在单个线程启动和终止时, DLLMain 函数也被调用。正如由 dwReason 参数所表明的那样。
  五、模块句柄
  进程中的每个 DLL 模块被全局唯一的 32 字节的 HINSTANCE 句柄标识。进程自己还有一个 HINSTANCE 句柄。所有这些模块句柄都只有在特定的进程内部有效,它们代表了 DLL EXE 模块在进程虚拟空间中的起始地址。在 Win32 中, HINSTANCE HMODULE 的值是相同的,这个两种类型可以替换使用。进程模块句柄几乎总是等于 0x400000 ,而 DLL 模块的加载地址的缺省句柄是 0x10000000 。如果程序同时使用了几个 DLL 模块,每一个都会有不同的 HINSTANCE 值。这是因为在创建 DLL 文件时指定了不同的基地址,或者是因为加载程序对 DLL 代码进行了重定位。
  模块句柄对于加载资源特别重要。 Win32 FindResource 函数中带有一个 HINSTANCE 参数。 EXE DLL 都有其自己的资源。如果应用程序需要来自于 DLL 的资源,就将此参数指定为 DLL 的模块句柄。如果需要 EXE 文件中包含的资源,就指定 EXE 的模块句柄。
  但是在使用这些句柄之前存在一个问题,你怎样得到它们呢?如果需要得到 EXE 模块句柄,调用带有 Null 参数的 Win32 函数 GetModuleHandle ;如果需要 DLL 模块句柄,就调用以 DLL 文件名为参数的 Win32 函数 GetModuleHandle
  六、应用程序怎样找到 DLL 文件
  如果应用程序使用 LoadLibrary 显式链接,那么在这个函数的参数中可以指定 DLL 文件的完整路径。如果不指定路径,或是进行隐式链接, Windows 将遵循下面的搜索顺序来定位 DLL
   1 包含 EXE 文件的目录,
   2 进程的当前工作目录,
   3 Windows 系统目录,
   4 Windows 目录,
   5 列在 Path 环境变量中的一系列目录。
  这里有一个很容易发生错误的陷阱。如果你使用 VC ++进行项目开发,并且为 DLL 模块专门创建了一个项目,然后将生成的 DLL 文件拷贝到系统目录下,从应用程序中调用 DLL 模块。到目前为止,一切正常。接下来对 DLL 模块做了一些修改后重新生成了新的 DLL 文件,但你忘记将新的 DLL 文件拷贝到系统目录下。下一次当你运行应用程序时,它仍加载了老版本的 DLL 文件,这可要当心!
  七、调试 DLL 程序
   Microsoft VC ++是开发和测试 DLL 的有效工具,只需从 DLL 项目中运行调试程序即可。当你第一次这样操作时,调试程序会向你询问 EXE 文件的路径。此后每次在调试程序中运行 DLL 时,调试程序会自动加载该 EXE 文件。然后该 EXE 文件用上面的搜索序列发现 DLL 文件,这意味着你必须设置 Path 环境变量让其包含 DLL 文件的磁盘路径,或者也可以将 DLL 文件拷贝到搜索序列中的目录路径下。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值