1.测试源代码
//foo1.c:
int foo1 = 10;
void foo1_func()
{
int ret = foo1;
}
//foo2.c:
int foo2 = 20;
void foo2_func(int x)
{
int ret = foo2;
}
//hello.c:
#include <stdio.h>
extern int foo2;
int main(int argc, char *argv[])
{
foo2 = 5;
foo2_func(50);
return 0;
}
2.链接时,在第一阶段完成后,目标文件已经合并完成,并且已经为符号分配了运行时地址,链接器将进行符号重定位。
3.查看重定位的符号
模块hello.o中有两处需要重定位,一处是偏移0xb处的变量foo2,另外一处是偏移0x1b处的函数foo2_func。汇编器已经将这两处需要重定位的符号记录在了重定位表中。
root@baisheng:~/demo# readelf -r hello.o
Relocation section '.rel.text' at offset 0x3c8 contains 2 entries:
Offset Info Type Sym.Value Sym. Name
0000000b 00000901 R_386_32 00000000 foo2
0000001b 00000a02 R_386_PC32 00000000 foo2_func
...
符号foo2的重定位类型是R_386_32,ELF标准规定的计算修订值的公式是:
S + A
其中,S表示符号的运行时地址(绝对地址),A就是汇编器填充在引用外部符号处的Addend。
符号foo2_func的重定位类型是R_386_PC32,ELF标准规定的计算修订值的公式是:
S + A - P
其中S、A的意义与前面完全相同,P为修订处的运行时地址或者偏移。对于目标文件,P为修订处在段内的偏移。对于可执行文件和动态库,P为修订处的运行时地址。
4.查看符号绝对地址
首先我们先来确定S。运行时地址在链接时才分配,因此,变量foo2和函数foo2_func的运行时地址在链接后的可执行文件hello的符号表中:
root@baisheng:~/demo# readelf -s hello | grep foo2
38: 00000000 0 FILE LOCAL DEFAULT ABS foo2.c
53: 0804a020 4 OBJECT GLOBAL DEFAULT 24 foo2
68: 08048414 16 FUNC GLOBAL DEFAULT 13 foo2_func
可见,符号foo2的运行时地址为0x0804a020(即符号的绝对地址),符号foo2_func的运行时地址是0x08048414(即符号的绝对地址)。
5.查看符号的临时填充地址,即Addend
接下来,我们再来看看汇编器为这两个符号填充的Addend是多少。我们使用工具objdump反汇编hello.o,其中黑体标识的分别是汇编器在引用foo2和foo2_func的地址处填充的Addend:
root@baisheng:~/demo# objdump -d hello.o
hello.o: file format elf32-i386
Disassembly of section .text:
00000000 <main>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 e4 f0 and $0xfffffff0,%esp
6: 83 ec 10 sub $0x10,%esp
9: c7 05 00 00 00 00 05 movl $0x5,0x0
10: 00 00 00
13: c7 04 24 32 00 00 00 movl $0x32,(%esp)
1a: e8 fc ff ff ff call 1b <main+0x1b>
1f: b8 00 00 00 00 mov $0x0,%eax
24: c9 leave
25: c3 ret
根据输出可见,汇编器在引用符号foo2处(偏移地址0x0b处)填充的Addend是四个00,在引用符号foo2_func处填充的Addend是fc ff ff ff,即–4。
于是,可执行文件hello中引用符号foo2的位置的修订值为:
S + A = 0x0804a020 + 0 = 0x0804a020
6.模块加载重定位的理解
所以在模块加载过程中重定位时:rel.r_offset在这里对应着0x0b,rel.r_info对应着foo2符号的索引值。即在这个节区0x0b处会引用符号foo2,所以这里要填充该符号的地址,但是在未链接时并不知道该符号的绝对地址,所以这里先是填充该符号的临时地址00 00 00 00 替代,到这里真正链接时再进行重定位,此时会得到符号的真正运行地址0x0804a020(sym->value),把符号真正的运行地址0x0804a020(sym->value)填写到偏移0x0b处,即替代先前的临时地址0x00000000,这样在该位置处就能正确引用到该符号了。
7.查看结果
我们反汇编可执行文件hello,来验证一下引用符号foo2处的值是否修订为我们计算的这个值:
root@baisheng:~/demo# objdump -d hello
hello: file format elf32-i386
...
080483dc <main>:
80483dc: 55 push %ebp
80483dd: 89 e5 mov %esp,%ebp
80483df: 83 e4 f0 and $0xfffffff0,%esp
80483e2: 83 ec 10 sub $0x10,%esp
80483e5: c7 05 20 a0 04 08 05 movl $0x5,0x804a020
80483ec: 00 00 00
80483ef: c7 04 24 32 00 00 00 movl $0x32,(%esp)
80483f6: e8 19 00 00 00 call 8048414
<foo2_func>
80483fb: b8 00 00 00 00 mov $0x0,%eax
8048400: c9 leave
8048401: c3 ret
8048402: 66 90 xchg %ax,%ax
...
注意偏移0x1b处,确实已经被链接器修订为0x0804a020了。
8.相对地址重定位
对于符号foo2_func的修订值,还需要变量P,即引用符号foo2_func处的运行时地址。根据可执行文件hello的反汇编代码可见,引用符号foo2_func的指令的地址是:
0x80483f6 + 1 = 0x80483f7
所以,可执行文件hello中引用符号foo2_func的位置的修订值为:
S + A – P = 0x08048414 + (-4) - 0x80483f7 = 0x19
观察hello的反汇编代码,从地址0x80483f7开始处的4字节,确实也已经被链接器修订为0x19。
9.结语
这里提醒一下读者,如果foo2_func占据的运行时地址小于main函数,那么这里foo2_func与PC的相对地址将是负数。在机器指令中,使用的是数的补码形式,所以一定要注意,以免造成困惑。
事实上,对于符号foo2使用的重定位类型R_386_32,是绝对地址重定位,链接器只要解析符号foo2的运行时地址替换修订处即可。而对于符号foo2_func,其使用的重定位类型是R_386_PC32,这是一个PC相对地址重定位。而当执行当前指令时,PC中已经加载了下一条指令的地址,并不是当前指令的地址,这就是在引用符号foo2_func处填充“–4”的原因。
我们看到,在链接时,链接器在需要重定位的符号所在的偏移处直接进行了编辑修订,所以人们通常也将链接器形象地称为“link editor”。
http://book./201309/33370.html
|