要链接PE文件中的重定位表的话,我们首先要知道哪些指令需要重定位,还有重定位的算法是怎样的。汇编中有些指令要用到内存地址,并且在编译的时候这些地址就固定在机器码里面了,如果我们程序的实际装入地址与模块的减一装入地址是相同的,那么这个指令就没问题,但是当实际装入地址与建议装入地址不相同时,如果没有重定位就会出问题了。
举个例子,假如我们在程序中写的代码是: mov eax,dword ptr [00400ffc] 。那么这条指令的机器码就是 ALFC0F4000 ,可以看到,地址已经是嵌入到机器码中了,如果程序的起始位置是建议装入地址00400000h,那么指令没有问题,但如果程序没有开始于建议装入地址,比如说程序开始地址在00500000h,那么正确的机器码应该是ALFC0F5000,而这个时候的机器码其实还是原来的ALFC0F4000,这样的话,程序运行就会出错了。
所以我们需要重定位,重定位的算法大致可以描述为:将直接寻址指令中的双字地址加上模块实际装入地址与模块建议装入地址之差。
那么,为了进行这个运算,我们需要三个数据:需要修正的机器码的地址,模块的建议装入地址,模块的实际装入地址。
知道这些后应该还有一个问题,重定位表中装的到底是什么呢?根据上面重定位需要的三个数据,模块的建议装入地址我们知道(在PE文件头的IMAGE_OPTIONAL_HEADER32结构中),模块的实际装入地址在程序装入后我们自然也可以知道,所以唯一少的就是需要修正的机器码的地址。
重定位表的结构定义如下:
IMAGE_BASE_RELOCATION STRUCT
VirtualAddress dd ? ;重定位内存页的起始RVA
SizeOfBlock dd ? ;重定位块的长度
IMAGE_BASE_RELOCATION ENDS
那么这里有个问题,重定位不是应该装的都是需要修正的机器码的地址吗,为什么还有这种结构?其实想一下,正常情况下,一个地址要用到32个比特也就是4个字节,而一个程序中往往有很多的地址需要重定位,所以全都这样来的话会占用很多内存空间,所以为了优化,重定位表也采用了类似偏移的这种方法。就是因为在比较靠近的重定位表项中,32位的地址的高位总是相同,所以我们可以把这个高位相同的重定位项放一起,然后在同一个块中只存储他们的低位,这样就可以节省很多空间了。
那么按照优化后的方法到底是怎么存储的呢?首先我们知道一个内存页是4KB,也就是4096个字节,也就是2的12次方,所以当我们用一个块表示一个内存页的需要重定位的地址时,我们只需要那个内存页的起始RVA,然后一个重定位项只需要12个比特就能寻址整个内存页。但是一般我们需要对齐,所以一个重定位项用16比特来表示。
这样的话,重定位表就变成了首先一个IMAGE_BASE_RELOCATION头,然后后面跟着一堆重定位项,然后后面再紧接着另一个内存页的重定位块,依次这样下去。一个重定位块描述一个内存页。上述结构中第二个字段SizeOfBlock表示的重定位块的长度是包括了这个头的,所以我们计算重定位项数时应该应该用这个字段的数据减8再除以2。
另外,上面说了重定位项虽然12位就够寻址一个内存页,但是我们为了对齐就扩充成了16位,其实那高4位也没有浪费。高四位被用来描述重定位的种类了。重定位项高四位的含义:
高4位的值 | 在 Windows.inc中出现的预定义值 | 含 义 |
0 | IMAGE_REL_BASE_ABSOLUTE | 这个重定位项无意义,只是作为对齐用 |
1 | IMAGE_REL_BASE_HIGH | 重定位地址指向的双字中,仅仅高16位需要被修正 |
2 | IMAGE_REL_BASE_LOW | 重定位地址指向的双字中,仅仅低16位需要被修正 |
3 | IMAGE_REL_BASE_HIGHLOW | 重定位地址指向的双字中32位都需要被修正,这是修正算法中举例的情况 |
4 | IMAGE_REL_BASE_HIGHADJ | 需要32位,高16位位于偏移量,低16位位于下一个偏移量数组元素,组合为一个带符号数,加上32位的一个数,然后加上8000然后把高16位保存在偏移量的16位域内 |
5 | IMAGE_REL_BASE_MIPS_JMPADDR | |
6 | IMAGE_REL_BASE_SECTION | |
7 | IMAGE_REL_BASE_REL32 |
虽然上面定义了这么多含义,但是一般在PE文件中只能看到0和3这两种情况。所有重定位块都以一个VirtualAddress字段为0的IMAGE_BASE_RELOCATION结构结束,所以PE文件的代码总是从装入地址的1000h 处开始的,如果代码直接从装入地址开始,那么这个内存页的RVA就是0,那么重定位表中描述这个内存页的IMAGE_BASE_RELOCATION结构中的VirtualAddress字段就为0了,这样会把第一个重定位块就当成结尾了。
现在我们可以根据这个写一个程序来显示PE文件中重定位表的信息,资源文件和框架用的和之前PE文件头博客中的是一样的。功能函数代码如下:
.const
szMsg db '文件名: %s',0dh,0ah
db '-------------------------------------------------------',0dh,0ah
db '重定位表所处的节: %s',0dh,0ah,0
szMsgRelocBlk db 0dh,0ah
db '-------------------------------------------------------',0dh,0ah
db '重定位基地址: %08X',0dh,0ah
db '重定位项的数量: %d',0dh,0ah
db '-------------------------------------------------------',0dh,0ah
db '需要重定位的地址列表',0dh,0ah
db '-------------------------------------------------------',0dh,0ah,0
szAll db '总共重定位的项: %d',0dh,0ah,0
szMsgReloc db '%08X ',0
szCrlf db 0dh,0ah,0
szErrNoReloc db '这个文件中不包括重定位信息!',0
.code
include _RvaToFileOffset.asm
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile proc _lpFile,_lpPeHead,_dwSize
local @szBuffer[1024]:byte,@szSectionName[16]:byte,@szBuall[256]:byte,@dwAll
pushad
mov esi,_lpPeHead
assume esi:ptr IMAGE_NT_HEADERS ;PE文件头
;**************************************************************************************************************
;根据IMAGE_DIRECTORY_ENTRY_BASERELOC目录表找到重定位表的位置
;**************************************************************************************************************
mov eax,[esi].OptionalHeader.DataDirectory[8*5].VirtualAddress ;获取重定位表的位置
.if ! eax
invoke MessageBox,hWinMain,addr szErrNoReloc,NULL,MB_OK ;如果没有重定位表则显示提示信息
jmp _Ret
.endif
push eax
invoke _RVAToOffset,_lpFile,eax
add eax,_lpFile ;获取重定位表的文件地址
mov esi,eax
pop eax
invoke _GetRVASection,_lpFile,eax ;获取重定位表所在的节的名称
invoke wsprintf,addr @szBuffer,addr szMsg,addr szFileName,eax
invoke SetWindowText,hWinEdit,addr @szBuffer
assume esi:ptr IMAGE_BASE_RELOCATION
mov @dwAll,0
;*******************************************************************************************************************
;循环处理每个重定位块
;*******************************************************************************************************************
.while [esi].VirtualAddress
cld
lodsd ;mov eax,[esi] esi+4
mov ebx,eax ;取得重定位块指向的页的RVA
lodsd
sub eax,sizeof IMAGE_BASE_RELOCATION ;用重定位块的长度减去头的长度得到重定位项的长度
shr eax,1 ;因为重定位项是两个字节为单位的,所以除以二得到重定位项的个数
add @dwAll,eax
push eax
invoke wsprintf,addr @szBuffer,addr szMsgRelocBlk,ebx,eax
invoke _AppendInfo,addr @szBuffer
pop ecx
xor edi,edi
.repeat
push ecx
lodsw ;mov eax,word ptr [esi] esi+2 重定位项
mov cx,ax
and cx,0f000h ;将低12位清0,只保留用来描述重定位项种类的高4位
;**********************************************************************************************************************
;仅处理IMAGE_REL_BASED_HIGHLOW类型的重定位项(即重定位指向的双字都需要修改)
;**********************************************************************************************************************
.if cx == 03000h
and ax,0fffh ;将重定位项的高四位都清0,只留下真正的重定位信息
movzx eax,ax
add eax,ebx ;将重定位项里面的相对于该重定位块指向的页的起始位置的偏移加上该重定位块指向的页的起始位置的RVA得到重定位位置
.else
mov eax,-1
.endif
invoke wsprintf,addr @szBuffer,addr szMsgReloc,eax
inc edi
.if edi == 4 ;每显示四个项目就换行
invoke lstrcat,addr @szBuffer,addr szCrlf
xor edi,edi
.endif
invoke _AppendInfo,addr @szBuffer
pop ecx
.untilcxz
.if edi ;如果之前没有换行,这里换行
invoke _AppendInfo,addr szCrlf
.endif
.endw
invoke wsprintf,addr @szBuall,offset szAll,@dwAll
invoke _AppendInfo,addr @szBuall
_Ret:
assume esi:nothing
popad
ret
_ProcessPeFile endp
程序运行: