0x01 重定位表结构
重定位表是由数据目录表中的第六个成员指出的:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
程序编译时每个模块有一个优先加载地址ImageBase,这个值是链接器给出的,因此链接器生成的指令中的地址是在假设模块被加载到ImageBase前提之下生成的,那么一旦程序没有将模块加载到ImageBase时,那么程序中的指令地址就需要重新定位,从而需要重定位表的修正。
对于EXE应用程序来说,在自己独立的4G进程空间中,它是肯定加载到ImageBase默认基地址上的。但是动态链接库就不一样了,动态链接库都是映射到别的应用程序的空间中的,所以出现要载入的基地址被应用程序占据了也是很正常的,这时它就必须进行重定位了。
重定位的算法功能可以描述为:将直接寻址指令中的地址加上模块实际装入地址与模块建议(默认)装入地址之差。
参与运算的是这3个数据:需要修正的机器码地址, 模块的实际装入地址,模块的建议(默认)装入地址ImageBase.而模块的建议(默认)装入地址已经ImageBase中定义了,而模块的实际装入地址是Windows加载器确定的, 因此,PE文件的重定位表中唯一保存的就是一组需要修正的代码的地址。
(位置)重定位表一般会被单独存放在一个以“.reloc”命名的节中,但这并不是必然的,因为重定位表放在其他节中也是合法的,但是如果重定位表存在的话,它的地址肯定可以在数据目录中找到。
为了节省空间,重定位表对需要重定位的指令的地址的存放方式做了一些优化。
直观上看,x86下每个32位的指针占用4个字节,如果有n个重定位项,那么重定位表的总大小就是4×n字节大小。
直接寻址指令在程序中还是比较多的,在相临近的各个重定位表项中,32位指针的高位地址总是相同的,如果将这些相近表项的高位地址只用少数几位来统一表示,那么就可以节省一部分的空间。
当按照一个内存页来分割时,在一个页面中寻址需要的指针位数是12位(一页4096字节,2的12次方),将这12位按WORD对齐为16位,再用一个附加的DWORD来表示页的起始指针,另一个DWORD来表示本页中的重定项数,那么占用的总空间就是4+4+2×n字节大小,当某个内存页中的重定位项多于4项的时候,后一种方法的占用空间就会比前面的方法要小。0
PE文件中重定位表的组织方法就是采用类似的按页分割的方法,从PE文件头的数据目录中得到重定位表的地址后,这个地址指向的就是顺序排列在一起的很多重定位块,每一块用来描述一个内存页中的所有重定位项。
重定位表由一个个的重定位块组成,如果重定位表存在的话,必定是至少有一个重定位块。因为每个块只负责定位0x1000大小范围内的数据,因此如果要定位的数据范围比较大的话,就会有多个重定位块存在。
每个块的首部IMAGE_BASE_RELOCATION定义如下:
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; //重定位内存页的起始RVA,本块中所有重定位项中的12位地址加上这个起始地址后就得到了真正的RVA值
DWORD SizeOfBlock; //重定位块的大小,从这个字段的值可以算出块中重定位项的数量
} IMAGE_BASE_RELOCATION
由于SizeOfBlock=4+4+2×n,也就是sizeof IMAGE_BASE_RELOCATION+2×n,所以重定位项的数量就等于(SizeOfBlock-sizeof IMAGE_BASE_RELOCATION)÷2。
MAGE_BASE_RELOCATION结构后面跟着的n个WORD就是重定位项,每个重定位项的16位数据位中的低12位就是需要重定位的数据在页面中的地址,剩下的高4位用来描述当前重定位项的种类。
所有的重定位块最终以一个VirtualAddress字段为0的IMAGE_BASE_RELOCATION结构作为结束,因此可执行文件的代码总是从装入地址的1000h处开始定义的了(比如装入00400000h处的.exe文件的代码总是从00401000h开始,而装入10000000h处的.dll文件的代码总是从10001000h处开始),要是代码从装入地址处开始定义,那么第一页代码的重定位块的VirtualAddress字段就会是0,这就和重定位块的结束方式冲突了。