You can now decode a line program. The remaining question is the one a debugger asks every time you stop: "I am at address 0x1154 — what do I tell the user?"
Answering it wrong is not a small error. A lookup that ignores where one function's rows end and the next one's begin will happily report that address 0x2000 is "line 17 of shape.c", because line 17 was the last row it happened to decode. Every line-number feature you have ever used in a debugger depends on two rules getting this right.
Two rules, and together they are the whole lookup:
DW_LNE_set_address and closed by DW_LNE_end_sequence. A lookup must not cross a sequence boundary, because the address ranges on either side may be in completely different places in memory.That is the model. Everything else is bookkeeping.
Our program has exactly one sequence, and it is the complete opcode list from shape_v5 — 22 statements, start to end:
[0x00000044] Set column to 35
[0x00000046] Extended opcode 2: set Address to 0x1129
[0x00000051] Special opcode 12: advance Address by 0 to 0x1129 and Line by 7 to 8
[0x00000052] Set column to 14
[0x00000054] Special opcode 202: advance Address by 14 to 0x1137 and Line by 1 to 9
[0x00000055] Set column to 1
[0x00000057] Special opcode 118: advance Address by 8 to 0x113f and Line by 1 to 10
[0x00000058] Set column to 16
[0x0000005a] Special opcode 35: advance Address by 2 to 0x1141 and Line by 2 to 12
[0x0000005b] Set column to 9
[0x0000005d] Special opcode 175: advance Address by 12 to 0x114d and Line by 2 to 14
[0x0000005e] Set column to 11
[0x00000060] Special opcode 104: advance Address by 7 to 0x1154 and Line by 1 to 15
[0x00000061] Set column to 9
[0x00000063] Extended opcode 4: set Discriminator to 1
[0x00000067] Special opcode 215: advance Address by 15 to 0x1163 and Line by 0 to 15
[0x00000068] Set column to 13
[0x0000006a] Special opcode 48: advance Address by 3 to 0x1166 and Line by 1 to 16
[0x0000006b] Set column to 1
[0x0000006d] Special opcode 48: advance Address by 3 to 0x1169 and Line by 1 to 17
[0x0000006e] Advance PC by 2 to 0x116b
[0x00000070] Extended opcode 1: End of SequenceStructure it and the shape is obvious:
set_column statements carry no row. They only change what the next emitted row will say about the column.set_address at 0x46 opens the sequence at 0x1129. It emits no row by itself.Special opcode emits one row. Nine of them, so nine rows.Advance PC by 2 to 0x116b is DW_LNS_advance_pc, which moves the address but emits nothing. The range from 0x1169 up to 0x116b is therefore still attributed to the line 17 row — the final } covers the last two bytes of main as well as the instructions readelf listed before it.End of Sequence at 0x70 closes it at 0x116b. The sequence's end address is exclusive: the last row runs from 0x1169 to 0x116b.So the sequence covers 0x1129 to 0x116b, which is 0x42 = 66 bytes. Hold onto that number, because the next unit uses it to catch a very common bug.
To map an address A to a source line:
≤ A. That is the row. Its file and line are the answer.Step 2's "last row" and step 1's "must not cross a boundary" are the two rules. Get step 2 wrong by taking the first row at or below A, or by stopping at the row whose address exactly equals A, and you will report the wrong line for most addresses. Get step 1 wrong and you will report lines from an unrelated function.
Run the lookup on real addresses from the disassembly we started with:
| Address | Instruction | Last row ≤ A | Answer |
|---|---|---|---|
| 0x1129 | endbr64 | row at 0x1129 | shape.c8 |
| 0x1131 | mov %edi,-0x4(%rbp) | row at 0x1129 | shape.c8 |
| 0x1137 | mov -0x4(%rbp),%edx | row at 0x1137 | shape.c9 |
| 0x113d | add %edx,%eax | row at 0x1137 | shape.c9 |
| 0x113e | (second byte of that same 2-byte add) | row at 0x1137 | shape.c9 — mid-instruction |
| 0x113f | pop %rbp | row at 0x113f | shape.c10 |
| 0x1154 | p.y = add(p.x,4) | row at 0x1154 | shape.c15 |
| 0x1163 | (second run of line 15) | row at 0x1163 | shape.c15, discriminator 1 |
| 0x116a | (last bytes of main) | row at 0x1169 | shape.c17 |
| 0x2000 | (outside the sequence) | none | no line information |
Three rows in that table earn their keep. 0x113e is in the middle of the add instruction at 0x113d, and it still resolves to line 9 — because rows describe ranges, not instructions. 0x116a is covered by the line 17 row even though no opcode started a row there, because advance_pc moved the address without emitting anything. And 0x2000 is the one that matters: the honest answer is "no line information", and a debugger that answers "line 17" here is producing a confident lie that will send a developer to the wrong file.
And now the trap. The compilation unit in .debug_info declares this function's extent, and here is the relevant pair of attributes, decoded from the real bytes in the header concept's dump:
DW_AT_low_pc : 0x1129
DW_AT_high_pc : 0x420x1129 + 0x42 = 0x116b — which is exactly the end of the sequence. The extent and the line program agree, from two completely different sections.
But look at what DW_AT_high_pcis: the value 0x42 is a length, not an address, because this DIE uses DW_FORM_data8. A reader that assumes high_pc is an address concludes the code ends at address 0x42 — 4,171 bytes into address zero, nowhere near the binary. The DWARF specification allows either an address or a length here, and the form tells you which. This single ambiguity is the most common real bug in DWARF tooling, and it is why you must read the form, not just the value.
Check the two halves of the extent against each other:
$ readelf --debug-dump=rawline shape_v5 | sed -n '/Line Number Statements/,$p' | tail -3
$ readelf --debug-dump=info shape_v5 | sed -n '/DW_AT_low_pc/,/DW_AT_stmt_list/p'What to look for: the line program's last address is 0x116b, and low_pc + high_pc is also 0x116b. Two sections, one fact, independently encoded.
Then confirm the form determines the meaning, by looking at the raw DIE bytes for those two attributes:
$ objcopy --dump-section .debug_info=di.bin shape_v5
$ xxd -s 0x1f -l 16 di.binYou will see 29 11 00 00 00 00 00 00 (8 bytes — DW_FORM_addr) followed by 42 00 00 00 00 00 00 00 (8 bytes — DW_FORM_data8). Same width, completely different meaning. The form byte in the abbrev table is the only thing that tells them apart.
Finally, see what a debugger does with a stripped file, which is the honest "no line information" case:
$ cp shape_v5 s && strip s
$ objdump -dl s | head -5No line annotations at all. That is not a bug — it is the correct output for a file with no line table, and the exact situation the first concept started from.
Without looking back: a line program's rows say address 0x1129 is line 8, and 0x1137 is line 9. A lookup is asked for address 0x1134. Which row applies, and why is that not surprising?
A linker has concatenated four object files into one executable, each contributing its own line program unit. Their code lands at 0x1000, 0x2000, 0x18000 and 0x40000. A lookup for 0x18500 returns "line 41 of a.c". What did the tool do, and which of the two rules from this concept did it break?
The general habit: when a lookup returns a confident answer from the wrong file, suspect the container, not the content. The data inside a table is usually right; the mistake is almost always about which rows you allowed yourself to consider.
Sequence ranges are ELF program header segments in miniature: a start address, an end address, and a rule that you must find the containing range before looking up anything inside it. The DW_AT_low_pc/DW_AT_high_pc pair is the same shape as a segment's p_vaddr and p_vaddr + p_filesz, and suffers from the same address-versus-length ambiguity that PE RVA conversion has to handle.
You have now taken a binary from "here is a program" to "here is exactly which source line each address belongs to", entirely from bytes on disk. That closes Module 2, and it is the whole of the line-number half of DWARF.
What remains is the other half: .debug_info and .debug_abbrev, which describe types and variables rather than lines. That is where a debugger gets int32_t instead of "4 bytes of something", and it is where the abbreviation table's indirection — the one piece of DWARF that is genuinely hard to hold in your head — finally earns its keep.