WINDOWS程式设计--动态链接库(2)

创建DLL模块

当创建DLL时,要创建一组可执行模块(或其他DLL)可以调用的函数。DLL可以将变量、函数或C/C++类输出到其他模块。在实际工作环境中,应该避免输出变量,因为这会删除你的代码中的一个抽象层,使它更加难以维护你的DLL代码。此外,只有当使用同一个供应商提供的编译器对输入C++类的模块进行编译时,才能输出C++类。由于这个原因,也应该避免输出C++类,除非知道可执行模块的开发人员使用的工具与DLL模块开发人员使用的工具相同。

当创建DLL模块时,首先应该建立一个头文件,该文件包含了你想要输出的变量(类型和名字)和函数(原型和名字)。头文件还必须定义用于输出函数和变量的任何符号和数据结构。你的DLL的所有源代码模块都应该包含这个头文件。另外,必须分配该头文件,以便它能够包含在可能输入这些函数或变量的任何源代码中。拥有单个头文件,供DLL创建程序和可执行模块的创建程序使用,就可以大大简化维护工作。

下面的代码说明了应该如何对单个头文件进行编码,以便同时包含可执行文件和DLL的源代码文件:

/*************************************************************************

Module: MyLib.h

*************************************************************************/

 

#ifdef MYLIBAPI

 

// MYLIBAPI should be defined in all of the DLL's source

// code modules before this header file is included.

 

// All functions/variables are being exported

 

#else

 

// This header file is included by an EXE source code module

//Indicate that all functions/variables are being imported

#define MYLIBAPI extern "C" _declspec(dllimport)

 

#endif

 

/

//Define any data structures and symbols here

 

//

//Define exported variables here .(NOTE: Avoid exporting variables.)

MYLIBAPI int g_nResult;

/

//Define exported function prototypes here.

MYLIBAPI int Add(int nLeft , int nRight);

// End of File /

在你的每个DLL源代码文件中,应该包含下面的头文件:

/*****************************************************************

Module: MyLibFile1.cpp

******************************************************************/

 

// Include the standar Windows and C-Runtime header files here.

#include <windows.h>

 

//This DLL source code file exports functions and variables

#define MYLIBAPI extern "C" _declspce(dllexport)

 

//Include the exported data structures,symbols,functions,and variable.

#include "MyLib.h"

 

///

// Place the code for this DLL source code file here

int g_nResult;

 

int Add(int nLeft,int nRight) {

    g_nResult = nLeft + nRight;

    return(g_nResult)

};

End of File

当上面的DLL源代码文件被编译时,在MyLib.h头文件的前面使用__declspec(DLLexport)对MYLIBAPI进行定义。当编译器看到负责修改变量、函数或C++类的__declspec(DLLexport)时,它就知道该变量、函数或C++类是从产生的DLL模块输出的。注意,MYLIBAPI标志被置于头文件中要输出的变量的定义之前和要输出的函数之前。

另外,在源代码文件(MyLibFile1.cpp0)中,MYLIBAPI标志并不出现在输出的变量和函数之前。MYLIBAPI标志在这里是不必要的,因为编译器在分析头文件时能够记住要输出哪些变量或函数。

你会发现,MYLIBAPI标志包含了extern“C”修改符。只有当你编写C++代码而不是直接编写C代码时,才能使用这个修改符。通常来说,C++编译器可能会改变函数和变量的名字,从而导致严重的链接程序问题。例如,假设你用C++编写一个DLL,并直接用C编写一个可执行模块,当你创建DLL时,函数名被改变,但是,当你创建可执行模块时,函数名没有改变。当链接程序试图链接可执行模块时,它就会抱怨说,可执行模块引用的符号不存在。如果使用extern“C”,就可以告诉编译器不要改变变量名或函数名,这样,变量和函数就可以供使用C、C++或任何其他编程语言编写的可执行模块来访问。

现在你已经知道DLL源代码文件是如何使用这个头文件的。但是,可执行模块的源代码文件情况又是如何呢?可执行模块的源代码文件不应该在这个头文件的前面定义MYLIBAPI。由于MYLIBAPI没有定义,因此头文件将MYLIBAPI定义为__declspec(DLLimport)。编译器看到可执行模块的源代码文件从DLL模块输入变量和函数。

