# Program Anatomy: An ELF Reader From Scratch > A compiled program is a set of tables that point at each other. A reader you wrote gives output you can check byte by byte. **Type:** Build **Languages:** Python (stdlib), C (fixture only) **Prerequisites:** Phase 0 ยท 01 (Dev Environment) with clang installed, Python 3.10 or later, hexadecimal arithmetic **Time:** ~120 minutes ## Learning Objectives - State the authorization rule for every target in this track and the four limits of 17 U.S.C. 1201(f). - Read the ELF header, the section header table, and both string tables with `struct.unpack`. - Decode `.symtab` entries and `.rela` records with the names from the gABI and the x86-64 psABI. - Return the exact bytes of a named function and map every file offset to the structure that owns it. - Read program headers and map a virtual address to a file offset through `PT_LOAD`. - Read the load commands of a Mach-O object and name the ELF idea that each one matches. ## Before You Start: Authorization and the Law Every target in lessons 32 to 36 is a program you wrote and compiled. The source is `code/fixtures/ledger.c`, four short C functions. You own the source, the compiler command, and the object files it produced. There is no licence to read, no protection to pass, and no third party whose code you open. The track never covers software protections, licence checks, or programs you do not own. The rule exists because the law treats disassembly as copying. In *Sega Enterprises Ltd. v. Accolade, Inc.*, 977 F.2d 1510 (9th Cir. 1992), the court held that intermediate copies made during disassembly are copies under the Copyright Act. It then held that "where disassembly is the only way to gain access to the ideas and functional elements embodied in a copyrighted computer program and where there is a legitimate reason for seeking such access, disassembly is a fair use of the copyrighted work, as a matter of law." The second case is *Sony Computer Entertainment, Inc. v. Connectix Corp.*, 203 F.3d 596 (9th Cir. 2000). Connectix copied the PlayStation BIOS while it built an emulator, and the court held those copies fair use. Both cases protected the study. Neither protected copied code in the final product. Two statutes set the limits. 17 U.S.C. 1201(f) lets a person who "has lawfully obtained the right to use a copy of a computer program" circumvent an access control "for the sole purpose of identifying and analyzing those elements of the program that are necessary to achieve interoperability of an independently created computer program with other programs". Four limits stack: a lawful copy, the interoperability purpose, elements that were not already "readily available", and analysis that is otherwise non-infringing. Section 1201(f) is an exception to the anti-circumvention rule only. The copying question stays with fair use. In the European Union, Directive 2009/24/EC Article 5(3) covers study. A person with a right to use a copy can "observe, study or test the functioning of the program in order to determine the ideas and principles which underlie any element of the program". The right applies while loading, displaying, running, transmitting, or storing the program. Article 6 permits decompilation without the rightholder's authorisation when it is "indispensable to obtain the information necessary to achieve the interoperability of an independently created computer program". The conditions match 1201(f). A licensee performs the acts, the information was not already readily available, and the acts stay within the parts needed. Article 6(2) bars using the result for any other goal or for a program that is substantially similar in expression. Article 8 makes contract terms contrary to Article 6 or Article 5(3) null and void. | Source | What it allows | Conditions | |---|---|---| | 17 U.S.C. 1201(f) | Circumventing an access control to analyze a program | Lawful copy, interoperability purpose, information not readily available, no infringement | | Directive 2009/24/EC Art. 5(3) | Observing, studying, and testing a running program | A right to use the copy, acts within normal loading and running | | Directive 2009/24/EC Art. 6 | Decompilation | Licensee, interoperability purpose, information not readily available, confined to the needed parts | | *Sega v. Accolade* | Disassembly as fair use | The only way to reach the ideas and functional elements, a legitimate reason | | *Sony v. Connectix* | Intermediate copies during reverse engineering | A non-infringing final product | These notes come from the statutes and the opinions, and they are not legal advice. When a future target is not yours, obtain written authorization before you open the file and keep the authorization with your evidence. Section 2.1 of the Reverse Engineering 101 manual gives the full citations and the CJEU cases. ## The Problem You compiled `ledger.c` and ran `llvm-objdump -t` on the object. The output lists `percent_change` at value `0000000000000000` with size `0000000000000010`. Which 16 bytes of the 1,768-byte file are that function? The listing also shows `R_X86_64_PC32 .rodata+0x10` at offset `0x36`, and the four bytes at that spot in the file are zero. What will they become, and why is the addend `0x10` when the table starts at `0x14`? rea answers layout questions with `inspect_binary_layout`, and that tool exists only on Linux x64 with pwntools 4.15.0 behind `REA_PWNTOOLS_PYTHON`. On this Mac it is absent. `address_to_file_offset` needs a Ghidra session and a complete copy of the original file bytes with a matching SHA-256. `inspect_artifact` lists what a container holds by path and content hash. It never opens the tables inside an object file. So you write the reader yourself in about 950 lines of Python with `struct.unpack` and no dependency. The tests compare every table with `llvm-objdump` text captured once into the fixtures. ```figure elf-byte-map ``` ## The File Is Tables That Point at Tables The System V gABI, chapter 4, Figure 4-1, draws two views of one file. The linking view has an ELF header, an optional program header table, sections, and a required section header table. The execution view has an ELF header, a required program header table, segments, and an optional section header table. The gABI adds one sentence that governs the whole reader: "Only the ELF header has a fixed position in the file". Every other structure is found by an offset stored in another structure. The gABI also fixes the scalar types and says that "ELF uses no bit-fields". `Elf64_Addr` and `Elf64_Off` are 8 bytes, `Elf64_Half` is 2, `Elf64_Word` is 4, and `Elf64_Xword` is 8. The x86-64 psABI, section 4.1.1, requires `ELFCLASS64`, `ELFDATA2LSB`, `e_machine` `EM_X86_64` (62), and `e_flags` 0. Little-endian with fixed widths means `struct.unpack` with a `<` prefix reads every record in one call. The first 64 bytes of `ledger-x86_64-O1.o`: ```text 00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............ 00000010: 0100 3e00 0100 0000 0000 0000 0000 0000 ..>............. 00000020: 0000 0000 0000 0000 2804 0000 0000 0000 ........(....... 00000030: 0000 0000 4000 0000 0000 4000 0b00 0100 ....@.....@..... ``` Read them against the gABI "ELF Header" page. `e_ident` is the 16 bytes `7f 45 4c 46` (the magic), class 2 (64-bit), data 1 (little-endian), version 1, OS ABI 0 (System V), and padding. Then `e_type` `0x0001` is `ET_REL`, a relocatable object. `e_machine` `0x003e` is 62, `EM_X86_64`. `e_entry` is zero because an object has no entry point. `e_phoff` is zero because an object has no program headers. `e_shoff` is `0x428`, the file offset of the section header table. The last row holds `e_ehsize` `0x40`, `e_phentsize` 0, `e_phnum` 0, `e_shentsize` `0x40`, `e_shnum` `0x0b` (11 headers), and `e_shstrndx` `0x0001`. The reader turns that row into one call: ```python fields = struct.unpack_from("> 32` and `ELF64_R_TYPE(i)` is `i & 0xffffffff`. In a relocatable file `r_offset` is an offset inside the section named by the table's `sh_info`. The type names and formulas come from the x86-64 psABI, Table 4.9. There `S` is the symbol value, `A` the addend, `P` the place being patched, and `L` the PLT entry. `R_X86_64_64` is `S + A`, `R_X86_64_PC32` is `S + A - P`, and `R_X86_64_PLT32` is `L + A - P`. The -O1 object has eight records: ```text .rela.text 0x36 R_X86_64_PC32 .rodata+0x10 .rela.text 0x84 R_X86_64_PC32 .rodata+0x24 .rela.text 0x93 R_X86_64_PC32 .rodata-0x4 .rela.rodata 0x0 R_X86_64_PC32 .rodata.str1.1 .rela.rodata 0x4 R_X86_64_PC32 .L.str.1+0x4 .rela.rodata 0x8 R_X86_64_PC32 .L.str.2+0x8 .rela.rodata 0xc R_X86_64_PC32 .L.str.3+0xc .rela.rodata 0x10 R_X86_64_PC32 .L.str.4+0x10 ``` Take the first one apart. Offset `0x36` lies inside `grade`, which spans `0x10` to `0x3e`. The instruction at `0x33` is `48 8d 0d 00 00 00 00`, a `lea` of a RIP-relative address, and its four-byte displacement sits at `0x36`. The processor adds the displacement to the address of the next instruction, `0x3a`. The compiler wants the result to be `.rodata + 0x14`, where a five-entry table of 32-bit values `1, 2, 3, 4, 4` begins. So the stored value must be `(.rodata + 0x14) - 0x3a`. The formula `S + A - P` with `P = 0x36` gives `.rodata + A - 0x36`. Setting the two equal gives `A = 0x14 - 4 = 0x10`. The addend carries the target offset minus the four bytes between the displacement and the end of the instruction. That table is how clang compiled the `switch` in `grade` at -O1. The code computes `score / 10` with a multiply by `0x66666667` and two shifts, subtracts 6, compares with 4, and indexes the table. The -O1 ELF object keeps no symbol for the table. The reference is the section symbol `.rodata` plus an addend, and only the arm64 build of the same source names it `l_switch.table.grade`. The second record, at `0x84` in `grade_label`, points at a second table of 8-byte values at `.rodata + 0x28`. The third, at `0x93`, points at `.rodata + 0x0`, the start of `labels.rel`. The five `.rela.rodata` records fill `labels.rel`. Each entry sits at offset `4i` and gets the addend `4i`, so `S + A - P` reduces to the string's address minus the base of the table. `grade_label` reads an entry with `movslq`, adds the table base back, and has the pointer. The first record names the section symbol `.rodata.str1.1` with addend 0 because the string `F` is the first byte of that section. The others name `.L.str.1` to `.L.str.4`, the compiler's local labels for `D`, `C`, `B`, and `A`. The same source compiles to three different tables at three optimization levels. The reader shows the difference without a disassembler: | Build | Where `labels` lives | How `grade` selects | How `grade_label` reaches `grade` | |---|---|---|---| | -O0 | `.data`, five 8-byte pointers, five `R_X86_64_64` records | A jump table in `.rodata`: five `R_X86_64_PC32` records pointing into `.text` | A call with `R_X86_64_PLT32 grade-0x4` | | -O1 | `.rodata` as `labels.rel`, five 32-bit relative entries | A value table at `.rodata+0x14`, one `R_X86_64_PC32` | Inlined, no call | | -O2 | Same as -O1 | Same as -O1 | Inlined, `total` grows to 140 bytes | At -O0 the `switch` is a real jump table whose five entries point back into `.text` at `+0x91`, `+0x8c`, `+0x87`, `+0x82`, and `+0x86`. The call to `grade` carries the same `-0x4` addend for the same reason as the `lea`. At -O1 the optimizer saw that every case returns a constant and replaced the jumps with a table of results. Lesson 33 decodes both shapes. ## Program Headers: The Loader's View A relocatable object has no program headers, so `e_phnum` is 0. An executable or a shared object has them, and the loader reads nothing else. An `Elf64_Phdr` is 56 bytes (gABI Figure 5-1): `p_type`, `p_flags`, `p_offset`, `p_vaddr`, `p_paddr`, `p_filesz`, `p_memsz`, `p_align`, format `