Linux kernel module relocation process simple example analysis

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 variable infosec in the loop is 1.
  • info->sechdrs[infosec].sh_flags, that is, flags in .rela.text section, its value contains SHF_ALLOC , so the program continues executing backwards.
  • The value of info->sechdrs[i].sh_type is RELA So, enter the apply_relocate_add function.
  • Incoming parameters:
    • symindex is 37
    • relsec 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 of sh_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 is 0x7000000 . It completes the relocation in the rewrite_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 is 00000000001d 003800000004 R_X86_64_PLT32 0000000000000000 msleep - 4
    • Then the value of rel[i].r_offset is 00000000001d
    • Comparing the offset value with the parts in the disassembly file, we can see that the 1c address line is the jump instruction callq . The entire jump instruction consists of two parts, the drill instruction and the destination address. The position of 1d 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.
  • The value of the sym variable.
    • The value of sechdrs[symindex].sh_addr is the symbol table address, which is 0x9000000 . It also completes the relocation in the rewrite_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 of 003800000004. That is 0x38 , 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 of msleep symbol in the symbol table.
  • The value of val.
    • sym->st_value has been relocated in simplify_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 of ELF64_R_TYPE(rel[i].r_info is R_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.