如果观察Microsoft的标准Windows头文件,如WinBase.h,你将会发现Microsoft使用的方法基本上与上面介绍的方法相同。

输出的真正含义是什么

上一节介绍的一个真正有意思的东西是__declspec(DLLexport)修改符。当Microsoft的C/C++编译器看到变量、函数原型或C++类之前的这个修改符的时候,它就将某些附加信息嵌入产生的.obj文件中。当链接DLL的所有.obj文件时,链接程序将对这些信息进行分析。

当DLL被链接时,链接程序要查找关于输出变量、函数或C++类的信息,并自动生成一个.lib文件。该.lib文件包含一个DLL输出的符号列表。当然,如果要链接引用该DLL的输出符号的任何可执行模块,该.lib文件是必不可少的。除了创建.lib文件外,链接程序还要将一个输出符号表嵌入产生的DLL文件。这个输出节包含一个输出变量、函数和类符号的列表(按字母顺序排列)。该链接程序还将能够指明在何处找到每个符号的相对虚拟地址(RVA)放入DLL模块。

使用Microsoft的VisualStudio的DumpBin.exe实用程序(带有-exports开关),你能够看到DLL的输出节是个什么样子。下面是Kernel32.DLL的输出节的一个代码段(我已经删除了DUMPBIN的某些输出,这样就不会占用本书的太多篇幅)。

C:|WINNT|SYSTEM32>DUMPBIN -exports Kernel32.DLL

 

Microsoft (R) COFF Binary File Dumper Version 6.00.8168

Copyright (C) Microsoft Corp 1992-1999.All right reserved.

 

Dump of file kernel32.dll

 

File Type: DLL

 

    Section contains the following exports for KERNEL32.dll

 

           0 characteristics

    36DB3213 time date stamp Mon Mar 01 16:34:27 1999

        0.00 version

           1 ordinal base

         829 number of functions

         829 number of names

 

  ordinal hint RVA     name

            1     0 0001A3C6 AddAtomA

          2     1 0001A367 AddAtomW

         3     2 0003F7C4 AddConsoleAliasA

         4     3 0003F78D AddConsoleAliasW

         5     4 0004085C AllocConsole

           6     5 0002C91D AllocateUserPhysicalPages

          7     6 00005953 AreFileApisANSI

          8     7 0003F1A0 AssignProcessToJobObject

          9     8 00021372 BackupRead

        10    9 000215CE BackupSeek

      11    A 00021F21 BackupWrite

        .

        .

        .

    828 33B 00003200 lstrlenA

    829 33C 000040D5 lstrlenW 

 

Summary

    3000 .date

    4000 .reloc

   4D000 .rsrc

   59000 .text

如你所见,这些符号是按字母顺序排列的,RVA这一列下面的数字用于指明在DLL文件映像中的什么位置能够找到输出符号的位移量。序号列可以与16位Windows源代码向后兼容,并且它不应该用于现在的应用程序中。hint(提示码)列可供系统用来改进代码的运行性能,在此并不重要。

注意 许多开发人员常常通过为函数赋予一个序号值来输出DLL函数。对于那些来自16位Windows环境的函数来说,情况尤其是如此。但是,Microsoft并没有公布系统DLL的序号值。当你的可执行模块或DLL模块链接到任何一个Windows函数时,Microsoft要求你使用符号的名字进行链接。如果你按照序号进行链接,那么你的应用程序有可能无法在其他Windows平台或将来的Windows平台上运行。

实际上,我就遇到过这样的情况。我曾经发布了一个示例应用程序,它使用MicrosoftSystemJournal中的序号。我的应用程序在WindowsNT3.1上运行得很好,但是当WindowsNT3.5推出时,我的应用程序就无法正确地运行。为了解决这个问题,我不得不用函数名代替序号。现在该应用程序既能够在WindowsNT3.1上运行,而且能够在所有更新的版本上运行。

我问过Microsoft公司,为什么它不使用序号,我得到的回答是:“我们认为可移植的可执行文件格式不仅具有序号的优点(查找迅速),而且提供了按名字输入的灵活性。我们可以随时增加函数。在带有多个实现代码的大型程序项目中,序号很难管理。”

