WIN32汇编语言教程:第17章 PE文件 · 17.3 导出表(1)

当PE文件被执行的时候,Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中的函数导出信息对被执行文件的IAT表进行修正。在这些包含导出函数的DLL文件中,导出信息被保存在导出表中,通过导出表,DLL文件向系统提供导出函数的名称、序号和入口地址等信息,以便Windows装载器通过这些信息来完成动态链接的过程。

扩展名为.exe的PE文件中一般不存在导出表,而大部分的.dll文件中都包含导出表,但是这并不是必然的,比如用作纯资源的.dll文件就不提供导出函数,文件中也就不存在导出表;另外,偶尔也可以见到包含导出函数和导出表的.exe文件。本节将介绍导出表的结构和使用方法。

17.3.1 导出表的结构

1. 获取导出表的位置

导出表的位置和大小可以从PE文件头中的数据目录中获取,与导出表对应的项目是数据目录中的首个IMAGE_DATA_DIRECTORY结构,从这个结构的VirtualAddress字段得到的就是导出表的RVA值。

与操作导入表时的方法类似,如果在内存中查找导出表,那么将RVA值加上PE文件装入的基址就是实际的地址;如果在磁盘上的PE文件中查找导出表,那么使用_RVAToOffset子程序将RVA转换成文件偏移就可以了。

2. 导出表的组成

显然,导出表的功能必须与导入表的功能相配合,既然在导入表中可以用函数名或序号来导入,那么导出表中必然要提供函数的名称和函数序号。实际上,函数的导出方法也分为使用函数名导出和使用序号导出两种,导出表中为每个导出函数定义了导出序号,但函数名的定义是可选的。对于定义了函数名的函数来说,既可以使用名称导出,也可以使用序号导出;对于没有定义函数名的函数来说,只能使用序号来导出。

导出表的起始位置有一个IMAGE_EXPORT_DIRECTORY结构,与导入表中有多个IMAGE_IMPORT_DESCRIPTOR结构不同,导出表中只有一个IMAGE_EXPORT_DIRECTORY结构,这个结构的定义如下:

IMAGE_EXPORT_DIRECTORY STRUCT
 Characteristics           DWORD     ? ;未使用,总是为0
 TimeDateStamp             DWORD     ? ;文件的产生时刻
 MajorVersion              WORD      ?    ;未使用,总是为0
 MinorVersion              WORD      ?    ;未使用,总是为0
 nName                          DWORD     ? ;指向文件名的RVA
 nBase                          DWORD     ? ;导出函数的起始序号
 NumberOfFunctions         DWORD     ? ;导出函数的总数
 NumberOfNames             DWORD     ? ;以名称导出的函数总数
 AddressOfFunctions        DWORD     ? ;指向导出函数地址表的RVA
 AddressOfNames            DWORD     ? ;指向函数名地址表的RVA
 AddressOfNameOrdinals    DWORD     ? ;指向函数名序号表的RVA
IMAGE_EXPORT_DIRECTORY ENDS

这个结构中的一些字段并没有被使用,其余有意义的字段说明如下,读者可以参考图17.7来理解这些字段之间的关系。


图17.7  函数导出的示意图

● nName字段

这个字段是一个RVA值,指向一个定义了模块名称的字符串。这个字符串说明了模块的原始文件名,比如说即使Kernel32.dll文件被改名为Ker.dll,仍然可以从这个字符串中的值得知它被编译时的文件名是“Kernel32.dll”。

● NumberOfFunctions字段

文件中包含的导出函数的总数。

● NumberOfNames字段

被定义了函数名称的导出函数的总数。显然,只有这个数量的函数既可以用函数名方式导出,也可以用序号方式导出,剩下的NumberOfFunctions减去NumberOfNames数量的函数只能用序号方式导出。NumberOfNames字段的值只会小于或者等于NumberOfFunctions字段的值,如果这个值是0,表示所有的函数都是以序号方式导出的。

● AddressOfFunctions字段

这是一个RVA值,指向包含全部导出函数入口地址的双字数组,数组中的每一项是一个RVA值,数组的项数等于NumberOfFunctions字段的值。

● nBase字段

导出函数序号的起始值。将AddressOfFunctions字段指向的入口地址表的索引号加上这个起始值就是对应函数的导出序号,举例来说,假如nBase字段的值为x,那么入口地址表指定的第一个导出函数的序号就是x,第二个导出函数的序号就是x+1,总之,一个导出函数的导出序号等于nBase字段的值加上其在入口地址表中的位置索引值。

● AddressOfNames和AddressOfNameOrdinals字段

AddressOfNames字段的数值是一个RVA值,指向函数名字符串地址表,这个地址表是一个双字数组,数组中的每一项指向一个函数名称字符串的RVA,数组的项数等于NumberOfNames字段的值,所有有名称的导出函数的名称字符串都定义在这个表中。

那么这些函数名称究竟对应地址表中的那个函数呢?AddressOfNameOrdinals字段就派上用途了,这个字段也是一个RVA值,指向另一个word类型的数组(注意不是双字数组),数组的项目与文件名地址表中的项目一一对应,项目的值代表函数入口地址表的索引,这样函数名称与函数入口地址就关联起来了。

举例说明,假如函数名称字符串地址表的第n项指向一个字符串“MyFunction”,那么可以去查找AddressOfNameOrdinals字段指向的数组的第n项,假如第n项中存放的值是x,表示AddressOfFunctions字段描述的地址表中的第x项函数入口地址(假定入口地址值是aaaa)对应的函数名就是“MyFunction”,这时这个函数的全部信息就可以如下描述。

函数名称:MyFunction,导出序号:nBase的值+x,入口地址:aaaa

可以看到,AddressOfNameOrdinals字段描述的数组仅仅起了一个桥梁的作用。

3. 从序号查找入口地址

下面来模拟一下Windows装载器查找导出函数入口地址的过程。如果已知函数的导出序号,如何得到入口地址呢?

步骤如下所示:

(1)定位到PE文件头。

(2)从PE文件头中的IMAGE_OPTIONAL_HEADER32结构中取出数据目录表,并从第一个数据目录中得到导出表的地址。

(3)从导出表的nBase字段得到起始序号。

(4)将需要查找的导出序号减去起始序号,就得到了函数在入口地址表中的索引。

(5)检测索引值是否大于导出表的NumberOfFunctions字段的值,如果大于后者的话,说明输入的序号是无效的。

(6)用这个索引值在AddressOfFunctions字段指向的导出函数入口地址表中取出相应的项目,这就是函数的入口地址RVA值,当函数被装入内存的时候,这个RVA值加上模块实际装入的基址,就得到了函数真正的入口地址。

上页:第17章 PE文件 · 17.2 导入表(3) 下页:第17章 PE文件 · 17.3 导出表(2)

第17章 PE文件

版权所有 © 云南伯恩科技 证书:粤ICP备09170368号