前面两篇 PE文件结构详解(四)PE导入表 和 PE文件结构详解(五)延迟导入表 介绍了PE文件中比较常用的两种导入方式,不知道大家有没有注意到,在调用导入函数时系统生成的代码是像下面这样的:
在这里,IE的iexplorer.exe导入了Kernel32.dll的GetCommandLineA函数,可以看到这是个间接call,00401004这个地址的内存里保存了目的地址,
根据图中显示的符号信息可知,00401004这个地址是存在于iexplorer.exe模块中的,实际上也就是一项IAT的地址。这个是IE6的
exe中的例子,当然在dll中如果导入其他dll中的函数,结果也是一样的。这样就有一个问题,代码里call的地址是一个模块内的地址,而且是一个
VA,那么如果模块基地址发生了变化,这个地址岂不是就无效了?这个问题如何解决?
答案是:Windows使用重定位机制保证以上代码无论模块加载到哪个基址都能正确被调用。听起来很神奇,是怎么做到的呢?其实原理并不很复杂,这个过程分三步:
1.编译的时候由编译器识别出哪些项使用了模块内的直接VA,比如push一个全局变量、函数地址,这些指令的操作数在模块加载的时候就需要被重定位。
2.链接器生成PE文件的时候将编译器识别的重定位的项纪录在一张表里,这张表就是重定位表,保存在DataDirectory中,序号是 IMAGE_DIRECTORY_ENTRY_BASERELOC。
3.PE文件加载时,PE 加载器分析重定位表,将其中每一项按照现在的模块基址进行重定位。
以上三步,前两部涉及到了编译和链接的知识,跟本文的关系不大,我们直接看第三步,这一步符合本系列的特征。
在查看重定位表的定义前,我们先了解一下他的存储方式,有助于后面的理解。按照常规思路,每个重定位项应该是一个DWORD,里面保存需要重定位的RVA,这样只需要简单操作便能找到需要重定位的项。然而,Windows并没有这样设计,
原因是这样存放太占用空间了,试想一下,加入一个文件有n个重定位项,那么就需要占用4*n个字节。所以Windows采用了分组的方式,按照重定位项所在的页面分组,每组保存一个页面起始地址的RVA,页内的每项重定位项使用一个WORD保存重定位项在页内的偏移,这样就大大缩小了重定位表的大小。
有了上面的概念,我们现在可以来看一下基址重定位表的定义了:
- typedef struct _IMAGE_BASE_RELOCATION {
- DWORD VirtualAddress;
- DWORD SizeOfBlock;
- // WORD TypeOffset[1];
- } IMAGE_BASE_RELOCATION;
- typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
VirtualAddress:页起始地址RVA。
SizeOfBlock:表示该分组保存了几项重定位项。
TypeOffset:这个域有两个含义,大家都知道,页内偏移用12位就可以表示,剩下的高4位用来表示重定位的类型。而事实上,Windows只用了一种类型IMAGE_REL_BASED_HIGHLOW,数值是3。
好了,有了以上知识,相信大家可以很容易的写出自己修正重定位表的代码,不如自己做个练习验证一下吧。
本文 by evil.eagle 转载的时候请注明出处。http://blog.csdn.net/evileagle/article/details/12886949
最后,还是总结一下,哪些项目需要被重定位呢?
1.代码中使用全局变量的指令,因为全局变量一定是模块内的地址,而且使用全局变量的语句在编译后会产生一条引用全局变量基地址的指令。
2.将模块函数指针赋值给变量或作为参数传递,因为赋值或传递参数是会产生mov和push指令,这些指令需要直接地址。
3.C++中的构造函数和析构函数赋值虚函数表指针,虚函数表中的每一项本身就是重定位项,为什么呢?大家自己考虑一下吧,不难哦~