静态链接---都说重定位必须保证链接地址与运行地址相同,真的吗?
讨论问题:
- 重定位发生在哪个阶段。
- 实现重定位是否一定是位置相关代码
- 重定位是否一定要保证链接地址与运行地址一致,否则无法正常执行
3.3 静态链接–指令修正
上一小节回答了重定位发生在链接阶段,本小节来进一步的分析重定位的过程。程序的链接也有静态静态链接和动态链接,我们结合静态链接继续分析前面静态重定位的例子。
/* 代码清单1 */
/* 反汇编程序: arm-none-linux-gnueabi-objdump -d main.o*/
00000000 :
0: e92d4800 push {fp, lr}
4: e28db004 add fp, sp, #4
8: ebfffffe bl 0
c: ebfffffe bl 0
10: e3a03000 mov r3, #0
14: e1a00003 mov r0, r3
18: e8bd8800 pop {fp, pc}
在未进行链接前,0x00000008和0x0000000c地址处的指令是‘bl 0’指令。为什么是bl指令呢,bl指令后的地址怎么确定,这就不得不来看main.o二进制目标文件的重定位表:
/* 重定位表: arm-none-linux-gnueabi-objdump -r main.o*/
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000008 R_ARM_CALL hello
0000000c R_ARM_CALL world
在它的重定位表中注明了这两个重定位的类型是R_ARM_CALL,这个类型的作用是什么,我们通过ARM架构elf文件中的说明来分析:
这是一种静态重定位
指令类型是ARM指令
它的指令修正操作是:((S + A) | T) – P
对应的重定位指令是BL/BLX 。
这也就回答了上述的第一个问题为什么是bl指令,而回答第二个问题就得来进一步的了解指令修正操作,指令修正操作中的几个字母的解释如下表:
在链接器对main.o和test.o文件完成链接后,hello、world这两个符号在程序中的位置也就确定了,我们来查看它们的地址。根据代码清单2,hello、world的地址分别为0x00008340和0x00008354,也就是S的值。
/* 代码清单2 */
/* arm-none-linux-gnueabi-readelf -s main */
Symbol table '.symtab' contains 100 entries:
Num: Value Size Type Bind Vis Ndx Name
80: 00008354 20 FUNC GLOBAL DEFAULT 12 world
85: 00008340 20 FUNC GLOBAL DEFAULT 12 hello
接着分析A的值,代码清单1中的反汇编程序的bl伪指令对应的指令码为ebfffffe,bl伪指令的结构如下图所示:
通过低24为([23:0])的值来确定跳转目标的地址。而A就是根据将低24左移2位,再将左移后的结果扩展到32位(符号位/高位扩展为1),得到补齐量A,具体计算如下:
0xfffffe:是指代码清单1中,重定入口地址中的指令的后24位
A = sign_extend(imm24 << 2) = sign_extend(0xfffffe << 2 )
= sign_extend (0xfffff8)
= 0xfffffff8
= -0x08
最后,我们需要来确定P值,通过反汇编链接后生成的可执行文件来确定原重定位入口的指令的位置:
/* 代码清单3 */
/* hello() */
00008340 :
8340: e52db004 push {fp}
/* world() */
00008354:
8354: e52db004 push {fp}
/* main() */
00008368:
8368: e92d4800 push {fp, lr}
836c: e28db004 add fp, sp, #4
8370: ebfffff2 bl 8340
8374: ebfffff6 bl 8354
根据代码清单3,这两个需要进行指令修正的位置分别是0x00008370和0x00008374,故P的值也确定了。
那接下来就可以确定bl指令低24位的值,也就是指令修正操作:
0x00008370位置指令修正 :
( ( (S+A) | T ) - P
= ((0x00008340 - 0x08) - 0x00008370) >> 2
= 0xfffffff2,故imm24[23:0]
= fffff2
0x00008370位置指令修正后的指令为:ebfffff2
0x00008374位置指令修正:
( ( (S+A) | T ) - P
= ((0x00008340 - 0x08) - 0x00008374) >> 2
= 0xfffffff2,故imm24[23:0]
= fffff6
0x00008374位置指令修正后的指令为:ebfffff6
我们通过修正后的指令就可以逆推出目标符号所在的位置。仔细看这个指令修正操作,是在做减法运算,而且还是目标符号地址和需要重定位入口地址之间的减法运算,减法运算的结果是得到一个距离,通常称之为偏移量。
也就是说R_ARM_CALL类型的指令修正的目的就是为了算出这个偏移量。在这个偏移量确定后,无论程序被加载到内存的哪个位置,都可以根据偏移量和当前指令的位置来确定目标符号所在的地址,从而实现跳转到目标符号处执行指令。
3.4 为什么有人说:重定位需要确保链接地址与运行地址一致
可能你也许会想,那么麻烦干嘛,既然知道了目标符号的地址,直接设置CPU下个指令的地址为目标符号的地址不就行了。这种重定位类型的也有,同时也会带来一个问题:
假设现在设置CPU下条执行指令是位于hello符号的地址0x00008340,但是当我们将程序加载到内存其它位置(只要hello的地址不为0x00008340)。此时,你会发现,不知道执行什么指令去了。只有当程序加载到内存的位置满足hello的地址为0x00008340时,程序才能正常工作。
为了分析这个问题,我们再来写个程序。代码清单4,通过修改pc寄存器中的值,来告诉CPU要执行的指令位于hello地址的内存上:
/* 代码清单4 */
/* 原程序 :start.S*/
.global _start
_start:
……
//set stack
ldr sp, =0x02050000
ldr pc, =hello
查看重定位表发现重定位类型变成了 R_ARM_ABS32 :
/* 重定位表 */
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000040 R_ARM_ABS32 hello
而它对应的指令修正操作为(S + A) | T。地址0x00000040是重定位的入口,在这里存放的是hello符号的地址,目前未知,因此设置为0:
/* 静态链接前:start.o 反汇编程序 */
00000000 <_start>:
30: e59fd004 ldr sp, [pc, #4]
34: e59ff004 ldr pc, [pc, #4]
38: 10060000 .word 0x10060000
3c: 02050000 .word 0x02050000
40: 00000000 .word 0x00000000
注:ARMv7-A中规定,执行ARM指令时,pc寄存中的值 = 当前指令的地址 + 8
在静态链接后,hello的链接地址已确定,通过指令修正,将此处保存的数据修改为hello的链接地址(代码清单5):
/* 代码清单5 */
/* 静态链接后:反汇编程序 */
00000000 <_start>:
34: e59ff004 ldr pc, [pc, #4]
38: 10060000 .word 0x10060000
3c: 02050000 .word 0x02050000
40: 00000044 .word 0x00000044
00000044:
44: e52db004 push {fp}
在程序运行时,ldr伪指令通过取出 该地址(0x00000040) 上保存的hello符号地址,修改pc寄存器的值,使得CPU能够去执行hello地址的指令。
也是因为在重定位入口处存放的是一个地址,也就是数据。这就造成了在文件链接后,这个地址也就确定下来,无法在加载时自动的进行变更。
现在我们已经可以回答上面提到问题了,为什么在这种情况下必须保证运行地址和链接地址一致,程序才能正常运行。