Back to course

Why DWARF Exists

14 minbeginnerModule 1: Why Debug Info Exists
14 min · Module 1: Why Debug Info Exists · Orientation

Why This Matters

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.

A Simple Model

For now, treat a compiled program as three separate things that happen to travel together:

  1. Code. Addresses and instructions. The CPU needs this and nothing else.
  2. Symbols. A name for a few addresses, like main. The linker keeps these so a crash report can print main+0x1a instead of 0x116b.
  3. Debug info. A description of types, variables, lines, and scopes, addressed by the same numbers.
This model is wrong in one important way, and knowing how is the point of this course. Debug info is not a tidy third list. It is a set of interlocking tables that each point at the others, and following a single source line can walk you through four of them. We build the real model in Module 2.

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.

The Actual Detail

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:

SectionFile offsetSizeAnswers the question
.debug_info0x30660x110What types, functions and variables exist?
.debug_abbrev0x31760xbfWhat shapes do the entries in .debug_info take?
.debug_line0x32350x73Which source line is this address on?
.debug_line_str0x33b90x5eFile and directory paths
.debug_str0x32a80x111Every other string (type names, producers, variables)
.debug_aranges0x30360x30Which 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.

A Real Example

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                   	ret

Now 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                   	ret

The 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.

Try It Yourself

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_stripped

What 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.

Check Your Understanding

Without looking back: a program runs correctly after you strip it. What does that tell you about whether the CPU reads the debug sections?

Apply It

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.

Connect

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.