Back to course

The .debug_line Header

20 minintermediateModule 2: The Line Number ProgramContinues from dwarf-versions
20 min · Module 2: The Line Number Program · Structures

Why This Matters

Everything a debugger shows you about "which line am I on" comes from one section. Its first 0x38 bytes are a fixed, self-describing header that tells you how long the header is, how wide an address is, and three constants that the rest of the section is meaningless without.

Read this header wrongly and the program that follows decodes into confident garbage. Read it correctly and every subsequent byte is interpretable. It is the highest-value 56 bytes in the file.

A Simple Model

Think of the header as answering four questions in order, for a reader that has just been handed an unknown byte stream:

  1. How long am I?unit_length and header_length — so you know where the variable-length part ends and where the program starts.
  2. How wide is the world?address_size and version — so you know how many bytes an address occupies and which rules to apply.
  3. What are the machine's constants?minimum_instruction_length, maximum_operations_per_instruction, line_base, line_range.
  4. Which byte codes mean what?opcode_base plus the standard_opcode_lengths table — so you can skip over an unknown opcode's arguments.

Questions 3 and 4 exist for one reason: the format is forward-compatible by construction. A v5 reader must be able to walk past an opcode it has never heard of. The standard_opcode_lengths table is what makes that possible — it tells you how many LEB128 arguments to consume so you can skip the opcode and carry on.

The Actual Detail

The complete header of shape_v5's .debug_line — 0x73 bytes total, 0x38 of which is this header. This is the whole section, and the header is the first fourteen lines of it:

00000000: 6f00 0000 0500 0800 3800 0000 0101 01fb  o.......8.......
00000010: 0e0d 0001 0101 0100 0000 0100 0001 0101  ................
00000020: 1f02 0000 0000 2500 0000 0201 1f02 0f04  ......%.........
00000030: 1d00 0000 001d 0000 0000 4800 0000 0150  ..........H..P
00000040: 0000 0001 0523 0009 0229 1100 0000 0000  .....#...)......

Field by field. Every value below was decoded from these bytes and then required to match readelf --debug-dump=rawline — agreement, not eyeballing:

OffsetSizeFieldBytesValue
0x004unit_length6f 00 00 00111 — bytes after this field, so the unit ends at 0x73
0x042version05 005
0x061address_size088 bytes
0x071segment_selector_size000 — no segment selector (flat model)
0x084header_length38 00 00 0056
0x0c1minimum_instruction_length011 byte
0x0d1maximum_operations_per_instruction011
0x0e1default_is_stmt011 — rows start flagged as statements
0x0f1line_basefb-5 (signed; 0xfb = 251 = -5)
0x101line_range0e14
0x111opcode_base0d13 — opcodes 1-12 are standard
0x1212standard_opcode_lengths00 01 01 01 01 00 00 00 01 00 00 01argument count for opcodes 1-12

Three of these deserve more than a table row.

line_base is negative and stored as one byte. 0xfb is not 251 in this field; it is read as a signed byte, giving -5. This is the offset that lets a single opcode advance the line register backwards as well as forwards, which is how a for loop's closing brace gets attributed to the line after the loop body. Getting the sign wrong turns every line delta into a large positive jump.

header_length is what makes the rest of the header skippable. It is 56, and the fixed part before it ends at 0x0c. So 0x0c + 0x38 = 0x44 is where the line program begins. The counted directory and file tables sit inside those 56 bytes, because their length depends on the program; the fixed fields do not move.

standard_opcode_lengths is the forward-compatibility table. Twelve bytes for twelve standard opcodes, summing to 6 arguments in total. Read them as "opcode 1 takes 0 arguments, opcode 2 takes 1, opcode 3 takes 1…". This is the array a parser consults to skip a byte code it does not recognise: read opcode_base-ish bounds, then consume that many LEB128 values. Without it, an unknown opcode would desynchronise the reader permanently.

A Real Example

Let us walk the whole header by hand, because "the header ends at 0x44" is a claim you should be able to defend.

  1. 6f 00 00 00 → unit_length = 111. Add the 4 bytes of the field itself and the unit occupies 115 bytes = 0x73. The section is 0x73 bytes, so there is exactly one unit and no trailing data.
  2. 05 00 → version 5, so read address_size and segment_selector_size next.
  3. 08 00 → address size 8, segment selector size 0.
  4. 38 00 00 00 → header_length = 56. Position is now 0x0c, so the program starts at 0x44.
  5. 01 01 01 → min instruction length 1, max ops per instruction 1, default is_stmt 1.
  6. fb 0e 0d → line_base = -5, line_range = 14, opcode_base = 13.
  7. 00 01 01 01 01 00 00 00 01 00 00 01 → twelve argument counts. Position is now 0x1e.
  8. From 0x1e the header's 56 bytes run out at 0x44 — the remaining 1f 02 00 00 00 00 25 00 00 00 02 01 1f 02 0f 04 1d 00 00 00 ... are the directory and file tables, which the next concept decodes.

Now the confirmation that matters. readelf prints its first line-program statement at [0x00000044]:

 Line Number Statements:
  [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

Byte 0x44 in our dump is 05, which is DW_LNS_set_column — opcode 5, and the statement is "set column to 35" because the next byte 23 is 0x23 = 35. The computed program start and readelf's first statement are the same byte. The header is not approximately right; it is exactly right.

One more check worth doing, because it validates unit_length independently: 0x44 (program start) plus the program itself lands on 0x73, the end of the section. The header claimed 56 bytes, the arithmetic consumed 56 bytes, and the program consumed the rest exactly.

Try It Yourself

$ gcc -gdwarf-5 -O0 -o shape_v5 shape.c
$ readelf --debug-dump=rawline shape_v5

$ objcopy --dump-section .debug_line=dl.bin shape_v5
$ xxd -l 80 dl.bin

Confirm each field yourself against the dump. The ones worth hand-checking are:

Then break it deliberately and watch the check you should have written fire. Overwrite unit_length with something absurd:

$ readelf --debug-dump=rawline broken1
readelf: Warning: The length field (0xfff0) in the debug_line header is wrong - the section is too small

Note that readelf decoded nothing. A length that overruns the section is not something to clamp or guess at — it means the unit cannot be trusted, and the correct response is to stop.

Check Your Understanding

Without looking back: a reader has validated the version and consumed the fixed part of the header, so it is positioned at offset 0x0c. It reads header_length = 0x38. At which offset does the line program begin, and which two independent facts confirm the answer?

Apply It

Byte 0x0f of a v5 line header is fb. A reader that treats it as an unsigned byte gets line_base = 251. Using the special-opcode rule from the next concepts, what does that do to the very first special opcode in shape_v5, whose raw byte is 0x19?

The general habit: in a binary format, exactly one field is signed. Find it, know its width, and never read it unsigned. Here it is one byte, so the failure is a clean 256-line offset rather than chaos — which makes it easy to spot once you know to look.

Connect

unit_length is the same variable-length prefix that opens every section in the PE COFF header and every ELF section you have parsed, and the discipline of validating it against the section size is the same one from DWARF 2, 3, 4 and 5. address_size = 8 is the DWARF copy of the ELF header's EI_CLASS idea: the same data, a different width, chosen once at the top and obeyed everywhere.

The header is done. The 56 bytes it skipped over are the next thing worth understanding, because they are the one place where DWARF 5 changed the encoding rather than just adding a field.

Next: Directory and File Tables — how a line number becomes a filename, in two incompatible encodings.