Back to course

DWARF 2, 3, 4 and 5

18 minintermediateModule 1: Why Debug Info ExistsContinues from dwarf-sections
18 min · Module 1: Why Debug Info Exists · Versioning

Why This Matters

You are about to write a parser. The version field is a 2-byte number at a known offset, and it is the only thing standing between you and a program that reads a v4 file as if it were v5 and produces confident nonsense — addresses in the wrong place, strings from the middle of other strings, no error anywhere.

DWARF has changed four times. Two of those changes altered the byte layout of headers that already existed, which is the dangerous kind of change: no new section appears to warn you, the file still looks structurally valid, and only the offsets move.

A Simple Model

Three rules cover every version difference you will meet:

  1. Version gates the layout. Every offset after the version field is version-relative. Check the version first, before trusting anything else.
  2. New sections are safe; moved fields are not. A parser that ignores a section it does not understand still works. A parser that mis-locates a field does not.
  3. The newest version is not always the one in your file. Toolchains default differently, and build systems pin flags. Always read the version; never assume it.

The Actual Detail

Same shape.c, same -O0, four versions. The line-number header of each, from readelf --debug-dump=rawline:

FieldDWARF 4DWARF 5Moved?
unit_length0x93 (147)0x6f (111)no
version45no
address_sizeabsentpresent at 0x06added
segment_selector_sizeabsentpresent at 0x07added
header_length0x5e (94) at 0x060x38 (56) at 0x08moved
fixed prologue size10 bytes12 byteschanged
line program starts at0x0a + 0x5e = 0x680x0c + 0x38 = 0x44derived
directory tableNUL-terminated stringscounted, explicit formreplaced
file tablename + 3 ULEBscounted, explicit formreplaced
line_base / line_range / opcode_base-5 / 14 / 13-5 / 14 / 13no — identical

The last row is worth pausing on. The arithmetic that decodes the line program is byte-identical between v4 and v5. All of the churn is in the header's front matter and in how paths are stored. So a v5 reader that gets the special-opcode maths right but the header wrong will still fail — and vice versa. The two halves of this course are independent skills.

The .debug_info compilation-unit header changed the same way:

FieldDWARF 4DWARF 5
unit_length0x10e (270)0x10c (268)
version45
unit_typeabsentpresent at 0x06
address_size88
debug_abbrev_offset00
first DIE begins at0x0b0x0c

One extra byte, and every DIE in the section moves. readelf prints those positions as <0><b> and <0><c>.

DWARF 5 also added attributes that did not exist before: DW_AT_language_name and DW_AT_language_version, both present in our compile unit (3 = C, 202311). And a whole new form, DW_FORM_line_strp, which resolves against .debug_line_str instead of .debug_str.

A Real Example

Two headers, side by side, from the same source file. Read the first sixteen bytes of each.

DWARF 4 (-gdwarf-4):

00000000: 9300 0000 0400 5e00 0000 0101 01fb 0e0d  ......^.........
00000010: 0001 0101 0100 0000 0100 0001 2f75 7372  ............/usr

DWARF 5 (-gdwarf-5):

00000000: 6f00 0000 0500 0800 3800 0000 0101 01fb  o.......8.......
00000010: 0e0d 0001 0101 0100 0000 0100 0001 0101  ................

Look at byte 0x06. In the v4 line it is 5e, the low byte of header_length = 94. In the v5 line it is 08, which is address_size = 8 — a completely different field, at the same offset. Everything after it shifts with it.

A v5 reader on a v4 file. It reads address_size = 0x5e = 94, then segment_selector_size = 0, then takes four bytes at 0x08 — which in a v4 file are 00 00 01 01 — as header_length = 16,842,752. Its computed program start is 0x101000c, in a section that is only 0x97 bytes long. The very first thing it reads is past the end of the file.

A v4 reader on a v5 file. It reads four bytes at 0x06 — in a v5 file those are 08 00 38 00 — giving header_length = 3,670,024, and computes a program start of 0x380012 in a 0x73-byte section. Again: immediately past the end.

Notice the failure shape. In both directions the parser does not produce wrong answers at first — it produces an offset that is obviously insane. That is the good case, and it is why validating the computed offsets against the section size is the single most useful check you can put in a DWARF parser. The genuinely dangerous case is a layout change that shifts an offset by a small amount, keeping every computed value inside the section. That is what the counted-tables change in v5 does, and it is the subject of the next concept.

This is also why the two tables look so different further in. In v4 the directory table is at offset 0x1c and is a bare run of NUL-terminated strings — the literal bytes /usr at 0x1f, as you can see above. In v5 there is no string data in the line header at all; every name is a 4-byte offset into .debug_line_str, plus a format descriptor saying how to interpret it.

Try It Yourself

Build all four versions and confirm the toolchain really does produce them:

$ for v in 2 3 4 5; do gcc -gdwarf-$v -O0 -o shape_v$v shape.c; done
$ for v in 2 3 4 5; do echo "--- v$v ---"; readelf --debug-dump=rawline shape_v$v | head -8; done

Now break a file and see the consumer's real complaint. Set the .debug_info version to 9:

$ readelf --debug-dump=info broken2
readelf: Warning: CU at offset 0 contains corrupt or unsupported version number: 9.

And set the .debug_lineunit_length to something larger than the section:

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

What to look for: readelf stops at the version gate and decodes nothing further. That is the correct behaviour, and it is the behaviour your own parser needs. When it printed a version warning it had already done the right thing; when it printed a length warning it had trusted unit_length before checking it against the section size — which is itself a bug worth knowing about.

Check Your Understanding

Without looking back: a v4 reader consumes a v5 file. At offset 0x06 the v5 file has 08 and the v4 reader expects the low byte of header_length. What wrong value does it compute, and where does its first incorrect byte come from?

Apply It

Your tool must read binaries from four toolchains: a v2-era cross compiler, a v4 GCC, a v5 GCC, and a v5 Clang whose unit_type is not DW_UT_compile. You may not assume the version, and you may not hard-code one layout. What is the minimum correct order of operations, and what must you do about the Clang unit type?

The general habit: version is the one offset that is stable across every version of a format. Make it your first read and your first branch.

Connect

The unit_length field is the same self-describing length you met in the ELF header, and the "validate the length against the section size before trusting it" habit is the same one the ELF section header table demands.

You now know which version your file is and how to tell. Module 2 opens the section that matters most and takes it apart byte by byte, starting with the header you have just learned to distrust.

Next: The .debug_line Header — all fourteen fixed fields of the v5 header, decoded from the real bytes of shape_v5.