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

在开始下面几节的介绍前,先来复习一下17.1节中提出的两个概念。

首先,PE文件中的数据按照装入内存后的页面属性被划分成多个节,并由节表中的数据来描述这些节。一个节中的数据仅仅是属性相同而已,并不一定就是同一种用途的,比如导入表、导出表等就有可能和只读常量一起被放在同一个节中,因为它们的属性同是可读不可写的。

其次,由于不同用途的数据可能被放在同一个节中,仅仅依靠节表是无法确定它们的存放位置的,PE文件中依靠文件头中IMAGE_OPTIONAL_HEADER32结构内的数据目录表来指出它们的位置,可以由数据目录表来定位的数据包括导入表、导出表、资源、重定位表和TLS等15种数据。

好了,现在要引出这几节将要讲述的内容了:从数据目录表得到的是这些数据的RVA和数据块的尺寸,很明显,不同的数据块中的数据组织方式是不同的,比如导入表和资源数据块中的数据就完全是两码事情,要想深入了解PE文件就必须了解这些数据的组织方式以及系统是如何处理它们的,这就是本节以及下面几个小节的内容。

本节将首先介绍导入表的格式,下面的几个小节将逐一介绍导出表、资源和重定位表的格式和使用方法。

17.2.1 导入表简介

在Win32编程中常常用到“导入函数”(Import functions),导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL中,在调用者程序中只保留一些函数信息,包括函数名及其驻留的DLL名等。

对于存储在磁盘上的PE文件来说,它无法得知这些导入函数会在内存的哪个地方出现,只有当PE文件被装入内存的时候,Windows装载器才将DLL装入,并将调用导入函数的指令和函数实际所处的地址联系起来,这就是“动态链接”的概念。动态链接是通过PE文件中定义的“导入表”(Import Table)来完成的,导入表中保存的正是函数名和其驻留的DLL名等动态链接所必需的信息。

1. 调用导入函数的指令

程序被执行的时候是怎样使用导入函数的呢?先将第03章中那个简单的Hello World程序反汇编一把,看看调用导入函数的指令都是什么样子的,需要反汇编的两句源代码如下:

   invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
   invoke ExitProcess,NULL

当使用W32Dasm反汇编以后,这两句代码变成了以下的指令:

:00401000 6A00                  push 00000000
:00401002 6800304000                push 00403000
:00401007 680F304000                push 0040300F
:0040100C 6A00                   push 00000000
:0040100E E807000000                Call 0040101A      ;MessageBox
:00401013 6A00                   push 00000000
:00401015 E806000000                Call 00401020      ;ExitProcess
:0040101A FF2508204000           Jmp dword ptr [00402008]
:00401020 FF2500204000           Jmp dword ptr [00402000]

反汇编后,对MessageBox和ExitProcess函数的调用变成了对0040101A和00401020地址的调用,但是这两个地址显然是位于程序自身模块而不是在DLL模块中的,实际上,这是由编译器在程序所有代码的后面自动加上的Jmp dword ptr [xxxxxxxx]类型的指令,这个指令是一个间接寻址的跳转指令,xxxxxxxx地址中存放的才是真正的导入函数的地址。在这个例子中,00402000地址处存放的就是ExitProcess函数的地址。

那么在没有装载到内存之前,PE文件中的00402000地址处的内容是什么呢?使用在17.1.4节中了解的方法来查看一下。

首先,使用17.1.4节的例子文件PEInfo.exe去查看一下Hello.exe文件,会得到以下的信息:

文件名:C:\Documents and Settings\Administrator\桌面\Hello.exe
----------------------------------------------------------
运行平台:         0x014C
节区数量:         3
文件标记:         0x010F
建议装入地址:     0x00400000
----------------------------------------------------------
节区名称 节区大小 虚拟地址 Raw_尺寸 Raw_偏移 节区属性
----------------------------------------------------------
.text    00000026 00001000 00000200 00000400 60000020
.rdata   00000092 00002000 00000200 00000600 40000040
.data    00000022 00003000 00000200 00000800 C0000040

由于建议装入地址是00400000h,所以00402000h地址实际上处于RVA为2000h的地方,再看看各个节的虚拟地址,可以发现2000h开始的地方位于.rdata节内,而这个节的Raw_偏移项目为600h,也就是说00402000h地址的内容实际上对应PE文件中偏移600h处的数据。

现在随便找一个16进制编辑器来看看文件0600h处的内容是什么:

0600 76 20 00 00 00 00 00 00-5C 20 00 00 00 00 00 00  v ......\ ......
0610 54 20 00 00 00 00 00 00-00 00 00 00 6A 20 00 00  T ..........j ..
0620 08 20 00 00 4C 20 00 00-00 00 00 00 00 00 00 00  . ..L ..........
0630 84 20 00 00 00 20 00 00-00 00 00 00 00 00 00 00  . ... ..........
0640 00 00 00 00 00 00 00 00-00 00 00 00 76 20 00 00  ............v ..
0650 00 00 00 00 5C 20 00 00-00 00 00 00 BB 01 4D 65  ....\ ........Me
0660 73 73 61 67 65 42 6F 78-41 00 55 53 45 52 33 32   ssageBoxA.USER32
0670 2E 64 6C 6C 00 00 75 00-45 78 69 74 50 72 6F 63  .dll..u.ExitProc
0680 65 73 73 00 4B 45 52 4E-45 4C 33 32 2E 64 6C 6C  ess.KERNEL32.dll
0690 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

查看的结果是00002076h,这显然不会是内存中的ExitProcess函数的地址,慢着!将它作为RVA看会怎么样呢?查看节表可以发现RVA地址00002076h也处于.rdata节内,减去节的起始地址00002000h后得到这个RVA相对于节首的偏移是76h,也就是说它对应文件0676h开始的地方,接下来可以惊奇地发现,0676h再过去两个字节的内容正是函数名字符串“ExitProcess”!

这都有点搞糊涂了,Call ExitProcess指令被编译成了Call aaaaaaaa类型的指令,而aaaaaaaa处的指令是Jmp dword ptr [xxxxxxxx],而xxxxxxxx地址的地方只是一个似乎是指向函数名字符串的RVA地址,这一系列的指令显然是无法正确执行的!

但如果告诉你,当PE文件被装载的时候,Windows装载器会根据xxxxxxxx处的RVA得到函数名,再根据函数名在内存中找到函数地址,并且用函数地址将xxxxxxxx处的内容替换成真正的函数地址,那么所有的疑惑就迎刃而解了。

接下来看看如何去获取导入表的位置,以及导入表中的数据是如何组织以便Windows装载器能够顺利地进行上面的转换工作的。

2. 获取导入表的位置

导入表的位置和大小可以从PE文件头中IMAGE_OPTIONAL_HEADER32结构的数据目录字段中获取,对应的项目是DataDirectory字段的第2个IMAGE_DATA_DIRECTORY结构(见表17.4)。

从IMAGE_DATA_DIRECTORY结构的VirtualAddress字段得到的是导入表的RVA值,如果在内存中查找导入表,那么将RVA值加上PE文件装入的基址就是实际的地址;如果在PE文件中查找导入表,那么需要首先使用17.1.4节中例举的_RVAToOffset子程序将RVA首先转换成文件偏移。

上页:第17章 PE文件 · 17.1 PE文件的结构(8) 下页:第17章 PE文件 · 17.2 导入表(2)

第17章 PE文件

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