一、什么是重定位?
重定位就是你本来这个程序理论上要占据这个地址,但是由于某种原因,这个地址现在不能让你霸占,你必须转移到别的地址,这就需要基址重定位。
二、为什么需要重定位?
这个和上面的问题的解释是一样的。不是说过每个进程都有自己独立的虚拟地址空间吗?既然都是自己的,怎么会被占据呢?对于EXE应用程序来说,是这样的。但是动态链接库就不一样了,我们说过动态链接库都是寄居在别的应用程序的空间的,所以出现要载入的基地址被应用程序占据了也是很正常的,这时它就不得不进行重定位了。
三、重定位表的结构
1.重定位所需的数据
在开始分析重定位表的结构之前需要了解两个问题:第一,对一条指令进行重定位需要哪些信息?第二,这些信息中哪些应该被保存在重定位表中?
下面举例来说明这两个问题,请看下面的这段代码:
:00400FFC 0000 ;dwVar变量
:00401000 55 push ebp
:00401001 8BEC mov ebp, esp
:0040100383C4FC add esp, FFFFFFFC
:00401006 A1FC0F4000 mov eax, dword ptr [00400FFC] ;mov eax,dwVar
:0040100B 8B45FC mov eax, dword ptr [ebp-04] ;mov eax,@dwLocal
:0040100E 8B4508 mov eax, dword ptr [ebp+08] ;mov eax,_dwParam
:00401011 C9 leave
:00401012 C20400 ret 0004
:00401015 68D2040000 push 000004D2
:0040101A E8E1FFFFFF call 00401000 ;invoke Proc1,1234
其中地址为00401006h处的mov eax,dword ptr [00400ffc]就是一句需要重定位的指令,当整个程序的起始地址位于00400000h处的时候,这句代码是正确的,假如将它移到00500000h处的时候,这句指令必须变成mov eax,dword ptr [00500ffc]才是正确的。这就意味着它需要重定位。
让我们看看需要改变的是什么,重定位前的指令机器码是A1 FC0F 40 00,而重定位后将是A1 FC0F 50 00,也就是说00401007h开始的双字00400ffch变成了00500ffch,改变的正是起始地址的差值(00500000h-00400000h)=00100000h。
所以,重定位的算法可以描述为:将直接寻址指令中的双字地址加上模块实际装入地址与模块建议装入地址之差。为了进行这个运算,需要有3个数据,
首先是需要修正的机器码地址;
其次是模块的建议装入地址;
最后是模块的实际装入地址。这就是第一个问题的答案。
在这3个数据中,模块的建议装入地址已经在PE文件头中定义了,而模块的实际装入地址是Windows装载器确定的,到装载文件的时候自然会知道,所以第二个问题的答案很简单,那就是应该被保存在重定位表中的仅仅是需要修正的代码的地址。
事实上正是如此,PE文件的重定位表中保存的就是一大堆需要修正的代码的地址。
2.重定位表的位置
重定位表一般会被单独存放在一个可丢弃的以“.reloc”命名的节中,但是和资源一样,这并不是必然的,因为重定位表放在其他节中也是合法的,惟一可以肯定的是,如果重定位表存在的话,它的地址肯定可以在PE文件头中的数据目录中找到。
3.重定位表的结构
虽然重定位表中的有用数据是那些需要重定位机器码的地址指针,但为了节省空间,PE文件对存放的方式做了一些优化。
在正常的情况下,每个32位的指针占用4个字节,如果有n个重定位项,那么重定位表的总大小是4×n字节大小。
直接寻址指令在程序中还是比较多的,在比较靠近的重定位表项中,32位指针的高位地址总是相同的,如果把这些相近表项的高位地址统一表示,那么就可以省略一部分的空间,当按照一个内存页来分割时,在一个页面中寻址需要的指针位数是12位(一页等于4096字节,等于2的12次方),假如将这12位凑齐16位放入一个字类型的数据中,并用一个附加的双字来表示页的起始指针,另一个双字来表示本页中重定位项数的话,那么占用的总空间会是4+4+2×n字节大小,计算一下就可以发现,当某个内存页中的重定位项多于4项的时候,后一种方法的占用空间就会比前面的方法要小。
PE文件中重定位表的组织方法就是采用类似的按页分割的方法,从PE文件头的数据目录中得到重定位表的地址后,这个地址指向的就是顺序排列在一起的很多重定位块,每一块用来描述一个内存页中的所有重定位项。
每个重定位块以一个IMAGE_BASE_RELOCATION结构开头,后面跟着在本页面中使用的所有重定位项,每个重定位项占用16位的地址(也就是一个word),结构的定义是这样的:
IMAGE_BASE_RELOCATION STRUCT
VirtualAddress dd ? ;重定位内存页的起始RVA
SizeOfBlock dd ? ;重定位块的长度
IMAGE_BASE_RELOCATION ENDS
VirtualAddress字段是当前页面起始地址的RVA值,本块中所有重定位项中的12位地址加上这个起始地址后就得到了真正的RVA值。SizeOfBlock字段定义的是当前重定位块的大小,从这个字段的值可以算出块中重定位项的数量,由于SizeOfBlock=4+4+2×n,也就是sizeof IMAGE_BASE_RELOCATION+2×n,所以重定位项的数量n就等于(SizeOfBlock-sizeof IMAGE_BASE_RELOCATION)÷2。
IMAGE_BASE_RELOCATION结构后面跟着的n个字就是重定位项,每个重定位项的16位数据位中的低12位就是需要重定位的数据在页面中的地址,剩下的高4位也没有被浪费,它们被用来描述当前重定位项的种类。虽然高4位定义了多种重定位项的属性,但实际上在PE文件中只能看到0和3这两种情况。
所有的重定位块最终以一个VirtualAddress字段为0的IMAGE_BASE_RELOCATION结构作为结束,读者现在一定明白了为什么可执行文件的代码总是从装入地址的1000h处开始定义的了(比如装入00400000h处的.exe文件的代码总是从00401000h开始,而装入10000000h处的.dll文件的代码总是从10001000h处开始),要是代码从装入地址处开始定义,那么第一页代码的重定位块的VirtualAddress字段就会是0,这就和重定位块的结束方式冲突了。
四、实例分析
我们来分析一个实例,简单分析一个DLL的重定位表,先反汇编:
根据上面的理论讲解,我们需要重定位的有两处:00402000和00403030
下面我们就来实例分析一下,是不是?首先找到重定位表的指针:
如果还不知道重定位表的RVA是怎么找的,前参照我前面加的内容。
从图中可以看出数据目录表指向重定位表的指针是5000h,换算成文件偏移地址就是0E00h,(也不要问我怎么来的,我前面已经说明三个步骤)我们在定位到File Offset为0E00处,可以得到IMAGE_BASE_RELOCATION结构如下图所示:
从图中可以看出:
VirtualAddress:00001000h
SizeOfBlock:00000010h(有四个重定位数据,(10h-8h)/2h=4h)
重定位数据1:300Fh
重定位数据2:3023h
重定位数据3:0000h(用于对齐)
重定位数据4:0000h(用于对齐)
重定位数据计算过程如下表所示:
用十六进制工具查看实例文件,其中060Fh和623h分别指向402000h和403030h,如下图所示:
和我们上面假设的完全一样!
执行PE文件前,加载程序在进行重定位的时候,会将PE文件在内存中的实际映像地址减去PE文件所要求的映像地址,得到一个差值,再将这一差值根据重定位类型的不同添加到地址数组中。