17.2.1
导入表简介
在Win32编程中常常用到“导入函数”(Import functions),导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL中,在调用者程序中只保留一些函数信息,包括函数名及其驻留的DLL名等。
对于存储在磁盘上的PE文件来说,它无法得知这些导入函数会在内存的哪个地方出现,只有当PE文件被装入内存的时候,Windows装载器才将DLL装入,并将调用导入函数的指令和函数实际所处的地址联系起来,这就是“动态链接”的概念。动态链接是通过PE文件中定义的“导入表”(Import Table)来完成的,导入表中保存的正是函数名和其驻留的DLL名等动态链接所必需的信息。
1. 调用导入函数的指令
程序被执行的时候是怎样使用导入函数的呢?先将第3章中那个简单的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装载器能够顺利地进行上面的转换工作的。