Developer Tools · Debuggers

DWARF Line Tables: Turning an Address into a File and Line

The line table is a bytecode program that expands into address ranges, not a list of instructions. How debuggers run it, and why a breakpoint on line 12 lands where it does.

A stack trace printed after a crash shows file names and line numbers for addresses that were never mentioned in the source. Between the address and the line sits a section named .debug_line, whose contents are not a table at all. It is a small program in a custom bytecode, executed once by the debugger to produce rows, each row stating that a range of addresses beginning at a given address corresponds to a given source position. The indirection exists because a table of one row per instruction would be enormous, and because the same encoding has to work for architectures with different instruction sizes and for code that the compiler reordered.

A bytecode program, not a table

The line program for a compilation unit lives in .debug_line at the offset named by the DW_AT_stmt_list attribute on the unit’s debugging entry. It begins with a header that fixes the parameters of the encoding: the minimum instruction length in bytes, the opcode at which the special opcodes begin, the size of the line range, and a signed line base. These four numbers let a single byte encode both an address advance and a line advance.

Every opcode at or above opcode_base is special. Subtracting opcode_base leaves an adjusted value; dividing it by line_range gives a count of instruction slots to advance the address by, and the remainder plus line_base gives the change in line number. A special opcode also emits a row. That is the whole trick: a step of a few instructions on one line costs one byte, and a jump to a nearby line costs one byte, because the compiler chooses the pair of deltas the encoding represents.

Standard opcodes handle what the packing cannot. DW_LNS_copy emits a row without advancing anything, DW_LNS_advance_pc and DW_LNS_advance_line take a variable-length operand, DW_LNS_set_file and DW_LNS_set_column change the file and column registers, and DW_LNS_const_add_pc advances by the largest address step a special opcode can express. Extended opcodes carry a length and cover the rest: DW_LNE_set_address loads a full address into the address register, and DW_LNE_end_sequence terminates the current run of rows.

The state that the program manipulates is a set of registers, not just address and line. There is a file index into a table of file names, a column, an is_stmt flag, a basic_block flag, a discriminator for distinguishing several blocks that share a line, and prologue and epilogue markers. Each row records the state at the moment it was emitted.

# The special-opcode arithmetic, which is most of a line program's volume.
def decode_special(op, state, line_base, line_range, opcode_base, min_inst_len):
    adjusted = op - opcode_base
    state["address"] += (adjusted // line_range) * min_inst_len
    state["line"] += line_base + (adjusted % line_range)
    return dict(state)          # a special opcode always emits a row

Rows delimit ranges

A row does not describe an instruction. It describes the interval from its own address to the address of the next row. The last row of a sequence is the end_sequence row, whose address is the first byte after the code the sequence covers, and which carries no source position. A sequence is contiguous and ascending in address; a compilation unit can contain several sequences, which is how a compiler expresses a function split into a hot part and a cold part, or code moved out of line.

The consequence is that the table has fewer rows than the program has instructions, sometimes by an order of magnitude. Most instructions within a statement share a row, and a long run of code generated from one expression is a single range. The debugger builds an index over the row addresses, sorted per sequence, and answers an address query with a binary search followed by a bounds check against the next row.

The same structure answers the reverse query, which is what a breakpoint command needs. Given a file and line, the debugger walks the unit’s rows looking for a row whose file index resolves to that file and whose line number matches. There may be many such rows, because a line inside a loop is reached from several addresses, and because inlining duplicates a line into several callers. Debuggers choose among them with a documented preference order, generally preferring a row with is_stmt set, then the lowest address, and skipping rows that fall inside a function prologue.

Where a breakpoint on a line actually lands

is_stmt exists because not every address that maps to a line is a sensible place to stop. The compiler marks statement boundaries, and a row without the flag usually sits in the middle of an expression, where the machine state does not correspond to anything the source describes. A debugger that ignored the flag and stopped at the first row for a line would frequently stop halfway through evaluating an argument list. The address that is finally chosen becomes the location of a software breakpoint, and the byte written there is the only thing the processor sees.

The prologue is the other source of surprise. The first row of a function often precedes the instructions that establish the stack frame and spill incoming arguments, so stopping there shows parameters that have not yet been stored where the debug information says they live. DWARF 3 added prologue_end on the row that marks the end of the prologue precisely so a debugger can place a function breakpoint after the frame is set up. Without that flag, a debugger has to guess from the instruction stream or from the frame description.

The discriminator register handles the case of several distinct basic blocks attributed to one source line, which is common when a macro or a single-line conditional expands into branches. Rows that share an address and line but differ in discriminator are distinct positions, and a debugger that reports coverage or sets a breakpoint by line alone collapses them into one.

Addresses in a linked executable are absolute and already relocated. In a relocatable object file they are section-relative until the linker applies relocations, which is why debug information is only usable for symbolization after linking, and why a debugger loading a .o file has to apply the same relocations the linker would.

When the table lies

Three failure modes are common enough to be recognizable.

The first is a missing or stale table. Code compiled without debug information has no .debug_line, and the debugger falls back to the symbol table, which gives function names but no lines. A binary whose debug information was built from different sources than the executable has rows that point at the wrong text: the addresses are plausible, the lines are wrong, and nothing in the file records the mismatch. Build identifiers and the checksum embedded in DWARF 5 file entries exist to detect this, and a debugger that verifies them reports a mismatch instead of printing a confident wrong answer.

The second is optimisation. A line table generated for optimised code can map one line to a range that contains code from several statements, or map a line to an address range that does not include the instruction that implements it, because the instruction was moved, merged with another, or deleted. A breakpoint on a line whose code was eliminated may resolve to a nearby row or to nothing. This is a property of the mapping, not a bug in the debugger, and it is why debugging optimised code needs more than a line number to locate a variable.

The third is address ambiguity across shared objects. Two libraries can be loaded at addresses that overlap in the debugger’s index if the load bias is applied inconsistently, and the resulting trace shows a file name from one library with a line number from another. Debuggers key rows by the section they came from and apply the load address at query time for this reason; the row addresses in the file are relative to the object, not to the process.

What the line table cannot express is anything about data. It maps addresses to positions in text, and a breakpoint derived from it stops before an instruction executes. Observing a variable change requires the processor’s watchpoint machinery or a conditional breakpoint, and neither is derived from .debug_line. The table’s job ends at the boundary between “where the program is” and “what the program is doing”.

References

  1. DWARF Debugging Information Format, Version 5 — DWARF Standards Committee
  2. LLVM Source Level Debugging with LLVM — LLVM Project
  3. GDB Documentation — GNU Project