Skip to content

Lesson 24.4 — Debug information: DWARF, DIBuilder metadata and debug records

Techniques: DWARF — the format debuggers read: a tree of debugging information entries (DIEs) describing scopes, types and variables, a line-number program mapping addresses to source lines, and a stack-machine expression language for where a variable is at each address; DIBuilder metadata — how a front end describes the same things in LLVM IR (DICompileUnit, DISubprogram, DILocalVariable, DILocation), from which the back end emits DWARF; debug records — #dbg_declare and #dbg_value, the non-instruction annotations that say which IR value holds a source variable at each point, surviving mem2reg and the optimizer, and producing location lists · Pebble implements: ★ pebble-debugify (E3): synthetic lines, real variable names, #dbg_declare/#dbg_value records; gdb then prints Pebble's variables · Drill: dwarf-location · Prerequisites: Lesson 9.6 (metadata, debug records as IR), Lesson 16.4 (what mem2reg does to #dbg_declare), Lesson 22.9 (where values live after allocation) · Time: 5 hours

Set a breakpoint on line 15 of prog-sieve.pbl, run, and ask gdb for count. Three things must exist for the answer to appear: a table from machine addresses to source lines (so that "line 15" is an address), a description of the variable count with its type, and a description of where count is at that address, which after register allocation is "in %rbx from here to there, then in the stack slot at rbp - 24, then nowhere". DWARF is the format of those three things. LLVM builds them from metadata the front end attaches with DIBuilder, and the middle of the pipeline keeps the "where" alive through every transformation with debug records. Pebble's code generator emits none of this (Chapter 11 kept it out of the way of the lowering), so this lesson's lab adds it after the fact: a pass that gives every instruction a line, every named alloca a #dbg_declare and every named SSA value a #dbg_value, which is enough for gdb to print count = 0 at the breakpoint and last after next.

1. Problem and motivation

DWARF

Debuggers need a map from the machine's state back to the source's names. DWARF (originally 1988 for Unix System V; DWARF 5 [DWARF5] in 2017) is that map as data in the object file: .debug_info holds a tree of DIEs (a compile unit contains subprograms, which contain variables, each with a type); .debug_line holds a compressed program that, when run, produces the (address, file, line, column, is_stmt) table; .debug_loclists holds, for each variable whose location changes, a list of address ranges with a location expression each. The expressions are programs for a small stack machine (DW_OP_fbreg -24, DW_OP_reg3, DW_OP_deref, DW_OP_stack_value), which is what lets the format describe a variable that lives in a register, in memory reached through two pointers, or as a computed constant. Every Unix compiler emits DWARF; gdb and lldb consume it; LLVM's llvm-dwarfdump prints it.

DIBuilder metadata

A front end does not write DWARF; it describes its program's scopes, types, variables and source positions in LLVM IR metadata, and the back end (DwarfDebug in the AsmPrinter) turns the metadata into DWARF sections for the target. DIBuilder [LLVM-SourceLevelDebugging] is the API: createCompileUnit, createFunction (a DISubprogram), createAutoVariable, createBasicType, and the DILocation attached to each instruction as its !dbg operand. Clang, rustc, Swift and Flang all go through it; pebble-debugify does too, which is why its output is DWARF that gdb reads without knowing that Pebble exists.

Debug records

