You press a breakpoint on a line, the program stops, and the debugger has to show you the value of a local. To do that it must answer two questions: which compilation unit does this address belong to, and which DIE is called this. Both are searches, and the DIE tree is a tree rather than a list, so neither is something you can binary-search.
DWARF’s answer is two index sections. Neither is required to understand the debug info — you can decode the whole tree and never look at them — but without them a debugger cannot be interactive, because every question costs a full walk of a section that can be megabytes long.
Two indexes, answering the two questions in opposite directions.
| Section | Question | Key | Gives you |
|---|---|---|---|
.debug_aranges | which unit covers this address? | address | the .debug_info offset of the unit |
.debug_pubnames | where is the DIE for this name? | name | the .debug_info offset of the DIE |
Both are per-compilation-unit: each unit gets its own set, and each set begins by saying which unit it describes. .debug_aranges does that with a debug_info_offset; .debug_pubnames with an offset and a size, so it can be checked against a unit boundary.
The one thing to hold onto is that these are accelerators, not authority. The offsets they contain point into .debug_info and must be correct, but they are derived data: a debugger that ignored both sections and walked the tree would get the same answers, only slowly. That is a useful thing to know when one of them disagrees with the tree — the tree wins.
The reference binary has two units, so two sets. The whole first set, 48 bytes, with every byte accounted for:
0000: 2c 00 00 00 unit_length = 44 0004: 02 00 version = 2 0006: 00 00 00 00 debug_info_offset = 0 000a: 08 address_size = 8 000b: 00 segment_selector_size = 0 000c: 00 00 00 00 <- FOUR BYTES OF PADDING. The table starts at 0x10, not 0x0c. 0010: 49 11 00 00 00 00 00 00 address = 0x1149 0018: 80 00 00 00 00 00 00 00 length = 0x80 0020: 00 00 00 00 00 00 00 00 <- terminator 0028: 00 00 00 00 00 00 00 00 (padding to the unit length) 0030: 2c 00 00 00 unit_length = 44 0034: 02 00 version = 2 0036: 0f 02 00 00 debug_info_offset = 0x20f (the second unit) 003a: 08 00 00 address_size = 8, segment_selector_size = 0 003c: 00 00 00 00 <- padding again 0040: c9 11 00 00 00 00 00 00 address = 0x11c9 0048: 69 00 00 00 00 00 00 00 length = 0x69
readelf agrees on every value, and the second set confirms the shape: header at 0x30, debug_info_offset = 0x20f, table at 0x40, first tuple 0x11c9/0x69.
unit_length, so the naive expectation is that the tuple table starts at byte 12. It starts at byte 16. gcc pads the table up to the tuple size, and a reader that trusts the field list reads the first address as 0x114900000000 — a plausible 48-bit value that points nowhere. Nothing downstream complains: the length decodes, the terminator is found, and the answer is simply wrong. This was found because an independently written decoder disagreed with readelf, and it is the reason every other claim in this course was checked the same way. Only an 8-byte address size could be tested here; for a 4-byte address, byte 12 is already aligned, so the padding would be zero — a prediction, not a measurement.One more thing about the version: gcc writes version 2 here even under -gdwarf-5. .debug_aranges is one of the sections DWARF 5 barely touched, so a modern compiler still emits a decade-old format version in it.
Built with -gpubnames. Two sets, one per unit:
| Set | debug_info offset | size | Entries |
|---|---|---|---|
| 1 | 0x0 | 527 | 0x10d g_nest, 0x122 g_count, 0x147 g_table, 0x15c make_point, 0x1a5 walk, 0x1d3 sum_table |
| 2 | 0x20f | 209 | 0x5a walk, 0x75 sum_table, 0x8a make_point, 0xa4 main |
Each entry is a 4-byte .debug_info offset then a NUL-terminated name, and a zero offset ends the set. Two observations that stop you building something wrong on top of it:
g_nest, g_count and g_table are all static in the source, and all three are listed. The set covers named DIEs of interest, not strictly external symbols, so a name lookup can return a variable with no linkage at all — which is correct, and which a linker-style “externals only” assumption would miss.Breakpoint on a line inside make_point, the program stops at address 0x1150, and the debugger wants to print a local. Using only the two indexes:
.debug_aranges for a tuple whose address ≤ 0x1150 < address + length. The first set has 0x1149 with length 0x80, and 0x1149 ≤ 0x1150 < 0x11c9. Its debug_info_offset is 0 — so the unit we want starts at the beginning of .debug_info, and only that unit needs decoding.unit_length = 0x20b, so the unit occupies 0x0 to 0x20f. Anything past 0x20f belongs to the second compilation and must not be searched. This bound is why the two units can be decoded independently..debug_pubnames for the string you want. Set 1 contains 0x15c make_point, and 0x15c is inside the first unit’s range, so the match is valid. If the same name appeared in both sets you would have to use the address from step 1 to choose between them — which is the whole reason the two indexes have to be used together..debug_info, read its abbrev code, and walk its children. This is the type chain, and it is confined to one unit.DW_AT_location is a location expression, and resolving its frame base needs the CFI for the FDE covering 0x1150.Five steps, and note how little was actually read: one aranges tuple, one unit header, one pubnames entry, one subprogram DIE and its children. The other unit’s 27 DIEs were never touched, and neither was the string section beyond the entries that matched.
aranges, pubnames and the line program, so the first three steps above work without ever opening the .dwo.$ readelf --debug-dump=aranges shape
$ readelf --debug-dump=pubnames shape
$ readelf -S -W shape | grep -E 'aranges|pubnames|pubtypes'Experiments, cheapest first:
-gpubnames and diff the section list: the indexes simply disappear, and the DIE tree is byte-for-byte the same size. That is the proof that they are derived — they add nothing the tree does not already say..debug_info unit offsets exactly, in the same order — the two sections are written together and there is nothing clever linking them.0x114900000000.pubtypes adds. With -gpubnames the same run produces a .debug_pubtypes as well, 0xe4 bytes, indexing types by name. It is the same structure answering the same question about a different kind of DIE.And the check for a debugger you are building: if a name lookup and a full tree walk ever disagree, the tree is right. These sections are hints, and a hint that is wrong should cost a slow answer, never a wrong one.
Without looking back: a debugger stops at 0x1150 and wants the DIE for make_point. The .debug_aranges tuple says address 0x1149, length 0x80, debug_info_offset 0. Does that tell you where make_point is?
Your debugger resolves most variable names instantly but occasionally reports “no symbol named foo” for a function that definitely exists, and the failure is not reproducible. The .debug_pubnames section is present and non-empty. What is the most likely cause, and what is the smallest change that makes it correct?
The general habit: an intermittent lookup failure is nearly always a search bug, not a data bug. Data that is wrong is wrong consistently — and an index that is present and wrong will eventually disagree with the authoritative source, which is the one thing you should always be able to fall back to.
An index that maps a key to an offset, so the reader can seek instead of scanning, appears in every format in these courses. ELF has .symtab and .dynsym, and its dynamic linking concept depends on the dynamic symbol table being resolvable by name at load time. The PE import directory is a three-level lookup — import table, name table, IAT — that exists for the same reason. The COFF course’s archive container has a symbol index whose entire purpose is to let a linker answer “which member defines this symbol” without opening every member.
What is different about DWARF’s indexes is that they are strictly optional and strictly derived. Nothing in the format requires a debugger to read .debug_aranges, and the ELF symbol table is a different animal: it is the authority, not a hint. Learning to tell those two apart — index versus source of truth — is what lets a tool degrade gracefully instead of failing.
That closes Module 3, and with it the DWARF course as written. The tree, the types, the scopes, the locations, the frames and now the indexes are the whole of what a debugger reads on an ordinary ELF binary.
What remains is specified in courses/dwarf/research.md and deliberately not written: .debug_rnglists and the contents of DW_AT_ranges, the .debug_loclists entry encoding, address_size 4, and the pre-DWARF-5 string forms. The .debug_loclists omission is the notable one, and it is a decision rather than an oversight — see the next paragraph.
A note on what is deliberately absent..debug_loclists appears at -O2 and the course could have described it. It is not taught because the two available readers did not agree on how to decode it: readelf prints “location view pair” rows whose begin and end are both zero at offsets where the bytes are 0x00, and no third reader was available to break the tie. Rather than pick whichever reading suited the lesson, the entry encoding is left out and the concept that needed it — location expressions, which is what -O0 produces — is taught and fully cross-checked instead. The same applies to anything else in the research file’s “still unverified” list.