Here is the most uncomfortable fact in the previous concept, stated plainly: while building emit_elf.py, four bugs produced object files that every available tool read without a single warning.
bug readelf llvm-readobj llvm-objdump ld result ------------- ------- ----------- ------------ ----- -------------- e_shoff wrong ERROR clean clean fail loud, lucky sh_link/info warns clean clean ok SYMBOLS EMPTY symbols overlap clean clean clean ok WRONG NUMBER add before call clean clean clean ok WRONG NUMBER
Read the last two rows. readelf, llvm-readobj-21 and llvm-objdump-21 all agreed completely with a file that computed the wrong answer. Agreement between readers is not evidence of correctness, because readers are being asked the wrong question. They are being asked “is this file well formed?” and the answer was yes. The question that mattered was “does this file mean what I meant?” and no reader was asked it, because no reader can be — a reader has no access to the intent.
So this concept is about the difference, and about the one technique that closes it. crosscheck.py in this course's assets/samples/ runs 95 checks over the specimens in this directory, and it deliberately uses three different kinds of evidence rather than three copies of the same one.
Three tiers of evidence, ordered by how much they can catch and by how much they cost to set up. The ordering is the important part, because the cheapest tier catches the most.
Note the counter-intuitive ordering: the strongest evidence is the cheapest. That is because Tier 3 needs no second opinion at all — it is a self-consistency check, and an object file is unusually rich in redundant facts. The COFF case is the clearest: a reader that mis-parses the auxiliary records disagrees with itself, because the same section's size appears in the section header and in a symbol table entry, and they no longer match. That is a bug caught with no oracle at all.
And the reason Tier 1 is last despite being the most obvious choice: a reference producer is not an oracle for your file, it is an oracle for a file. It will differ from yours in section ordering, in how much padding it inserts, in whether it emits .comment, in the order of the symbol table — and every one of those differences is a legitimate choice. Diffing the whole file gives you a hundred differences of which perhaps one is a bug, which is a bad signal-to-noise ratio and a test that everyone learns to ignore.
demo_coff.o has PointerToSymbolTable = 0x211 and NumberOfSymbols = 26. The records are 18 bytes each, so the string table begins at 0x211 + 26*18 = 0x3e5. Now decode all 26 and count how many are symbols:
26 records. 14 are symbols. 12 are AUXILIARY.
A reader that ignores NumberOfAuxSymbols prints 12
nonsense symbols, each with a garbage Value such as
0x672d789d, and NOTHING LOOKS WRONG.
And the check that catches it is one line, because
every aux record carries facts the section header
also carries:
$ python3 crosscheck.py
ok aux record for .text: Length 62 == section RawDataSize 62
ok aux record for .text: RelocationCount 4 == header 4
ok aux record for .data: Length 16 == section RawDataSize 16
ok aux record for .bss: Length 4 == section RawDataSize 4
ok aux record for .rdata: Length 6 == section RawDataSize 6
ok aux record for .rdata: Length 3 == section RawDataSize 3
ok every one of the 7 sections has a section-definition aux record
That is the entire technique, and it is worth noticing that the seven sections include two both named .rdata. So the check cannot be keyed by name — it must be keyed by index, which is itself a lesson from Sections, Compared. A validator that builds a dictionary from section name to section will silently lose one of the two .rdata sections, and the loss will not be an error.
ELF offers a different flavour of the same thing, and it caught a real mistake while this course was being written. The spec says a symbol table's sh_info is the index of the first symbol whose binding is not STB_LOCAL, and that all locals must precede globals. So a validator can check:
$ python3 crosscheck.py
ok symtab sh_info == index of first GLOBAL symbol (4)
ok every symbol before sh_info is LOCAL, as the rule requires
Both are Tier 3: no second implementation, no external tool, just the specification's own internal consistency requirement, checked mechanically. And the ELF relocation sections give a third: sh_link must be a SHT_SYMTAB and sh_info must be a valid section index. Getting those two backwards — which is exactly bug 2 in the previous concept — is caught by a check that costs one line and needs no oracle.
The interesting problem with Tier 2 is that independent is doing more work than it appears. On this machine there are two genuinely separate ELF toolchains — LLVM 21 and binutils — and for COFF there are two as well. For Mach-O there is one, because binutils refuses the format. That asymmetry is a fact about the world and it is worth stating rather than hiding, because it changes how much a Mach-O claim is worth.
And here is the sharpest point in the concept, which is about the failure mode of Tier 2 itself. A reader written from a specification and a reader written from the same specification share their author's misconceptions. If you misread the Mach-O r_info bit layout in your decoder, and you also misread it when you wrote the check, the two will agree perfectly and both will be wrong. Tier 2 only catches what the two implementations got independently right or independently wrong by different routes.
Which is why the strongest form of the check is a byte-exact structural diff over every field, compared as sorted sets rather than in order. The diff that found a real problem in this course was not a value mismatch; it was that readelf prints a section symbol's target as the section's name while the file stores st_name = 0 and the reader has to substitute one. That is a difference in presentation, discovered only by comparing everything, and it is exactly the kind of thing a spot check of “does the symbol name look right?” would have missed.
So what actually caught bug 3, the overlapping symbols? Not a reader. The program's output. The expected answer was 41 and the program printed 389845008, and no tool in the world could have told you why — because the file was legal, the toolchain was correct, and the bug was in a description of intent that existed nowhere except in the emitter's head.
And the ordering of step 3 is not incidental. If you run the program first and then work out what it should have printed, you will rationalise whatever it printed. If you write down start(5) = helper(5) + 1 = 11 and answer = 41 before linking, then a mismatch is unambiguous — and in this course it was unambiguous immediately, which is how the overlapping symbols were found in minutes rather than days.
-0x4 claim, the Mach-O linkonce claim, and the string-table claim in Where Names Live were all found by building a specimen and measuring, not by reading a specification and believing it.What the harness actually reports, and what each block of it is for:
$ python3 crosscheck.py
========================================================================
Object Files -- crosscheck
Three tiers: independent decoder, external toolchain, and the
file's own internal invariants. Tier 3 catches the most.
========================================================================
demo_elf.o (ELF64 x86-64 relocatable)
ok e_type == 1 (ET_REL) -- this is a relocatable file
ok e_phnum == 0 -- no program headers, and that is not an omission
ok [3] .rela.text sh_link points at the symtab
ok [3] .rela.text sh_info is a valid target section (.text)
ok [5] .rela.data sh_link points at the symtab
ok [5] .rela.data sh_info is a valid target section (.data)
ok [12] .rela.eh_frame sh_link points at the symtab
ok [12] .rela.eh_frame sh_info is a valid target section (.eh_frame)
ok symtab sh_info == index of first GLOBAL symbol (4)
ok every symbol before sh_info is LOCAL, as the rule requires
ok TIER 2: all 8 relocations match readelf exactly
ok .text relocations are the ones the course claims
ok section names are exactly the ones the course lists
...
hand.o (emitted by emit_elf.py, no library involved)
ok e_type == ET_REL
ok e_phnum == 0
ok exactly 8 section headers, as emit_elf.py says
ok section 5 is .symtab and section 6 is .strtab
ok the five symbols are the ones emit_elf.py declares
ok answer (4 bytes at 0) does not overlap answer_ptr at 4
ok .data is big enough for both
ok TIER 2: a real linker accepts it (ld -r exit 0)
...
========================================================================
95 passed, 0 failed
========================================================================
Two lines in that output are worth reading twice, because they are the harness doing the thing no single tool does.
“answer (4 bytes at 0) does not overlap answer_ptr at 4” is a Tier 3 check on the hand-built file, and it exists precisely because the overlapping version of that file was produced and shipped by mistake. It is not a specification requirement — ELF permits overlap — it is a check against this emitter's intent, written after the bug. That is the honest shape of the technique: the checks you end up with are the ones your past mistakes demanded, and the harness is a list of scar tissue.
“TIER 2: a real linker accepts it (ld -r exit 0)” is the only check that involves a different program entirely, and it is one line. It is also the only check that would have caught bug 1 — the wrong e_shoff — instantly, because GNU ld would have refused the file rather than trying to interpret it. Which raises the obvious question: why not just always run the linker?
Because a linker accepts plenty it should not, and refuses some it could cope with. It accepted the file with overlapping symbols. It accepted the file with a mov whose add was in the wrong place. It did not complain about a .text section containing something that is not instructions. The linker is a consumer with needs, not a validator, and treating its silence as approval is the same mistake as treating readelf's silence as approval — one tier up.
$ cd courses/obj/assets/samples
$ python3 crosscheck.py # 95 checks
$ python3 crosscheck.py -v # show the diffs
$ python3 ardec.py libdemo.a
$ sh build_samples.sh # rebuild every specimendemo_elf.o and swap sh_link and sh_info on .rela.text. Run crosscheck.py and count how many checks fail — the two internal-invariant checks should fire immediately, before any external tool is consulted. Then run readelf -rW on it and note what it says. The order of the failure is the lesson: the cheapest oracle speaks first.demo_elf.o — set answer's st_value to 0 and answer_ptr's to 0 as well. Every Tier 3 check still passes and readelf is silent. Now find out what it takes to notice: linking and running, and comparing against a hand-computed expected value. Write down how many steps that was, because the answer is the argument for the third tier.wasm-objdump or javap. Then deliberately introduce a misreading into your own decoder that the specification's wording genuinely permits, and check whether the diff catches it. If it does not — and for an ambiguous point it may not — you have just measured the real limit of Tier 2, which is more instructive than any amount of theory about it.sh_addralign is a power of two; every sh_entsize is a multiple of the structure size; no symbol's st_value exceeds its section's sh_size; every relocation's offset is inside the section its sh_info names. Pick one, add it, and then try to find a real object that violates it. If you find one, you have learned something about the format that no specification states.sh build_samples.sh rebuilds everything and cmps the three-format specimen against the staged copy. It currently reports IDENTICAL. Then change one compiler flag, rebuild, and see which specimens change. The ones that change are the ones whose recorded byte values you must re-verify, and knowing which is the difference between updating a course correctly and quietly making it wrong.Without looking back: you have written a decoder for an object format from its specification, and a second tool on your machine also reads the format. Both agree on every field of all 400 files in your corpus. Your decoder is nonetheless wrong about one field, in a way that makes it report a plausible but incorrect value. How is that possible, and what would have caught it?
You maintain a tool that reads a binary format, and a user reports that it misparses one file out of ten thousand. You have checked the file with your own reader, with the reference implementation, and with a second third-party library — all three agree, field for field. The file is well formed by every tool available. Given the tiers in this concept, what is the most likely explanation, what is the first thing you should do, and what is the thing you are most likely to do instead that will waste a day?
The general habit: when every tool agrees and the answer is wrong, the tools are not the variable. Go get the bytes, then look for a fact the format records twice, and treat any investigation in which nothing can fail as a signal that you are in the wrong place.
This is the last concept of the course, and it is the one that makes the other seventeen safe to trust. Everything asserted in this course was measured on committed, byte-reproducible specimens, and the three claims that were wrong — the addend's location, Mach-O's linkonce mechanism, and ELF's two string tables — were all wrong in the same way: they were plausible readings of a specification that had been consulted rather than a file that had been opened. That is the argument for the whole method, stated as three corrections.
The connection to Writing One From Scratch is direct, and the four bugs are the evidence. Every one of them got past every reader, and two of them produced a program that ran and printed a plausible wrong number. The encodings concept adds the fifth that did not happen — a relocation offset one byte off the field — which on x86-64 produces a binary that links and jumps slightly wrong, because a fixed four-byte field gives nothing to signal the mistake. Between the two concepts is the whole argument for a verification discipline: agreement is cheap, and it is cheap precisely because readers are answering “is this well formed?” rather than “is this what you meant?”
The connection to The archive is a Tier 3 opportunity that the format hands you for free, and it is the clearest one in the course. An archive's index says which member defines a symbol, and each member's own symbol table independently says what it defines. Two records of the same fact, in two different files, which is exactly the shape a self-consistency check needs — and the course uses it, cross-checking all four index entries against the members they point at. This is also where the COFF course's ar_decode.py and this course's version of it are the same idea applied twice, which is the triangulation principle at the level of tooling rather than of formats.
The COFF connection is the strongest, because COFF is where redundancy is densest. The same facts appear in the section header and the auxiliary record; the same class of data appears in the symbol table and the string table; the section name appears both inline and as an escape into that table. That density is not a design virtue — it is the residue of a format that grew by accretion, and the COFF course documents three places where the specification and the actual emitters disagree. But the accidents of a format are often more useful than its intentions, because a redundant fact is a free consistency check that nobody had to design. The best validation harnesses are built out of the parts of a format nobody chose on purpose.
And the connection outward, to the whole course collection, is the method rather than the format. Every course here claims the same thing in a different costume: that a claim is worth teaching once two implementations that share no code agree on it. The JVM course has a crosscheck.py, the COFF course has one, the WebAssembly course has one, and this course has one. They are not identical, and they should not be: a harness is specific to what a format makes checkable. What they share is the tiering, and the tiering is the transferable part. Run the self-consistency checks first because they are cheapest and catch the most. Diff against an unrelated toolchain second, because that catches a misreading of the specification. Execute against a hand-computed expectation last, because that is the only tier that can catch an error of intention — and an error of intention is the only kind that no amount of reading will ever find.