Positions and types are static; where a variable's value is changes with every optimization. LLVM tracks it with debug records (Lesson 9.6): #dbg_declare(ptr %count.addr, !var, !expr, !loc) says the variable lives at that address for the whole function (the -O0 situation), and #dbg_value(i64 %count.next, !var, !expr, !loc) says that from this point the variable's value is that SSA value. mem2reg converts declares into values at every store and phi (Lesson 16.4); later passes move, delete and merge instructions and must keep the records truthful (a deleted value becomes #dbg_value(poison, ...): "optimized out"). The back end's LiveDebugValues turns the records, after register allocation, into the address ranges of the location lists. Until LLVM 19 the records were call void @llvm.dbg.value(...) instructions, which made passes behave differently with -g; the record form fixed that (Lesson 9.6, Proposition 9.6.11).

2. Definitions and algorithms

Definition 24.4.1 (DIE, compile unit tree)

A debugging information entry (DIE) is a pair \((\mathrm{tag}, \mathrm{attrs})\) with a tag from the DWARF vocabulary (DW_TAG_compile_unit, DW_TAG_subprogram, DW_TAG_variable, DW_TAG_base_type, ...) and a finite map from attribute names (DW_AT_name, DW_AT_type, DW_AT_location, DW_AT_low_pc, ...) to values, which may be references to other DIEs. The DIEs of one compilation form a tree rooted at a compile unit; a variable's DIE is a child of the scope (subprogram or lexical block) that declares it.

Definition 24.4.2 (Line table)

The line table of a compile unit is a finite set of rows \((a, f, \ell, c, \mathrm{is\_stmt})\): address \(a\) is the first address of code generated for file \(f\), line \(\ell\), column \(c\); is_stmt marks rows a debugger may stop at. The table is encoded as a program for a line-number state machine (DW_LNS_advance_pc, DW_LNS_advance_line, special opcodes that do both in one byte); running the program produces the rows. A breakpoint on line \(\ell\) is the smallest address of a row with line \(\ell\) and is_stmt.

Definition 24.4.3 (Location expression)

A location expression is a sequence of operations for a stack machine over integers, evaluated with a frame base \(\mathrm{fb}\) and register contents \(R\); it denotes a location: a memory address (the value on top of the stack at the end), a register (DW_OP_regN alone), or a value (DW_OP_stack_value: the top of the stack is the variable's value). The operations used in this chapter:

operation effect
DW_OP_fbreg N push \(\mathrm{fb} + N\)
DW_OP_bregN M push \(R[N] + M\)
DW_OP_regN the location is register \(N\) (no stack)
DW_OP_constu N, DW_OP_litN push \(N\)
DW_OP_plus, DW_OP_plus_uconst N pop two and push their sum; add \(N\) to the top
DW_OP_deref pop an address and push the word it holds
DW_OP_stack_value the top of the stack is the value, not an address

Definition 24.4.4 (Location list)

The location list \(\mathrm{LL}(v)\) of a variable \(v\) is a finite set of pairs \(([a_1, a_2), e)\): within the half-open address range, \(v\)'s location is the expression \(e\). Ranges do not overlap; an address in no range means \(v\) has no location there ("optimized out"). A variable with one location for its whole scope uses a single expression (DW_AT_location as an exprloc) instead of a list.

The three artifacts for count in the sieve

DIE: DW_TAG_variable {DW_AT_name "count", DW_AT_type → int, DW_AT_location DW_OP_fbreg +56}; line-table row: 0x18 → line 16, is_stmt; location list: not needed at -O0 (one frame slot for the whole function), needed at -O2 (the box under debug records: [0x1a, 0x1d): DW_OP_consts +0, DW_OP_stack_value).

DWARF

Algorithm 24.4.5 (Evaluating a location expression; the drill's oracle)

  • Input: an expression \(e = \langle o_1, \dots, o_k \rangle\), the frame base \(\mathrm{fb}\), registers \(R\), memory \(M\).
  • Output: a location: memory a, register N, or value x.
  • Precondition: every DW_OP_deref, DW_OP_plus has enough operands on the stack; DW_OP_regN, if present, is the whole expression.
  • Postcondition: the returned location is the one Definition 24.4.3 denotes.
  • Invariant: after \(o_i\), the stack holds the values the operations \(o_1..o_i\) define, and value is set iff a DW_OP_stack_value was seen.
function Evaluate(e, fb, R, M):
    stack ← []; reg ← none; value ← false
    for o in e:
        match o:
            DW_OP_fbreg N:        push(fb + N)
            DW_OP_bregN M:        push(R[N] + M)
            DW_OP_regN:           reg ← N
            DW_OP_constu N | DW_OP_litN: push(N)
            DW_OP_plus:           b ← pop(); a ← pop(); push(a + b)
            DW_OP_plus_uconst N:  push(pop() + N)
            DW_OP_deref:          push(M[pop()])
            DW_OP_stack_value:    value ← true
    if reg ≠ none: return register reg
    if value:      return value top(stack)
    return memory top(stack)

What clang -g -O2 leaves for total: the DIE, the line table and the location list

Reproduce (clang 23.1.2, llvm-dwarfdump 23.1.2, x86-64):

cat > dbg.c <<'EOF'
int scale(int n) {
  int total = 0;
  for (int i = 0; i < n; i++)
    total += i * 3;
  return total;
}
EOF
clang-23 -g -O2 -c dbg.c -o dbg-O2.o
llvm-dwarfdump --debug-info dbg-O2.o | grep -B1 -A4 'DW_AT_name ("total")'
llvm-dwarfdump --debug-line dbg-O2.o | sed -n '/^Address/,$p'
llvm-dwarfdump --debug-loclists dbg-O2.o | grep -A2 'DW_LLE_offset_pair' | head -3

Output (the DIE's attributes and the line table complete; the file path shortened to dbg.c):

                     [0x000000000000001a, 0x000000000000001d): DW_OP_consts +0, DW_OP_stack_value)
                  DW_AT_name    ("total")
                  DW_AT_decl_file   ("dbg.c")
                  DW_AT_decl_line   (2)
                  DW_AT_type    (0x0000005a "int")
Address            Line   Column File   ISA Discriminator OpIndex Flags
------------------ ------ ------ ------ --- ------------- ------- -------------
0x0000000000000000      3     21      0   0             0       0  is_stmt prologue_end
0x0000000000000002      3      3      0   0             0       0  is_stmt
0x0000000000000019      5      3      0   0             0       0  is_stmt
0x000000000000001a      0      3      0   0             0       0
0x000000000000001c      5      3      0   0             0       0  is_stmt
0x000000000000001d      5      3      0   0             0       0  is_stmt end_sequence
            DW_LLE_offset_pair     (0x0000000000000000, 0x0000000000000004): DW_OP_consts +0, DW_OP_stack_value
            DW_LLE_offset_pair     (0x000000000000001a, 0x000000000000001d): DW_OP_consts +0, DW_OP_stack_value

What to notice: at -O2 the loop was turned into a closed form and total has no storage: its location list says "the constant 0" for the addresses before the loop and after it, and nothing in between (optimized out while the closed-form arithmetic runs). Line 4 (total += i * 3) has no row at all: no instruction belongs to it any more; line 0 at 0x1a marks compiler-generated code with no source line. This is Definition 24.4.4 with two ranges and a gap, and the reason gdb says <optimized out> in optimized builds.

DIBuilder metadata

Algorithm 24.4.6 (Synthetic debug info for a module: pebble-debugify)

  • Input: a module \(M\) without debug info, from pebblec (allocas named after Pebble variables, SSA values named x.addr.phiN by mem2reg).
  • Output: \(M\) with a DICompileUnit, a DISubprogram per defined function, a DILocation per instruction (a fresh line each), a DILocalVariable per (function, source name), a #dbg_declare per named alloca and a #dbg_value after every named scalar definition; the Debug Info Version and Dwarf Version module flags.
  • Precondition: \(M\) has no llvm.dbg.cu (the pass is idempotent: it returns at once otherwise).
  • Postcondition: \(M\) verifies; llc emits a line table with one row per instruction that produces machine code (allocas and folded branches produce none) and a DW_TAG_variable per named local (Proposition 24.4.9); behavior is unchanged (records are not instructions).
  • Invariant: every DILocation created so far belongs to the DISubprogram of the function that holds its instruction (the verifier's scope rule), and every DILocalVariable is retained by its subprogram.
function Debugify(M):
    DIB ← DIBuilder(M); file ← DIB.createFile(M.sourceFileName)
    CU ← DIB.createCompileUnit(DW_LANG_C, file, "pebblec")
    line ← 1
    for each defined function F in M:
        SP ← DIB.createFunction(CU, F.name, F.name, file, line, subroutineType(F), line, Prototyped, Definition)
        F.subprogram ← SP
        for each instruction I in F:  I.debugLoc ← DILocation(line, 1, SP); line ← line + 1
        vars ← {}                                        # source name ↦ DILocalVariable
        for each instruction I in F:
            if I is an alloca named "<v>.addr" or "<v>":  # Pebble variables; temporaries "_N" are skipped
                insert #dbg_declare(I, var(v, I.allocatedType), empty expression, I.debugLoc) after I
            else if I is a named scalar definition "<v>.…":
                insert #dbg_value(I, var(v, I.type), empty expression, I.debugLoc) after I
                                                          # for a phi: after the block's phis
    DIB.finalize(); M.flags += {"Debug Info Version" ↦ 3, "Dwarf Version" ↦ 5}

function var(v, type):  vars[v] if present, else vars[v] ← DIB.createAutoVariable(SP, v, file, line, TypeOf(type))

TypeOf maps i1/i8 to bool, iN to int, double to float, ptr to a pointer to an unspecified type, [N x T] to an array type (createArrayType with one subrange), anything else to an unspecified type.

gdb prints Pebble's variables from the synthetic debug info

Reproduce (pebblec and the course plugin from a -DPEBBLE_USE_SOLUTION=all build, opt 23.1.2, GNU gdb 15.1; SOL as in Lesson 24.1):

$SOL/bin/pebblec -O0 --emit=llvm tests/ch11/e2e/prog-sieve.pbl -o sieve.ll
opt -load-pass-plugin=$SOL/lib/PebblePasses.so -passes=pebble-debugify -S sieve.ll -o sieve-dbg.ll
$SOL/bin/pebblec --from-llvm sieve-dbg.ll -O0 -o sieve-dbg
gdb -q -batch -ex 'break pebble_main' -ex run -ex 'next 6' -ex 'info locals' -ex 'print last' -ex 'info line' ./sieve-dbg 2>&1 | grep -v '^\[\|^warning\|libthread_db\|^$'

Output (complete):

Breakpoint 1 at 0x11d7: file tests/ch11/e2e/prog-sieve.pbl, line 15.
Breakpoint 1, pebble_main () at tests/ch11/e2e/prog-sieve.pbl:15
15              composite[j] = true;
composite = {false <repeats 100 times>}
count = 0
last = 0
i = 2
j = 49152
$1 = 0
Line 23 of "tests/ch11/e2e/prog-sieve.pbl" starts at address 0x555555555211 <pebble_main+65> and ends at 0x555555555219 <pebble_main+73>.

What to notice: every Pebble variable of main is there with its type: composite as a 100-element array of bool, the integers as int. The line numbers are the pass's synthetic lines (one per LLVM instruction), so "line 15" shows the 15th line of the source file, which happens to be composite[j] = true;, and next 6 advanced six instructions, not six statements. j = 49152 is an uninitialized slot read before its first store (the program has not reached the inner loop). Replacing the synthetic lines by PIR's real @line:col is the natural extension: the code generator would attach DILocations during lowering (Chapter 11 E5), and this pass would only add the variables.

Debug records

Algorithm 24.4.7 (From debug records to location lists, after LiveDebugValues)

  • Input: a machine function after register allocation, with DBG_VALUE instructions (the lowered #dbg_value records: a variable and a register, stack slot or constant) in program order.
  • Output: for each variable \(v\), the location list \(\mathrm{LL}(v)\).
  • Precondition: every DBG_VALUE is placed where its value becomes the variable's value; register clobbers (calls, redefinitions) and spills/reloads are visible as instructions.
  • Postcondition: at every address \(a\) in a range of \(\mathrm{LL}(v)\), the location holds \(v\)'s value as the debug records defined it (Theorem 24.4.10); addresses in no range have no reliable location.
  • Invariant: the current location of \(v\) is the last DBG_VALUE for \(v\) whose location has not been clobbered, transferred through spills and reloads.
function LocationLists(MF):
    for each variable v: cur[v] ← none; start[v] ← 0; LL[v] ← []
    for each instruction I at address a, in layout order:        # a dataflow over blocks in the real pass
        if I is DBG_VALUE v, loc:                 emit(v, a); cur[v] ← loc; forget v's slot   # a new value: the old spill is stale
        else if I is a call:                      for each v with cur[v] in caller-saved registers:
                                                      emit(v, a + size(I)); cur[v] ← spill slot of v if one exists else none
        else if I defines register r:             for each v with cur[v] = r: emit(v, a); cur[v] ← none
        else if I is a spill of r to slot s:      for each v with cur[v] = r: remember slot s for v
        else if I is a reload of slot s into r:   for each v with slot s: emit(v, a); cur[v] ← r
    for each v: emit(v, end)
    return LL

function emit(v, a):  if cur[v] ≠ none and a > start[v]: LL[v].append(([start[v], a), cur[v]));  start[v] ← a

Declares become values under mem2reg, and values become ranges

Reproduce (clang 23.1.2, opt 23.1.2; dbg.c from the DWARF box):

clang-23 -g -O0 -Xclang -disable-O0-optnone -fno-discard-value-names -S -emit-llvm dbg.c -o dbg-O0.ll
grep -n '#dbg_' dbg-O0.ll
opt -passes=mem2reg -S dbg-O0.ll -o dbg-m2r.ll && grep -n '#dbg_' dbg-m2r.ll

Output (complete):

13:    #dbg_declare(ptr %n.addr, !14, !DIExpression(), !15)
14:    #dbg_declare(ptr %total, !16, !DIExpression(), !17)
16:    #dbg_declare(ptr %i, !18, !DIExpression(), !20)
9:    #dbg_value(i32 %n, !14, !DIExpression(), !15)
10:    #dbg_value(i32 0, !16, !DIExpression(), !15)
11:    #dbg_value(i32 0, !17, !DIExpression(), !19)
17:    #dbg_value(i32 %i.0, !17, !DIExpression(), !19)
18:    #dbg_value(i32 %total.0, !16, !DIExpression(), !15)
25:    #dbg_value(i32 %add, !16, !DIExpression(), !15)
30:    #dbg_value(i32 %inc, !17, !DIExpression(), !19)

What to notice: at -O0 each variable is one #dbg_declare on its alloca: a single location for the whole function. mem2reg deletes the allocas and replaces each declare by a #dbg_value at every store (0 for the initializers, %add and %inc for the updates) and at every phi (%i.0, %total.0): from now on the variable's location is a sequence of SSA values over program points, which is what Algorithm 24.4.7 turns into the ranges of the -O2 box once those values have registers. The -O2 list for total had only the constant ranges because the optimizer proved the loop's closed form and the %add values disappeared with it: #dbg_value(poison, ...), then no range.

3. Worked example

Running example (used for every technique in this lesson): the variable x in a straight-line function of 8 instructions, with the events

instruction event
0 x defined into rax
1 x spilled to the frame slot fp-16 (also stays in rax)
2 call f (caller-saved registers clobbered after it)
3 x reloaded from fp-16 into rcx
4 unrelated
5 x redefined (a new value) into rdx
6 call g
7 unrelated
flowchart LR
  I0[0: def rax] --> I1[1: spill fp-16] --> I2[2: call f] --> I3[3: reload rcx] --> I4[4] --> I5[5: def rdx] --> I6[6: call g] --> I7[7]

DWARF

Evaluate the expression DW_OP_fbreg -16; DW_OP_deref; DW_OP_plus_uconst 8 with \(\mathrm{fb} = \mathtt{0x7ffc0000}\) and \(M[\mathtt{0x7ffbfff0}] = \mathtt{0x2000}\) (Algorithm 24.4.5):

op stack flag
DW_OP_fbreg -16 [0x7ffbfff0]
DW_OP_deref [0x2000]
DW_OP_plus_uconst 8 [0x2008]
end → memory 0x2008

The variable lives at address 0x2008: it is a field at offset 8 of an object whose address is stored in the frame slot; this is how a Pebble &mut Body parameter's field would be described. With a trailing DW_OP_stack_value the answer would be value 0x2008 instead: the variable is the pointer.

DIBuilder metadata

Algorithm 24.4.6 on the sieve's main (the box in §2): 1 compile unit, 1 subprogram, 121 locations (one per instruction of the -O0 module), 5 variables (composite, count, last, i, j; the temporaries _4…_12 are skipped), 5 #dbg_declare records and, since -O0 has no named SSA scalars, 0 #dbg_values. After opt -passes=pebble-debugify,mem2reg the declares become values as in the records box.

Debug records

Algorithm 24.4.7 on the running example (the dwarf-location drill's part B, seed-free):

instr event cur[x] after emitted range
0 def rax reg rax –
1 spill reg rax (slot remembered) –
2 call f fp-16 (rax clobbered after the call; the slot survives) [0, 3): reg rax
3 reload rcx reg rcx [3, 3): nothing (empty)
4 – reg rcx –
5 def rdx reg rdx (the old spill is stale: a new value) [3, 5): reg rcx
6 call g none (no slot for the new value) [5, 7): reg rdx
7 – none –
end [7, 8): <optimized out>

\(\mathrm{LL}(x) = \{[0,3) \mapsto \mathtt{rax}, [3,5) \mapsto \mathtt{rcx}, [5,7) \mapsto \mathtt{rdx}\}\), and no location on \([7, 8)\). Note that the call at 2 clobbers from instruction 3, and that the reload at 3 makes the register authoritative again, so the fp-16 range is empty and does not appear; a debugger stopped at instruction 2 itself (before the call executes) still reads rax.

Try it

./course drill dwarf-location --seed 5 --difficulty hard, then --solution: part A is Algorithm 24.4.5, part B is Algorithm 24.4.7 on random events.

4. Invariants and correctness

DWARF

Theorem 24.4.8 (Expressiveness of location expressions)

Every location of the form "register \(N\)", "memory at \(\mathrm{fb} + c\)", "memory at \(R[N] + c\)", "memory at the address stored in any such location plus \(c\)" (to any depth), and "the constant \(k\)" is denoted by an expression of Definition 24.4.3, and Algorithm 24.4.5 computes it in \(O(\lvert e \rvert)\) steps.

Proof

By induction on the structure of the location. Registers: DW_OP_regN. Frame or register offsets: DW_OP_fbreg c or DW_OP_bregN c. Indirection: if \(e\) denotes memory at \(a\), then \(e \cdot\) DW_OP_deref DW_OP_plus_uconst c denotes memory at \(M[a] + c\) (Algorithm 24.4.5: the stack top becomes \(M[a]\), then \(M[a] + c\)). Constants: DW_OP_constu k DW_OP_stack_value. Each operation runs in constant time and the stack depth is bounded by the number of pushes. Where it stops: a value split across two registers or partly optimized out needs DW_OP_piece, and a value that is a function of two locations (a variable equal to a + b after reassociation) needs the arithmetic operations with DW_OP_stack_value; DWARF has both, LLVM emits DW_OP_piece for fragments and DW_OP_LLVM_arg-based expressions for salvaged arithmetic, and the drill's subset omits them.

DIBuilder metadata

Proposition 24.4.9 (Well-formedness of the synthetic metadata)

The output of Algorithm 24.4.6 passes the IR verifier's debug-info rules, and running the algorithm twice equals running it once.

Proof

The verifier requires: every DILocation on an instruction of \(F\) has a scope whose subprogram is \(F\)'s (the invariant of the algorithm: locations are created with SP of the current function); every DILocalVariable used by a record has a scope under a subprogram of the compile unit (created with SP and retained by finalize); every function with a subprogram has that subprogram marked as a definition and unique (createFunction with SPFlagDefinition, one per function); the Debug Info Version flag is present. All hold by construction. Idempotence: the pass returns immediately when llvm.dbg.cu exists, which the first run creates. The lit test debugify.ll checks both (opt -passes=verify and the diff of the two runs).

Debug records

Theorem 24.4.10 (Soundness of location lists)

Assume every DBG_VALUE v, loc is placed at the point where loc holds \(v\)'s value (the records were maintained truthfully by every pass), spills and reloads are of the register that holds \(v\), and calls clobber exactly the caller-saved registers. Then at every address \(a\) inside a range \(([a_1, a_2), e)\) of the list produced by Algorithm 24.4.7, evaluating \(e\) yields \(v\)'s value.

Proof

By induction over the instructions in order. Initialization: before any DBG_VALUE for \(v\), no range is open (\(\mathrm{cur}[v] = \mathrm{none}\)), so nothing is claimed. Maintenance: consider the open range with location \(\mathrm{cur}[v]\) at instruction \(I\). If \(I\) is a DBG_VALUE for \(v\), the old range is closed at \(I\) (its claim held up to \(I\) by the induction hypothesis) and the new location holds \(v\) by assumption. If \(I\) defines the register \(\mathrm{cur}[v]\), the range is closed at \(I\): the claim held before \(I\) and is not made after. If \(I\) is a call and \(\mathrm{cur}[v]\) is caller-saved, the range is closed after the call; the slot, if one was remembered, holds the value that was spilled, which was \(v\)'s value at the spill and has not been redefined since (a redefinition resets the remembered slot, as instruction 5 of the running example shows). A reload copies the slot's value, so the new register range is truthful. Any other instruction leaves both the location and the value unchanged. Termination: one pass over the instructions. The real LiveDebugValues is this argument as a forward dataflow over the CFG (join = intersection of locations at block entries), because a location that differs on two incoming edges is no location.

When it breaks: a pass that moves an instruction after its #dbg_value without updating the record (the value is then claimed before it exists), or that deletes a value and leaves the record pointing at a replacement that is not equal (the classic "wrong value in the debugger" bug). LLVM's debugify utility (-passes=debugify,<pass>,check-debugify) tests passes for exactly these: it is pebble-debugify plus a checker.

5. Complexity

\(n\) = instructions, \(v\) = variables, \(r\) = debug records, \(s\) = rows of the line table, \(\lvert e \rvert\) = the length of an expression, \(L\) = total ranges over all variables.

Technique Time Space Justification
DWARF (emission and reading) emit: \(O(\text{DIEs} + s + L)\); evaluate an expression: \(O(\lvert e \rvert)\) (Theorem 24.4.8); find a line's address: \(O(s)\) or \(O(\log s)\) with an index .debug_info + .debug_line + .debug_loclists: typically 2–5× the code size at -g each DIE, row and range is written once; the line program is a run-length encoding of the rows
DIBuilder metadata \(O(n + v)\) for pebble-debugify; the same order for a front end one DILocation per distinct (line, column, scope), uniqued; one variable node per variable Algorithm 24.4.6 visits each instruction twice
Debug records → location lists \(O(n \cdot v)\) worst case for the dataflow (each instruction may transfer every variable), \(O(n + r)\) typical \(L \le r + \text{clobbers}\) ranges Algorithm 24.4.7: each event closes or opens at most one range per variable

Pathological family (location lists). A function with \(v\) live variables in registers and \(c\) calls: every call clobbers every register, so each variable's list has \(\Theta(c)\) ranges and the lists total \(\Theta(v \cdot c)\), larger than the code itself; this is why LiveDebugValues has per-function budgets (-livedebugvalues-input-bb-limit, -livedebugvalues-input-dbg-value-limit) and why -O2 -g objects are several times larger than -O2 ones. At scale: the -O0 sieve module of 121 instructions gets 121 DILocations from pebble-debugify and 75 line-table rows from llc -O0 (llvm-dwarfdump --debug-line; the allocas and the branches that fold into a compare produce no code of their own; a real front end emits one location per statement, ~20 here), and clang -g -O2 on dbg.c produced 6 rows and a 2-range list for total (the boxes).

6. Variants and refinements

DWARF

  • DWARF 5 vs 4 [DWARF5]: .debug_loclists/.debug_rnglists with offset pairs and base addresses (compact) replace .debug_loc/.debug_ranges; .debug_names is a standard index (the gdb warning in the box says gdb prefers its own); trade-off: older tools.
  • Split DWARF (-gsplit-dwarf, .dwo files): the bulk of the debug info leaves the object, the linker sees only skeletons; trade-off: a second file per object and a dwp packaging step.
  • CodeView / PDB (Windows): the same information in Microsoft's format, emitted by CodeViewDebug in the AsmPrinter; trade-off: one back end, two emitters.
  • Type units and -fdebug-types-section: deduplicate identical type DIEs across objects by hashing; trade-off: the linker must merge them.

DIBuilder metadata

  • Real source positions from PIR (the natural extension of E3): attach DILocation(line, col) from @line:col during lowering, then one row per Pebble statement instead of per instruction; trade-off: an edit to the code generator's contract (Chapter 11 E5).
  • Assignment tracking (#dbg_assign, Lesson 9.6): records linked to stores, so variable locations survive SROA and DSE with less loss; trade-off: more metadata per store.
  • Key instructions (UseKeyInstructions in createFunction, LLVM 20+): mark which instructions carry a statement's "key" effect, so is_stmt lands on the right rows in optimized code; trade-off: front-end annotation work.
  • -gmlt (line tables only): no variables, small objects, enough for symbolized stack traces; trade-off: no info locals.

Debug records

  • The intrinsic form (llvm.dbg.value calls, before LLVM 19) [LLVM-RemoveDIs]: the same information as instructions; trade-off: passes that count or scan instructions behaved differently under -g (Lesson 9.6, Proposition 9.6.11).
  • Salvaging (salvageDebugInfo, llvm/lib/Transforms/Utils/Local.cpp): when a value is deleted, rewrite the record's expression to recompute it from surviving values (DW_OP_plus_uconst, DW_OP_LLVM_arg); trade-off: expression size, and only for pure operations.
  • Instruction-referencing LiveDebugValues (InstrRefBasedImpl.cpp, the default on x86-64): records refer to the instruction that defines the value rather than to a register, so register allocation cannot lose the link; trade-off: a more complex dataflow (value tables per block).
  • Fragments (DW_OP_LLVM_fragment): one variable split over several values after SROA; trade-off: DW_OP_piece in the output and partial "optimized out".

7. In real compilers

DWARF

LLVM

llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp — DwarfDebug::beginFunctionImpl, collectEntityInfo, buildLocationList (LLVM 23.1.2) [LLVM-DwarfDebug]: builds the DIEs and location lists from the machine function's debug values; llvm/lib/CodeGen/AsmPrinter/DwarfCompileUnit.cpp — constructVariableDIE, constructSubprogramScopeDIE; llvm/lib/CodeGen/AsmPrinter/DwarfExpression.cpp — DwarfExpression::addExpression, addMachineRegExpression (Definition 24.4.3's operations from DIExpressions). The line table is emitted by the MC layer (MCDwarfLineTable) from .loc directives.

  • GCC gcc/dwarf2out.cc — add_location_or_const_value_attribute, dw_loc_list, loc_list_from_tree (GCC 15) [GCC-Dwarf2out]: the same construction from GCC's var_loc_list notes.
  • The standard [DWARF5]: §2.5 (location expressions), §2.6 (location lists), §6.2 (the line-number program).

Find where LLVM does it. In llvm/lib/CodeGen/AsmPrinter/DwarfDebug.cpp, find DwarfDebug::buildLocationList. Question: what is the function's boolean result, and what does collectEntityInfo do with a variable whose list "merged to one location"?

DIBuilder metadata

LLVM

llvm/lib/IR/DIBuilder.cpp — DIBuilder::createAutoVariable, insertDeclare, insertDbgValueIntrinsic (the name is historical: it inserts a record), finalize (LLVM 23.1.2) [LLVM-DIBuilder]; the utility this lab reimplements is llvm/lib/Transforms/Utils/Debugify.cpp — applyDebugifyMetadata (synthetic lines and variables, plus check-debugify to detect loss).

  • Clang clang/lib/CodeGen/CGDebugInfo.cpp — CGDebugInfo::EmitDeclare, EmitFunctionStart: the front end's use of the same calls.
  • rustc compiler/rustc_codegen_ssa/src/mir/debuginfo.rs — debug_introduce_local (rustc 1.94) [Rust-Debuginfo]: MIR locals to DILocalVariables through rustc_codegen_llvm/src/debuginfo/.
  • Pebble solutions/pebble/lib/Passes/DebugInfo/Debugify.cpp — the reference E3.

Find where LLVM does it. In llvm/lib/IR/DIBuilder.cpp, find DIBuilder::finalize. Question: what does it do with the retainedNodes of each subprogram, and why must a pass call it before the module is printed? (Quiz find-dibuilder-finalize.)

Debug records

LLVM

llvm/lib/IR/DebugProgramInstruction.cpp — DbgVariableRecord (LLVM 23.1.2) [LLVM-DbgRecords]; llvm/lib/Transforms/Utils/PromoteMemoryToRegister.cpp — ConvertDebugDeclareToDebugValue (the records box); llvm/lib/CodeGen/LiveDebugValues/VarLocBasedImpl.cpp — VarLocBasedLDV::transferDebugValue, transferRegisterDef (Algorithm 24.4.7 as a dataflow) [LLVM-LiveDebugValues]; InstrRefBasedImpl.cpp is the instruction-referencing variant.

  • GCC gcc/var-tracking.cc: the equivalent dataflow over RTL VAR_LOCATION notes.
  • rustc emits #dbg_declare/#dbg_value through the same DIBuilder (above); Swift's IRGen likewise (lib/IRGen/IRGenDebugInfo.cpp).

Find where LLVM does it. In llvm/lib/CodeGen/LiveDebugValues/VarLocBasedImpl.cpp, find VarLocBasedLDV::transferRegisterDef. Question: what does it do to the open locations of a register that a call instruction clobbers, and which register set does it consult?

8. Comparison

Technique Power / precision Speed Output / error quality Implementation effort Typical use
DWARF describes any variable location (expression language, Theorem 24.4.8) tables read lazily by the debugger · object size grows 2–5× gdb prints Pebble variables (the box) high to write by hand; LLVM emits it every Unix toolchain, LLDB, GDB
DIBuilder metadata scopes, types, variables, locations as IR metadata linear in the IR the source of every DWARF DIE low: builder calls Clang, rustc, Swift, pebble-debugify
Debug records variable values per program point, surviving mem2reg; passes unaffected (Lesson 9.6) no instruction-count effect location lists at -O2 (the box); "optimized out" where values die low to emit; passes must preserve them LLVM 19+ IR; -g everywhere

Choose to emit DWARF whenever a debugger, profiler or crash reporter will look at the code: it is the only one of the three that exists after the compiler exits. Choose DIBuilder as the way to produce it: it is the only path LLVM offers, and it costs a front end a few calls per declaration. Choose records consciously in every pass you write: a pass that deletes a value must salvage or poison its records, or the debugger lies.

Measured: the -O0 sieve gets 5 DW_TAG_variable DIEs, 121 locations and 75 line-table rows from pebble-debugify and llc -O0; clang -g -O2 keeps 2 ranges of total out of the 4 #dbg_values that mem2reg produced (the boxes).

9. Assessment

  • Quiz: dwarf-expression-eval (number), loclist-from-events (mapping), dibuilder-required-flags (multi), debugify-variable-count (number), find-dibuilder-finalize (text), records-vs-intrinsics (single). Tags dwarf, dibuilder, debug-records.
  • Drill: ./course drill dwarf-location (part A: Algorithm 24.4.5; parts B on medium and hard: Algorithm 24.4.7).
  • Flashcards: tags dwarf, dibuilder, debug-records.
  • Exercises: ★ E3 (pebble-debugify; tests tests/ch24/lit/debugify.ll: records, verifier, llvm-dwarfdump line table and variables, idempotence, <strip>).

Pitfall

"#dbg_value(i64 %x, ...) means the variable is %x." It means the variable's value is %x's value from that point until the next record: if %x is later reused for something else in the source (it is not: SSA), or if %x's register is clobbered by a call, the machine-level location ends there even though the IR record has no end. Ranges come from the back end's dataflow (Algorithm 24.4.7), not from the record.

References

See the chapter references.