你可以将序号用于你创建的任何DLL,并且按照序号将你的可执行文件链接到这些DLL。Microsoft保证,即使在将来的操作系统版本中,这个方法也是可行的。但是我在我的工作中总是避免使用序号,并且从现在起只按名字进行链接。

创建用于非VisualC++工具的DLL  

如果使用MicrosoftVisualC++来创建DLL和将要链接到该DLL的可执行模块,可以跳过本节内容的学习。但是,如果使用VisualC++创建DLL,而这个DLL要链接到使用任何供应商的工具创建的可执行模块,那么必须做一些额外的工作。

前面讲过当进行C和C++混合编程时使用extern“C”修改符的问题。也讲过C++类的问题以及为什么因为名字改变的缘故你必须使用同一个编译器供应商的工具的问题。当你直接将C语言编程用于多个工具供应商时将会出现另一个问题。这个问题是,即使你根本不使用C++,Microsoft的C编译器也会损害C函数。当你的函数使用__stdcall(WINAPI)调用规则时会出现这种问题。这种调用规则是最流行的一种类型。当使用__stdcall将C函数输出时,Microsoft的编译器就会改变函数的名字,设置一个前导下划线,再加上一个@符号的前缀,后随一个数字,表示作为参数传递给函数的字节数。例如,下面的函数是作为DLL的输出节中的_MyFunc@8输出的:

_declspec(dllexport) LONG _stdcall MyFunc(int a, int b);

如果用另一个供应商的工具创建了一个可执行模块,它将设法链接到一个名叫MyFunc的函数,该函数在Microsoft编译器已有的DLL中并不存在,因此链接将失败。

若要使用与其他编译器供应商的工具链接的Microsoft的工具创建一个可执行模块,必须告诉Microsoft的编译器输出没有经过改变的函数名。可以用两种方法来进行这项操作。第一种方法是为编程项目建立一个.def文件,并在该.def文件中加上类似下面的EXPORTS节:

EXPORTS

    MyFunc

当Microsoft的链接程序分析这个.def文件时,它发现_MyFunc@8和MyFunc均被输出。由于这两个函数名是互相匹配的(除了截断的尾部外),因此链接程序使用MyFunc的.def文件名来输出该函数,而根本不使用_MyFunc@8的名字来输出函数。

现在你可能认为,如果使用Microsoft的工具创建一个可执行模块,并且设法将它链接到包含未截断名字的DLL,那么链接程序的运行将会失败,因为它将试图链接到称为_MyFunc@8的函数。当然,你会高兴地了解到Microsoft的链接程序进行了正确的操作,将可执行模块链接到名字为MyFunc的函数。

如果想避免使用.def文件,可以使用第二种方法输出未截断的函数版本。在DLL的源代码模块中,可以添加下面这行代码:

#pragma comment(linker , "/export:MyFunc = _MyFunc@8")

这行代码使得编译器发出一个链接程序指令,告诉链接程序,一个名叫MyFunc的函数将被输出,其进入点与称为_MyFunc@8的函数的进入点相同。第二种方法没有第一种方法容易,因为你必须自己截断函数名,以便创建该代码行。另外,当使用第二种方法时,DLL实际上输出用于标识单个函数的两个符号,即MyFunc和_MyFunc@8,而第一种方法只输出符号MyFunc。第二种方法并没有给你带来更多的好处,它只是使你可以避免使用.def的文件而已。

创建可执行模块

下面的代码段显示了一个可执行的源代码文件,它输入了DLL的输出符号,并且在代码中引用了这些符号。

/*****************************************************************

Module: MyExeFile1.cpp

******************************************************************/

 

// Include the standar Windows and C-Runtime header files here.

#include <windows.h>

 

//Include the exported data structures,symbols , functions,and variables

#include "MyLib/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 from the last Add is: %d"), g_nResult);

    MessageBox(NULL, sz, TEXT("Last Result"),MB_OK);

 

    return(0);

}

End of File

当创建可执行源代码文件时,必须加上DLL的头文件。如果没有头文件,输入的符号将不会被定义,而且编译器将会发出许多警告和错误消息。

