示例
以test_module1.ko为例,进行说明。
.rela.text节内容
readelf -r test_module1.ko | head -n 10
得到如下内容,如下内容为 .rela.text
节的内容。
Relocation section '.rela.text' at offset 0x21ab0 contains 12 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000005 002a00000004 R_X86_64_PLT32 0000000000000000 __fentry__ - 4
000000000011 002e0000000b R_X86_64_32S 0000000000000000 test_weakref + 0
00000000001d 003800000004 R_X86_64_PLT32 0000000000000000 msleep - 4
000000000026 000200000002 R_X86_64_PC32 0000000000000000 .text.unlikely + 17
00000000002b 002e00000004 R_X86_64_PLT32 0000000000000000 test_weakref - 4
000000000030 002900000004 R_X86_64_PLT32 0000000000000000 kthread_should_stop - 4
000000000045 002a00000004 R_X86_64_PLT32 0000000000000000 __fentry__ - 4
.text节内容
此内容为反汇编 test_module1.ko
所得到的部分反汇编文件的 .text
节的部分内容。
Disassembly of section .text:
0000000000000000 <debug_1>:
{
printk("weakref\n");
}
#endif
int debug_1(void *arg)
{
0: f3 0f 1e fa endbr64
4: e8 00 00 00 00 callq 9 <debug_1+0x9>
9: 55 push %rbp
a: 48 89 e5 mov %rsp,%rbp
d: 53 push %rbx
while(!kthread_should_stop()){
msleep(5000);
if(test_weakref)
e: 48 c7 c3 00 00 00 00 mov $0x0,%rbx
while(!kthread_should_stop()){
15: eb 18 jmp 2f <debug_1+0x2f>
msleep(5000);
17: bf 88 13 00 00 mov $0x1388,%edi
1c: e8 00 00 00 00 callq 21 <debug_1+0x21>
if(test_weakref)
21: 48 85 db test %rbx,%rbx
24: 0f 84 00 00 00 00 je 2a <debug_1+0x2a>
test_weakref();
2a: e8 00 00 00 00 callq 2f <debug_1+0x2f>
}
节头信息
此内容为 readelf -S test_module1.ko
获得的各节的节头信息:
There are 40 section headers, starting at offset 0x3c788:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
00000000000000b0 0000000000000000 AX 0 0 16
[ 2] .rela.text RELA 0000000000000000 00021ab0
0000000000000120 0000000000000018 I 37 1 8
......
[37] .symtab SYMTAB 0000000000000000 00021368
0000000000000558 0000000000000018 38 36 8
[38] .strtab STRTAB 0000000000000000 000218c0
00000000000001eb 0000000000000000 0 0 1
[39] .shstrtab STRTAB 0000000000000000 0003c600
0000000000000181 0000000000000000 0 0 1
符号表:
此内容为 readelf -s test_module1.ko
获得的符号表
Symbol table '.symtab' contains 57 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
......
54: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND unregister_module_notifie
55: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND register_module_notifier
56: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND msleep
解析
流程解析从 apply_relocations
函数开始。
模块函数调用关系:
init_module
->load_module
–>apply_relocations
static int apply_relocations(struct module *mod, const struct load_info *info)
{
unsigned int i;
int err = 0;
/* Now do relocations. */
for (i = 1; i < info->hdr->e_shnum; i++) {
unsigned int infosec = info->sechdrs[i].sh_info;
/* Not a valid relocation section? */
if (infosec >= info->hdr->e_shnum)
continue;
/* Don't bother with non-allocated sections */
if (!(info->sechdrs[infosec].sh_flags & SHF_ALLOC))
continue;
/* Livepatch relocation sections are applied by livepatch */
if (info->sechdrs[i].sh_flags & SHF_RELA_LIVEPATCH)
continue;
if (info->sechdrs[i].sh_type == SHT_REL)
err = apply_relocate(info->sechdrs, info->strtab,
info->index.sym, i, mod);
else if (info->sechdrs[i].sh_type == SHT_RELA)
err = apply_relocate_add(info->sechdrs, info->strtab,
info->index.sym, i, mod);
if (err < 0)
break;
}
return err;
}
以 apply_relocations
函数中的第第二次循环(i=2)为例,即处理的是 .rela.text
节。
在此基础上,apply_relocations
函数执行流程如下:
- 从节头信息列表中可知,当前节的
sh_info
为 1 ,则循环中变量infosec
的值为 1 。 info->sechdrs[infosec].sh_flags
,也就是.rela.text
段中的flags
,其值包含SHF_ALLOC
,所以,程序继续向后执行。info->sechdrs[i].sh_type
的值为RELA
所以,进入到apply_relocate_add
函数中。- 传入参数 :
symindex
为 37relsec
为 2
int apply_relocate_add(Elf64_Shdr *sechdrs,
const char *strtab,
unsigned int symindex,
unsigned int relsec,
struct module *me)
{
unsigned int i;
Elf64_Rela *rel = (void *)sechdrs[relsec].sh_addr;
Elf64_Sym *sym;
void *loc;
u64 val;
for (i = 0; i < sechdrs[relsec].sh_size / sizeof(*rel); i++) {
/* This is where to make the change */
loc = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr
+ rel[i].r_offset;
/* This is the symbol it is referring to. Note that all
undefined symbols have been resolved. */
sym = (Elf64_Sym *)sechdrs[symindex].sh_addr
+ ELF64_R_SYM(rel[i].r_info);
DEBUGP("type %d st_value %Lx r_addend %Lx loc %Lx\n",
(int)ELF64_R_TYPE(rel[i].r_info),
sym->st_value, rel[i].r_addend, (u64)loc);
val = sym->st_value + rel[i].r_addend;
switch (ELF64_R_TYPE(rel[i].r_info)) {
case R_X86_64_32S:
if (*(s32 *)loc != 0)
goto invalid_relocation;
*(s32 *)loc = val;
if ((s64)val != *(s32 *)loc)
goto overflow;
break;
case R_X86_64_PC32:
case R_X86_64_PLT32:
if (*(u32 *)loc != 0)
goto invalid_relocation;
val -= (u64)loc;
*(u32 *)loc = val;
break;
default:
pr_err("%s: Unknown rela relocation: %llu\n",
me->name, ELF64_R_TYPE(rel[i].r_info));
return -ENOEXEC;
}
}
return 0;
}
假设,.text
段起始地址为 0x7000000
,.rela.text
段起始地址为 0x8000000
, .symtab
段的起始地址为 0x9000000
。
在次基础上,且以第三次循环(i=2)为例, apply_relocate_add
函数执行流程如下:
loc
变量的值。- 从节头信息列表中可知,
sechdrs[relsec].sh_info
,也就是.rela.text
的sh_info
的值为 1. - 则,
sechdrs[sechdrs[relsec].sh_info].sh_addr
的值为.text
节的起始地址,也就是0x7000000
。其在rewrite_section_headers
函数中完成重定位,位绝对地址,而非相对地址。 rel[i]
为.rela.text
节的第三个条目,也就是00000000001d 003800000004 R_X86_64_PLT32 0000000000000000 msleep - 4
- 则
rel[i].r_offset
的值为00000000001d
- 将该偏移值对比反汇编文件中的部分可知,
1c
地址行处为跳转指令callq
。整个跳转指令包含两部分,条钻指令和目的地址。而1d
的位置就是跳转指令的后三个字节,也就是跳转指令要跳转符号的具体地址。
- 从节头信息列表中可知,
sym
变量的值。sechdrs[symindex].sh_addr
的值为符号表地址,也就是0x9000000
。其同样在rewrite_section_headers
函数中完成重定位,位绝对地址,而非相对地址。ELF64_R_SYM(rel[i].r_info)
就是003800000004
的高32位。即0x38
,也就是十进制的 56。- 查看符号表可以指导,符号表中,第56个条目正是
msleep
。 - 则
sym
的值就是符号表中,msleep
符号的信息。
val
的值。sym->st_value
在simplify_symbols
函数中,已被重新定位过,即sym[i].st_value +=info->sechdrs[sym[i].st_shndx].sh_addr;
,其值为符号所在的绝对地址。rel[i].r_addend
为加数。
- 根据
.rela.text
节的内容可知,ELF64_R_TYPE(rel[i].r_info
的值为R_X86_64_PLT32
。 val -= (u64)loc;
的值为,跳转指令与目的符号的相对地址。*(u32 *)loc = val;
将跳转指令,写入到.text
节中。至此,完成重定位。
总结
在编译阶段,由于无法确定符号运行时的实际地址,只能将跳转语句的目的地址置空(即 e8 00 00 00 00
),然后将条目信息记录到重定位表中(也就是 .rela.xxxx
或者 .rel.xxxx
中),等模块在加载时重新写入。
重定位的过程,就是在加载模块时,根据 ELF 头中的重定位节中的信息以及符号表信息,计算符号的实际地址,然后填入到跳转语句的目的地址部分,最终变成完整的跳转语句。
加数的作用
以下回答来自 chatgpt:
重定位过程中需要加上一个加数的主要原因是,目标文件中的代码和数据段并非总是以对齐的地址开始,这时候需要通过加数来校正引用地址。
在链接过程中,代码和数据段的起始地址往往用一个虚拟起始地址表示,在运行时需要将其映射到实际的物理地址中。而在映射过程中,因为不同目标文件的段起始地址不一定对齐,导致代码和数据的实际地址与虚拟地址存在偏移。因此,在重定位过程中需要通过加上合适的加数,才能够正确地修正符号的引用地址,映射到正确的实际地址中。
例如,在 ELF32 的静态重定位中,对于绝对符号的引用,重定位类型为 R_386_32
,重定位表中会记录需要将该符号引用的地址加上一个绝对的数值(加数)来得到符号的真实地址。而对于相对符号的引用,重定位类型为 R_386_PC32
,重定位表中会记录需要将该符号引用的地址加上另一个相对的地址值(加数),计算出地址最终的绝对值。实际上,加数中记录的是绝对地址和相对地址的差值,通过加上这个差值,才能实现符号的正确重定位。
综上,加数在重定位过程中起到了一个关键作用,用于校正引用地址和符号地址之间的偏移,确保引用的地址最终能够正确地映射到实际的物理地址中。