Mini-Readelf: Gluing It All Together
You have built the pieces over 4 parts. Part 1 read the header and walked sections. Part 2 made a binary actually run. Part 3 gave you symbol tables. Part 4 gave you relocations. This time there’s nothing new to learn, just one table we haven’t printed yet, one flag to add and a tool you can point at any binary.
This is Part 5: the mini-readelf.
What we’re building
One C program that takes an ELF and prints:
- the header (
-h) - the sections (
-S) - the symbols (
-s) - the relocations (
-r)
Let it run with no flags and it shows everything. You already have all the code for the first three. Relocations need one more lookup chain, and that’s the last puzzle.
The one new thing: sh_link
Every Elf64_Shdr has an sh_link field, the section index of another section that this one depends on. We ignored it in Part 1 and 3. Now it’s the whole trick.
For a symbol table section (.symtab or .dynsym), sh_link points to its string table (.strtab or .dynstr). Part 3 used exactly this, the string table comes from the symbol table’s sh_link, not from anywhere nearby:
Elf64_Shdr *strtab_hdr = &shdr_table[symtab_hdr->sh_link];const char *strtab = (const char *)elf->map + strtab_hdr->sh_offset;For a relocation section (.rela.dyn or .rela.plt), sh_link points to the symbol table the relocations index into, .dynsym. And that section’s sh_link points to .dynstr. A two-hop chain:
.rela.dyn --sh_link--> .dynsym --sh_link--> .dynstr r_info st_name string bytesEach Elf64_Rela has an r_info field. The high 32 bits are ELF64_R_SYM, an index into .dynsym. The symbol at that index has an st_name, an offset into .dynstr. Three dereferences and you’ve got the function name next to the relocation that patches its GOT slot.
The lookup chain, in code
int sym_index = ELF64_R_SYM(rela->r_info);
Elf64_Shdr *shdr_table = (Elf64_Shdr *)(elf->map + elf->ehdr->e_shoff);Elf64_Shdr *symtab = &shdr_table[rela->sh_link]; // 1-hop: .dynsymElf64_Shdr *strtab = &shdr_table[symtab->sh_link]; // 2-hop: .dynstr
Elf64_Sym *syms = (Elf64_Sym *)(elf->map + symtab->sh_offset);const char *names = (const char *)(elf->map + strtab->sh_offset);
const char *symbol_name = names + syms[sym_index].st_name;That’s it. The thing that looks scary, “how does readelf know that R_X86_64_JUMP_SLOT at 0x5000 is puts?”, is just this chain. An offset into a table that holds offsets into a table that holds strings.
The type string
Before the printer we need the one helper the whole series didn’t draw yet, a name for each relocation type. Four of them cover the real world:
const char *reloc_type_str(uint32_t type) { switch (type) { case R_X86_64_64: return "R_X86_64_64"; case R_X86_64_RELATIVE: return "R_X86_64_RELATIVE"; case R_X86_64_GLOB_DAT: return "R_X86_64_GLOB_DAT"; case R_X86_64_JUMP_SLOT: return "R_X86_64_JUMP_SLOT"; case R_X86_64_COPY: return "R_X86_64_COPY"; default: return "(unknown)"; }}The relocation printer
With the name resolver, printing relocations is a loop per .rela section:
void print_relocations(const elf_file_t *elf) { Elf64_Shdr *shdr_table = (Elf64_Shdr *)(elf->map + elf->ehdr->e_shoff);
for (int i = 0; i < elf->ehdr->e_shnum; i++) { if (shdr_table[i].sh_type != SHT_RELA) continue;
printf("Relocation section '%s':\n", get_section_name(elf, shdr_table[i].sh_name)); printf(" Offset Type Sym. Name + Addend\n");
Elf64_Rela *rela = (Elf64_Rela *)(elf->map + shdr_table[i].sh_offset); int count = shdr_table[i].sh_size / sizeof(Elf64_Rela);
for (int n = 0; n < count; n++) { int type = ELF64_R_TYPE(rela[n].r_info); int sym_idx = ELF64_R_SYM(rela[n].r_info);
// resolve name through the two-hop chain Elf64_Shdr *symtab = &shdr_table[shdr_table[i].sh_link]; Elf64_Shdr *strtab = &shdr_table[symtab->sh_link]; Elf64_Sym *syms = (Elf64_Sym *)(elf->map + symtab->sh_offset); const char *names = (const char *)(elf->map + strtab->sh_offset); const char *name = names + syms[sym_idx].st_name;
if (name[0] == '\0') name = (type == R_X86_64_RELATIVE) ? "(base)" : "<none>";
printf(" %016lx %-17s %s + %lx\n", rela[n].r_offset, reloc_type_str(type), name, rela[n].r_addend); } printf("\n"); }}The main that ties it all together
The parse functions we already have, print_header, print_sections, print_symbols, plus the new print_relocations. main just switches on flags:
int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "Usage: %s [-h] [-S] [-s] [-r] <elf-binary>\n", argv[0]); return EXIT_FAILURE; }
int show_h = 0, show_S = 0, show_s = 0, show_r = 0; const char *path = NULL;
for (int i = 1; i < argc; i++) { if (argv[i][0] == '-') { for (const char *c = argv[i] + 1; *c; c++) { if (*c == 'h') show_h = 1; else if (*c == 'S') show_S = 1; else if (*c == 's') show_s = 1; else if (*c == 'r') show_r = 1; } } else if (!path) { path = argv[i]; } }
elf_file_t elf = {0}; if (!elf_load(path, &elf)) return EXIT_FAILURE;
if (show_h) print_header(&elf); if (show_S) print_sections(&elf); if (show_s) print_symbols(&elf); if (show_r) print_relocations(&elf);
elf_unload(&elf); return EXIT_SUCCESS;}The reunion
Point it at our own elf-loader binary (the one we built in Part 2, it has full symbols, unlike most binaries):
$ ./mini-readelf -h -s -r ./elf-loader
ELF Header: Class: ELF64 Type: DYN Machine: x86-64 Entry: 0x1160 Sections: 37
Symbol table '.dynsym' contains 27 entries: Num: Value Size Type Bind Name 1: 0x0000000000000000 0 FUNC GLOBAL __libc_start_main 3: 0x0000000000000000 0 FUNC GLOBAL puts 7: 0x0000000000000000 0 FUNC GLOBAL printf ...
Symbol table '.symtab' contains 60 entries: 33: 0x00000000000013c8 2627 FUNC GLOBAL load_elf 43: 0x0000000000001e0b 1283 FUNC GLOBAL build_stack 49: 0x0000000000001259 367 FUNC GLOBAL main ...
Relocation section '.rela.dyn': Offset Type Sym. Name + Addend 0000000000004dd0 R_X86_64_RELATIVE (base) + 1250 0000000000004dd8 R_X86_64_RELATIVE (base) + 1200 00000000000050a0 R_X86_64_RELATIVE (base) + 50a0 0000000000004fc0 R_X86_64_GLOB_DAT __libc_start_main + 0 00000000000050c0 R_X86_64_COPY stdout + 0 ...
Relocation section '.rela.plt': Offset Type Sym. Name + Addend 0000000000005000 R_X86_64_JUMP_SLOT puts + 0 0000000000005008 R_X86_64_JUMP_SLOT strlen + 0 0000000000005020 R_X86_64_JUMP_SLOT printf + 0 ...Every one of those rows came from bytes in the file, no libelf, no parsing library. load_elf at 0x13c8, size 2627? That’s st_value and st_size from .symtab row 33. puts at 0x5000? That’s r_info’s symbol index 3 walked through .dynsym to name puts. You can cross-check every line with readelf -s and readelf -r, same numbers.
The catch: stripped binaries
Now run it on something real from your system:
$ ./mini-readelf -s /usr/bin/lsSymbol table '.dynsym' contains 132 entries:One symbol table. No .symtab. This is the Part 3 lesson made visible: Arch strips release binaries, and .symtab is what dies. .dynsym survives because the dynamic linker needs it, readelf’s --dyn-syms still works on a stripped binary, ours does too, and now you know why. You’re not fighting the tool. You’re reading the same bytes it does.
Try it
Point it at things and let it talk about them:
gcc minireadelf.c -o mini-readelf -O2
# your own tools./mini-readelf ./elf-explorer./mini-readelf ./elf-loader
# the rest of the system, notice which ones still have .symtab./mini-readelf /bin/bash | grep "Symbol table"./mini-readelf /usr/bin/python 2>/dev/null | grep "Symbol table"Diff against the real one to stay honest:
readelf -s ./elf-loader > /tmp/readelf-syms./mini-readelf -s ./elf-loader | grep "load_elf\|build_stack\|main"Why this series ended here
Every part built on the last, and every part was the same move: the file is bytes, the structs define the layout, cast a pointer and read. Headers, sections, symbols, relocations, it never stopped being that.
That’s the whole point of you. You could have just run readelf. Anyone can. What you did is rebuild it, one table at a time, until the tool had nothing left to hide. That’s the actual skill, the tool is just the receipt.
Next reads
View all →5 Sept
Relocations: How PIE Binaries Fix Their Addresses
A PIE binary can't write final addresses because ASLR moves it. The linker leaves placeholders and the loader patches them after mapping. That's a relocation: R_X86_64_RELATIVE, GLOB_DAT and JUMP_SLOT.
4 Sept
Symbol Tables: What Function Names Actually Are
Function names in a binary are just entries in a table. Two tables actually: .symtab and .dynsym. Here's what each is for, how the struct works, and how to resolve a name from an address.
3 Sept
Building a Custom ELF Loader from Scratch, in C
You type ./program and the kernel loads it. Here's how that works: we build a userspace ELF loader in ~300 lines of C that maps PT_LOAD segments, builds a stack by hand, and jumps to the entry point.
Get posts by email
One email when I publish, not a drip, not weekly. Sign up and I'll only write when there's something new.
You won't get mail just for signing up. Unsubscribe any time.