可执行源代码文件不应该定义DLL的头文件前面的MYLIBAPI。当上面显示的这个可执行源代码文件被编译时,MYLIBAPI由MyLib.h头文件使用__declspec(DLLimport)进行定义。当编译器看到修改变量、函数或C++类的__declspec(DLLimport)时,它知道这个符号是从某个DLL模块输入的。它不知道是从哪个DLL模块输入的,并且它也不关心这个问题。编译器只想确保你用正确的方法访问这些输入的符号。现在你在源代码中可以引用输入的符号,一切都将能够正常工作。

接着,链接程序必须将所有.obj模块组合起来,创建产生的可执行模块。该链接程序必须确定哪些DLL包含代码引用的所有输入符号的DLL。因此你必须将DLL的.lib文件传递给链接程序。如前所述,.lib文件只包含DLL模块输出的符号列表。链接程序只想知道是否存在引用的符号和哪个DLL模块包含该符号。如果连接程序转换了所有外部符号的引用,那么可执行模块就因此而产生了。

输入的真正含义是什么

上一节介绍了修改符--declspec(DLLimport)。当输入一个符号时,不必使用关键字--declspec(DLLimport),只要使用标准的C关键字extern即可。但是,如果编译器预先知道你引用的符号将从一个DLL的.lib文件输入,那么编译器就能够生成运行效率稍高的代码。因此建议你尽量将--declspec(DLLimport)关键字用于输入函数和数据符号。当你调用标准Windows函数中的任何一个时,Microsoft将为你进行这项设置。

当链接程序进行输入符号的转换时,它就将一个称为输入节的特殊的节嵌入产生的可执行模块。输入节列出了该模块需要的DLL模块以及由每个DLL模块引用的符号。

使用VisualStudio的DumpBin.exe实用程序(带有-imports开关),能够看到模块的输入节的样子。下面是Calc.exe文件的输入节的一个代码段(同样,我删除了DUMPBIN的某些输出,这样它就不会占用太多的篇幅)。

C:/WINNT/SYSTEM32>DUMPBIN -imports Calc.EXE

 

Microsoft (R) COFF Binary File Dumper Version 6.00.8168

Copyright (C) Microsoft Corp 1992-1998.All rights reserved.

 

Dump of file calc.exe

 

File Type: EXECUTABLE IMAGE

 

    Section contains the following imports;

 

    SHELL32.dll

        10010F4 Import Address Table

        1012820 Import Name Table

       FFFFFFFF time date stamp

       FFFFFFFF Index of first forwarder reference

 

    77C42983    7A ShellAboutW

 

   MSVCRT.dll

    1001094 Import Address Table

    10127C0 Import Name Table

   FFFFFFFF time date stamp

   FFFFFFFF Index of first forwarder reference

 

   78010040    295 memmove

   78018124    42  _EH_prolog

   78014C34    2D1 toupper

   78010F6E    2DD wcschr

   78010668    2E3 wcslen

.

.

.

ADVAPI32.dll

            1001000 Import Address Table

            101272C Import Name   Table

           FFFFFFFF time date.stamp

           FFFFFFFF Index of first forwarder reference

 

    779858F4    19A  RegQueryValueExA

    77985196    190  RegOpenKeyExA

    77984BA1    178  RegCloseKey

 

KERNEL32.dll

    100101C Import Address Table

    1012748 Import Name Table

   FFFFFFFF time date stamp

   FFFFFFFF Index of first forwarder reference

 

77ED4134    336 lstrcpyW

77ED33EB    1E5 LocalAlloc

77EDEF36    DB  GetCommandLineW

77ED1610    15E GetProfileIntW

77ED4BA4    1EC LocalReAlloc

.

.

.

 

Header contains the followint bound import information

 Bount to SHELL32.dll [36E449E0] Mon Mar 08 14:06:24 1999

 Bount to MSVCRT.dll [36888379] Fri Feb 05 15:49:13 1999

 Bount to ADVAPI32.dll [36E449E1] Mon Mar 08 14:06:25 1999

 Bount to KERNEL32.dll [36DDAD55] Wed Mar 03 13:44:53 1999

 Bount to GDI32.dll [36E449E0] Mon Mar 08 14:06:24 1999

 Bount to USER32.dll [36E449E0] Mon Mar 08 14:06:24 1999

 

Summary 

         2000 .data

         3000 .rsrc

        13000 .text

