PE重定位表
PE文件基址重定位(Base Relocation),程序编译时每个模块有一个优先加载地址ImageBase,这个值是连接器给出的,因此连接器生成的指令中的地址是在假设模块被加载到ImageBase前提之下生成的,那么一旦程序没有将模块加载到ImageBase时,那么程序中 的指令地址就需要重新定位,例如:假设一个可执行文件,基址是0x400000,在这个image偏移0x1234处是一个指针,指向一个字符串,字符串始于实际地址0x404002处,所以指针应该是0x404002,加载文件时,由于种种原因,加载器决定把他加载到0x600000处,连接器假设的地址和实际的地址之差成为delta,上例delta为0x200000,整个位置提高了0x200000,那么字符串位置应该为0x604002,原来指向字符串的指针就错误了,所以要把delta加到指针值中,为了让加载器有这样的能力做调整,可执行文件内含许多个【基址重定位项】,给那些存放指针的位置使用,加载器必须把delta加载到各个基址上。本例中应该把0x200000加给原来的指针值,0x404002,并将0x604002写回原处。
下面我们来介绍一下【基址重定位项】,加载器就是利用它来知道模块是否按预期的位置加载,哪些指令是需要修改的。因此我们研究的重点将是【基址重定位项】,首先加载器也是通过数据目录来定位【基址重定位项】,【基址重定位项】被包装为一系列连续区段,每一个区段来描述一个4K PAGE(也就是一页)的重定位信息,长短不一(每页中需要重定位的指令数目也不一样),它们以一个IMAGE_BASE_RELOCATION结构作为开始,格式如下:
DWORD VirtualAddress重定位内存页的起始RVA,每一个【基址重定位项】的偏移位置(即下面的TypeOffset的低12位,它是指令相对于它所在页的第一条指令的偏移),必须加上重定位页的RVA才是一个真正的RVA,指向【基址重定位项】
DWORD SizeOfBlock:结构大小,在加上跟着后面的所有【基址重定位项】(都是WORDS),为了决定【基址重定位项】的个数,=(SizeOfBlock-Sizeof(IMAGE_BASE_RELOCATION)(8个字节))/2(WORD占2个字节);例如此值为44,则个数为44-8/2=18.
总结:IMAGE_BASE_RELOCATION包含两个成员,一个是VirtualAddress,包含自身内存页起始位置RVA,另一个SizeOfBlock,表明这个结构有多大。
WORD TypeOfOffset
这并不是单独一个WORD,而是一个WORDS组,数组元素个数可以有上一个式子计算得到,每一个WORD的最底部12位代表【基址重定位项】的位置偏移,但必须在加上IMAGE_BASE_RELOCATION表头中VirtualAddress,最高4位是【基址重定位项】的型态。对于在Intel CPU中的PE文件,你将看到两种状态,0(IMAGE_REL_BASED_ABSOLUTE)此一【基址重定位数据项】无意义,只是用来充数而已,使所有【基址重定位项】总数为DWORD倍数。3(IMAGE_REL_BASED_HIGNLOW):把delta值加到欲计算的RVA值,另外还有其他状态在WINNT.h中,它们大部分是给i386以外的的CPU使用。
下面给出一些【基址重定位项】,请注意其中的RVA已经被IMAGE_BASE_RELOCATION中的VirtualAddress校正过。
VirtualAddress:00001000 Size0000012C
000001032 HIGHLOW
00000106D HIGHLOW
0000010AF HIGNLOW
......
VirtualAddress:00002000 Size0000009C
000020A6 HIGHLOW
00002110 HIGHLOW
00002136 HIGHLOW
00002156 HIGHLOW
........
VirtualAddress:000003000 Size00000114
0000300A HIGHLOW
0000301E HIGHLOW
0000303B HIGHLOW
0000306A HIGHLOW
通过上面的例子我们可以看到相邻区段正好相差0X1000,也就是4K,证实了上面所说的“每一个区段描述image一个4K Page(也就是一页)的重定位信息”同时我们可以看到第一个区段的VirtualAddress是00001000,正好是.text起始RVA。
现在问题基本明白了,一旦模块没有价加载到预期地址,编译器就会根据【基址重定位项】去修正哪些需要修正的指令,这样程序就可以正常执行了。