Example
Take test_module1.ko as an example to illustrate.
.rela.text section content
readelf -r test_module1.ko | head -n 10
Get the following content, which is the content of the .rela.text
section.
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 disassembled content
This content is part of the .text
section of the partial disassembly file obtained by disassembling test_module1.ko
.
Disassembly of section .text: 0000000000000000 <debug_1>: { printk("weakref\\ "); } #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,?i 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> }
section header information
This content is the section header information of each section obtained by 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 00000000000000181 0000000000000000 0 0 1
Symbol table:
This content is the symbol table obtained by 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
Analysis
Process analysis begins with the apply_relocations
function.
Module function call relationship:
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; }
Take the second loop (i=2) in the apply_relocations
function as an example, that is, the .rela.text
section is processed.
On this basis, the execution flow of the apply_relocations
function is as follows:
- It can be seen from the section header information list that the
sh_info
of the current section is 1, and the value of the variableinfosec
in the loop is 1. info->sechdrs[infosec].sh_flags
, that is,flags
in.rela.text
section, its value containsSHF_ALLOC
, so the program continues executing backwards.- The value of
info->sechdrs[i].sh_type
isRELA
So, enter theapply_relocate_add
function. - Incoming parameters:
symindex
is 37relsec
is 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\\ ", (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\\ ", me->name, ELF64_R_TYPE(rel[i].r_info)); return -ENOEXEC; } } return 0; }
Suppose, the starting address of the .text
section is 0x7000000
, and the starting address of the .rela.text
section is 0x8000000
, The starting address of the .symtab
section is 0x9000000
.
On the second basis, and taking the third loop (i=2) as an example, the execution flow of the apply_relocate_add
function is as follows:
- The value of the
loc
variable.- From the section header information list,
sechdrs[relsec].sh_info
, that is, the value ofsh_info
of.rela.text
is 1. - Then, the value of
sechdrs[sechdrs[relsec].sh_info].sh_addr
is the starting address of the.text
section, which is0x7000000
. It completes the relocation in therewrite_section_headers
function, which is an absolute address rather than a relative address. rel[i]
is the third entry in the.rela.text
section, which is00000000001d 003800000004 R_X86_64_PLT32 0000000000000000 msleep - 4
- Then the value of
rel[i].r_offset
is00000000001d
- Comparing the offset value with the parts in the disassembly file, we can see that the
1c
address line is the jump instructioncallq
. The entire jump instruction consists of two parts, the drill instruction and the destination address. The position of1d
is the last three bytes of the jump instruction, which is the specific address of the jump symbol to be jumped by the jump instruction.
- From the section header information list,
- The value of the
sym
variable.- The value of
sechdrs[symindex].sh_addr
is the symbol table address, which is0x9000000
. It also completes the relocation in therewrite_section_headers
function, which is an absolute address rather than a relative address. ELF64_R_SYM(rel[i].r_info)
is the upper 32 bits of003800000004
. That is0x38
, which is 56 in decimal.- Looking at the symbol table can guide, in the symbol table, the 56th entry is exactly
msleep
. - Then the value of
sym
is the information ofmsleep
symbol in the symbol table.
- The value of
- The value of
val
.sym->st_value
has been relocated insimplify_symbols
function, that is,sym[i].st_value + =info->sechdrs[sym[i] .st_shndx].sh_addr;
, its value is the absolute address where the symbol is located.rel[i].r_addend
is the addend.
- According to the content of the
.rela.text
section, the value ofELF64_R_TYPE(rel[i].r_info
isR_X86_64_PLT32
. - The value of
val -= (u64)loc;
is the relative address of the jump instruction and the destination symbol. *(u32 *)loc = val;
Write the jump instruction into the.text
section. At this point, the relocation is complete.
Summary
In the compilation stage, since the actual address of the symbol at runtime cannot be determined, the destination address of the jump statement can only be blanked (ie e8 00 00 00 00
), and then the entry information is recorded in the relocation table (that is, in .rela.xxxx
or .rel.xxxx
), wait for the module to be rewritten when it is loaded.
The process of relocation is to calculate the actual address of the symbol according to the information in the relocation section in the ELF header and the symbol table information when loading the module, and then fill in the destination address of the jump statement, and finally become a complete jump statement.
The role of addend
The following answer is from chatgpt:
The main reason for adding an addend during the relocation process is that the code and data segments in the object file do not always start with aligned addresses. At this time, the addend is needed to correct the reference address.
During the linking process, the start address of the code and data segments is often represented by a virtual start address, which needs to be mapped to the actual physical address at runtime. In the mapping process, because the segment start addresses of different target files are not necessarily aligned, there is an offset between the actual address of the code and data and the virtual address. Therefore, in the relocation process, it is necessary to add an appropriate addend to correct the reference address of the symbol and map it to the correct actual address.
For example, in the static relocation of ELF32, for the reference of an absolute symbol, the relocation type is R_386_32
, and the relocation table will record that the address of the symbol reference needs to be added with an absolute value (plus number) to get the real address of the symbol. For relative symbol references, the relocation type is R_386_PC32
, and the relocation table will record the address that needs to be referenced by the symbol plus another relative address value (addend) to calculate the final address the absolute value of . In fact, what is recorded in the addend is the difference between the absolute address and the relative address. By adding this difference, the correct relocation of the symbol can be realized.
In summary, the addend plays a key role in the relocation process, which is used to correct the offset between the reference address and the symbol address, so as to ensure that the referenced address can finally be correctly mapped to the actual physical address.