如你所见,这一节为Calc.exe需要的每个DLL设置了一个项目,这些DLL是Shell32.DLL、MSVCRt.DLL、AdvAPI32.DLL、Kernel32.DLL、GDI32.DLL和User32.DLL。在每个DLL的模块名下面,有一个Calc.exe从该特定模块输入的符号列表。例如,Calc模块调用包含在Kernel32.DLL中的下列函数:lstrcpyW、LocalAlloc、GetCommandLineW和GetProfileIntW等。

紧靠符号名左边的数字是符号的提示(hint)值,它与讨论无关。每个符号行最左边的数字用于指明该符号在进程的地址空间中所在的内存地址。该内存地址只有在可执行模块相链接时才出现。在DumpBin的输出的结尾处,可以看到更多的链接信息。

运行可执行模块

当一个可执行文件被启动时,操作系统加载程序将为该进程创建虚拟地址空间。然后,加载程序将可执行模块映射到进程的地址空间中。加载程序查看可执行模块的输入节,并设法找出任何需要的DLL,并将它们映射到进程的地址空间中。

由于该输入节只包含一个DLL名而没有它的路径名。因此加载程序必须搜索用户的磁盘驱动器,找出DLL。下面是加载程序的搜索顺序:

1)包含可执行映像文件的目录。

2)进程的当前目录。

3)Windows系统目录。

4)Windows目录。

5)PATH环境变量中列出的各个目录。

应该知道其他的东西也会影响加载程序对一个DLL的搜索(详细说明参见第20章)。当DLL模块映射到进程的地址空间中时,加载程序要检查每个DLL的输入节。如果存在输入节(通常它确实是存在的),那么加载程序便继续将其他必要的DLL模块映射到进程的地址空间中。加载程序将保持对DLL模块的跟踪,使模块的加载和映射只进行一次(尽管多个模块需要该模块)。

如果加载程序无法找到需要的DLL模块,用户会看到图19-2、图19-3所示的消息框中的一个:如果是Windows2000,那么将出现图19-2所示的消息框,如果是Windows98,则出现图19-3所示的消息框。

当所有的DLL模块都找到并且映射到进程的地址空间中之后,加载程序就会确定对输入的符号的全部引用。为此,它要再次查看每个模块的输入节。对于列出的每个符号,加载程序都要查看指定的DLL的输出节,以确定该符号是否存在。如果该符号不存在(这种情况很少),那么加载程序就显示图19-4、图19-5所示的消息框之一:如果是Windows2000,那么出现图19-4所示的消息框,如果是Windows98,则出现图19-5所示的消息框。

如果Windows2000版本的消息框指明漏掉的是哪个函数,而不是显示用户难以识别的错误代码0xC000007B,那么这将是非常好的。也许下一个Windows版本能够做到这一点。

如果这个符号不存在,那么加载程序将要检索该符号的RVA,并添加DLL模块被加载到的虚拟地址空间(符号在进程的地址空间中的位置)。然后它将该虚拟地址保存在可执行模块的输入节中。这时,当代码引用一个输入符号时,它将查看调用模块的输入节,并且捕获输入符号的地址,这样它就能够成功地访问输入变量、函数或C++类的成员函数。好了,动态链接完成,进程的主线程开始执行,应用程序终于也开始运行了!

当然,这需要加载程序花费相当多的时间来加载这些DLL模块,并用所有使用输入符号的正确地址来调整每个模块的输入节。由于所有这些工作都是在进程初始化的时候进行的,因此应用程序运行期的性能不会降低。不过,对于许多应用程序来说,初始化的速度太慢是不行的。为了缩短应用程序的加载时间,应该调整你的可执行模块和DLL模块的位置并且将它们连接起来。真可惜很少有开发人员知道如何进行这项操作,因为这些技术是非常重要的。如果每个公司都能够使用这些技术,系统将能运行的更好。实际上,我认为操作系统销售时应该配有一个能够自动执行这些操作的实用程序。下一章将要介绍对模块调整位置和进行连接的方法。

 


<script type="text/javascript"> google_ad_client = "pub-2416224910262877"; google_ad_width = 728; google_ad_height = 90; google_ad_format = "728x90_as"; google_ad_channel = ""; google_color_border = "E1771E"; google_color_bg = "FFFFFF"; google_color_link = "0000FF"; google_color_text = "000000"; google_color_url = "008000"; </script><script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值