Your program has a bug at line 15. You attach a debugger, and it stops. But the machine does not know what a "line 15" is — it knows that address 0x1154 holds the bytes 48 8b 45 f8. Between your source text and that byte sequence, the compiler discarded every scrap of information about which statement produced which instruction.
DWARF (Debugging With Attributed Record Formats) is the standard that puts that information back, as data in the file itself. It is not a debugger. It is not a library. It is a file format — the same kind of thing as the ELF header you learned to parse, just describing a different question.
You have already met the other half of this. ELF memory mapping turns file offsets into virtual addresses. DWARF is the table that says what source construct lives at each of those addresses. Debug a program and you are using both at once.
For now, treat a compiled program as three separate things that happen to travel together:
main. The linker keeps these so a crash report can print main+0x1a instead of 0x116b.Also note what the model leaves out: DWARF says nothing about running. The CPU never reads it. A program with its DWARF sections deleted executes identically, just 15 KB smaller. Debug info is strictly for tools.
DWARF is produced by the compiler, stored in sections the linker keeps, and read by debuggers, profilers, crash reporters, and language runtimes. The reference implementation on this machine is GCC 15.2, and the shape.c used throughout this course is:
#include <stdint.h>
struct Point {
int32_t x;
int32_t y;
};
int32_t add(int32_t a, int32_t b) {
return a + b;
}
int main(void) {
struct Point p;
p.x = 3;
p.y = add(p.x, 4);
return p.y;
}Compiled gcc -gdwarf-5 -O0 -o shape_v5 shape.c, it produces six debug sections:
| Section | File offset | Size | Answers the question |
|---|---|---|---|
.debug_info | 0x3066 | 0x110 | What types, functions and variables exist? |
.debug_abbrev | 0x3176 | 0xbf | What shapes do the entries in .debug_info take? |
.debug_line | 0x3235 | 0x73 | Which source line is this address on? |
.debug_line_str | 0x33b9 | 0x5e | File and directory paths |
.debug_str | 0x32a8 | 0x111 | Every other string (type names, producers, variables) |
.debug_aranges | 0x3036 | 0x30 | Which address ranges belong to which CU? |
Every one of them has Addr 0 and ES 0 in the section header table. A debug section is never mapped into memory. It is file content that tools read from disk, which is exactly why you can strip it without changing behaviour.
Here is the payoff, on the real binary. This is objdump -d — pure disassembly, no source, no line numbers. This is what the machine actually has:
0000000000001129 <add>:
1129: f3 0f 1e fa endbr64
112d: 55 push %rbp
112e: 48 89 e5 mov %rsp,%rbp
1131: 89 7d fc mov %edi,-0x4(%rbp)
1134: 89 75 f8 mov %esi,-0x8(%rbp)
1137: 8b 55 fc mov -0x4(%rbp),%edx
113a: 8b 45 f8 mov -0x8(%rbp),%eax
113d: 01 d0 add %edx,%eax
113f: 5d pop %rbp
1140: c3 retNow add -l. objdump reads the DWARF line table and interleaves the source positions. Same bytes, same addresses, and now every instruction is attributed:
0000000000001129 <add>:
add():
/tmp/opencode/dwarf-research/shape.c:8
1129: f3 0f 1e fa endbr64
112d: 55 push %rbp
112e: 48 89 e5 mov %rsp,%rbp
1131: 89 7d fc mov %edi,-0x4(%rbp)
1134: 89 75 f8 mov %esi,-0x8(%rbp)
/tmp/opencode/dwarf-research/shape.c:9
1137: 8b 55 fc mov -0x4(%rbp),%edx
113a: 8b 45 f8 mov -0x8(%rbp),%eax
113d: 01 d0 add %edx,%eax
/tmp/opencode/dwarf-research/shape.c:10
113f: 5d pop %rbp
1140: c3 retThe whole difference between these two outputs is 115 bytes of .debug_line (plus the tables it references). 0x113d is an add %edx,%eax instruction. Because the line table says address 0x1137 is on line 9, and a line row stays current until the next row, objdump can say that 0x113d is also line 9 — and line 9 is return a + b;. It did not disassemble anything clever. It just did a lookup.
Notice line 8 begins at 0x1129, the very first instruction of add, and line 10 begins at 0x113f, the pop — the closing brace. The compiler's prologue and epilogue are attributed to the braces that produced them.
Build the reference binary. This is the exact command used to produce every hex dump in this course:
$ gcc -gdwarf-5 -O0 -o shape_v5 shape.c
$ objdump -d shape_v5 | sed -n '/<add>:/,/^$/p'
$ objdump -dl shape_v5 | sed -n '/<add>:/,/^$/p'Then watch the debug info disappear:
$ cp shape_v5 shape_stripped
$ strip shape_stripped
$ objdump -l shape_strippedWhat to look for: the second command prints nothing for add. The disassembly is still there — objdump -d still works — but every line-number annotation is gone, because strip removed .debug_*. That is the whole subject of this course in one experiment.
Also try readelf -S shape_v5 | grep debug and compare against readelf -S shape_stripped | grep debug.
Without looking back: a program runs correctly after you strip it. What does that tell you about whether the CPU reads the debug sections?
You are handed a core dump from a stripped production binary. The report says crash at 0x4005a1, in ???. Which single fact decides whether you can recover the source line, and why is it not something you can fix by re-running the binary?
The general habit: before shipping a binary, ask which of its debug sections a future you will actually need. The usual answer is that you cannot re-create them later.
Two things established here carry through the rest of the course. First, the mental model: code, symbols, debug info are separate layers, and only symbols survive a normal strip. Second, the mechanism: an address-to-line table is a lookup, not an inference — the whole of Module 2 is the byte layout of that lookup table.
Next: The .debug_* Sections — which sections exist, who reads each one, and why the linker usually throws half of them away.