One compilation unit, one source file, one .debug_info unit — that is the mental model from Module 3, and for a C program it is simply true. C++ breaks it, and it breaks it for a specific reason worth stating: the same type is described over and over. A header defining std::string is included by four hundred translation units, and if each one carries its own copy of the full std::string DIE tree, the debug info is four hundred times larger than it needs to be, and a linker merging them has four hundred structurally identical things to compare.
The fix is to factor the type out of the compile unit. A type unit is a compilation unit whose job is to describe one type, given a name that identifies it independently of where it was found. The compile units then refer to that name rather than repeating the tree, and identical types from different files land in the same unit.
Which makes type units the first place in DWARF where identity is separated from location. Everything so far in this course has said "the type is at offset X in this unit". A type unit says "the type is whatever has this signature" — and as this concept shows, that signature is not what its name suggests it is.
Two header layouts, because type units predate version 5 by a long way and version 5 moved them.
| Field | DWARF 4, in .debug_types | DWARF 5, in .debug_info |
|---|---|---|
unit_length | yes | yes |
version | yes | yes |
unit_type | — implied by the section | DW_UT_type = 2 |
address_size | yes | yes |
debug_abbrev_offset | yes | yes |
type_signature | yes, 16 bytes | yes, 16 bytes |
type_offset | yes | yes |
Read the version 4 column as what a reader learns from the section name instead of a field: anything in .debug_types is a type unit, and anything in .debug_info is a compile unit, and the two can never be confused. That is a genuinely nice property of the older design and version 5 gave it up — because it bought something else, which is the next section.
.debug_infoCompile the same file both ways and the difference is not subtle:
$ gcc -gdwarf-4 -fdebug-types-section -c t.c -o v4t.o $ readelf -S -W v4t.o | grep -c '\.debug_types ' -> 1 $ gcc -gdwarf-5 -fdebug-types-section -c t.c -o types.o $ readelf -S -W types.o | grep -c '\.debug_types ' -> 0 $ readelf --debug-dump=info types.o | grep 'Unit Type' Unit Type: DW_UT_type (2)
The version 5 build has no .debug_types section at all, and the type unit is sitting in .debug_info where readelf reports it by its unit_type. This is a real, checkable relocation of a structure, and it is the kind of change that breaks every tool that walked .debug_info assuming every unit in it was a compile unit.
The reason is worth knowing because it is the same reason as a great many format decisions: a section has overhead, and units do not divide evenly into sections. A file with one type unit and one compile unit used to need two sections, each with a section header, an alignment requirement, and an entry in the relocatable object. In version 5 they share one. The cost is that the unit header grew by the unit_type byte you decoded in the previous concept; the benefit is that the boundary between "compile units" and "type units" stops being a section boundary and becomes a value, which is far cheaper to extend — version 5 added split units and partial units in exactly this space, and neither needed a new section.
The type unit is the first thing in .debug_info of the version 5 build. The whole header, 32 bytes:
0000: 67 00 00 00 05 00 02 08 00 00 00 00 89 5c 02 be 0010: e0 73 7e 69 23 00 00 00 01 1d 03 47 23 00 00 00 01 1d
| Bytes | Field | Value |
|---|---|---|
67 00 00 00 | unit_length | 103 |
05 00 | version | 5 |
02 | unit_type | DW_UT_type = 2 |
08 | address_size | 8 |
00 00 00 00 | debug_abbrev_offset | 0 |
89 5c 02 be e0 73 7e 69 23 00 00 00 01 1d 03 47 | type_signature | 16 bytes |
23 00 00 00 | type_offset | 0x23 = 35 |
01 | first abbrev code | the type DIE begins at 0x23 |
One thing to check before trusting the table: type_offset is 0x23, and the unit's unit_length is 103, so the type DIE starts 35 bytes into a 107-byte unit. That is a plausible position — there is a root DIE and some type DIEs before the named type. And readelf independently reports Type Offset: 0x23, which is the cross-check that matters.
Same source, version 4, in .debug_types:
0000: 61 00 00 00 04 00 00 00 00 00 08 89 5c 02 be e0 0010: 73 7e 69 1d 00 00 00 01 0c 00 00 01 0c 00 00 00 00 02 53
Eleven bytes of the standard version 4 header — no unit_type — then the signature at offset 0x0b, then type_offset. Compare the two signature fields directly:
v4, in .debug_types: 89 5c 02 be e0 73 7e 69 1d 00 00 00 01 0c 00 00
v5, in .debug_info : 89 5c 02 be e0 73 7e 69 23 00 00 00 01 1d 03 47
^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
IDENTICAL type_offset differs
Both readers agree on the version 4 unit and report the same signature:
$ readelf --debug-dump=info types.o | grep Signature
Signature: 0x697e73e0be025c89
$ readelf --debug-dump=info v4t.o | grep Signature
Signature: 0x697e73e0be025c89Identical. The same type, compiled by the same compiler at two different DWARF versions, has the same signature as far as readelf is concerned. And yet the two sixteen-byte fields in the files are not identical. Decompose both into four 32-bit words:
| Word | Version 4 | Version 5 | Same? |
|---|---|---|---|
u32[0] | 0xbe025c89 | 0xbe025c89 | yes |
u32[1] | 0x697e73e0 | 0x697e73e0 | yes |
u32[2] | 0x0000001d | 0x00000023 | no |
u32[3] | 0x00000c01 | 0x47031d01 | no |
And here is the part that makes this worth a concept. u32[2] is the type_offset field, sitting inside the signature:
v4 u32[2] = 0x0000001d = 29 and readelf's Type Offset for v4 is 0x1d v5 u32[2] = 0x00000023 = 35 and readelf's Type Offset for v5 is 0x23
Both match, exactly, in both files. So the sixteen bytes called a type signature are not sixteen opaque hash bytes: the third word is a copy of the unit's own type_offset, and the fourth word is the only part that looks like a hash.
type_offset in both; readelf prints only the first eight bytes as a 64-bit value; and u32[3] is the only word that differs in a way not already explained by the offset.readelf shows you half of it, and leaves the reason open. If you work it out, that is a real result and it belongs in a reader.And the practical consequence of readelf showing you only eight bytes: two type units with the same displayed signature are not thereby known to be the same type. They could differ in the half you were not shown. Any tool that treats the displayed value as the whole signature is making an assumption it has not checked, and the assumption is wrong in at least one direction here — the same type in two versions does produce different full signatures.
How a reader uses this, and where the two versions genuinely differ rather than merely look different. The reference form is DW_FORM_ref_sig8: an eight-byte value that is not an offset into .debug_info but a type signature, to be resolved by searching for the type unit that claims it.
Two properties of that algorithm are worth naming, and both come from the design rather than from the code.
It is a two-pass algorithm, and the first pass is global. A DW_FORM_ref_sig8 in the first unit of a file can refer to a type unit at the end of the file, or in another file entirely, or in a .dwp package. There is no "the target is nearby" assumption available, so the index must be built before any reference can be resolved. A reader that resolves references as it walks will fail on exactly the units that use them.
The failure mode is a dangling reference, not a wrong one. If the signature is not in the index, there is no fallback that produces a plausible answer — there is simply no type. That is unusual and valuable. Compare it with a DW_FORM_ref_addr, where a stale offset silently resolves to whatever DIE now lives there: same size, plausible structure, wrong type. A signature-based reference fails loudly; an offset-based reference fails silently. That is a real argument for the whole indirection, and it is the mirror image of what the portability concept found about indexed addresses.
Which also explains why the signature is worth having even though the mechanism above does not verify it. The signature's job is to let a linker merge two type units that describe the same type, and to let a debugger share one copy across four hundred files. Both jobs are optimisation jobs — the debug info is correct without them. And that is exactly why the unverified half matters: a signature that collided or drifted would not make the debug info wrong, it would make a linker merge two different types, and the result would be a program whose types are subtly wrong with no diagnostic anywhere. The field is load-bearing precisely where nobody is looking.
$ gcc -gdwarf-4 -fdebug-types-section -O0 -c t.c -o v4t.o
$ gcc -gdwarf-5 -fdebug-types-section -O0 -c t.c -o types.o
$ readelf -S -W v4t.o | grep -oE '\.debug_[a-z_]+'
$ readelf --debug-dump=info types.o | grep -E 'Unit Type|Signature|Type Offset'readelf's Type Offset for both versions. Then confirm that readelf's printed 64-bit value is words 0 and 1 only. Five minutes, and you will never again assume a displayed signature is the whole field.-gdwarf-2 and -gdwarf-3 and -fdebug-types-section as well. If u32[0] and u32[1] stay fixed while the offset word moves, you have isolated which part of the field is encoding-dependent. If they move too, the field is encoding-dependent throughout and the version 4 versus 5 match was the coincidence.-gdwarf-5 -fdebug-types-section, note a DW_FORM_ref_sig8 value, then recompile without the flag and look for the reference that no longer resolves. It is the cleanest demonstration available of a reference that fails loudly instead of resolving to the wrong thing..debug_info's size with and without it. Then count how many type units there are and how many DW_FORM_ref_sig8 references point at them. The ratio is the size argument, measured rather than asserted.DW_FORM_ref_sig8. Then point it at the version 5 file and confirm it agrees with readelf on which unit each reference lands in. It is about thirty lines and it is the only way to find out whether the offset word inside the signature agrees with the header's own type_offset — two fields that ought to be redundant.Without looking back: the same type is compiled at DWARF 4 and at DWARF 5. readelf reports Signature: 0x697e73e0be025c89 for both units. The two sixteen-byte fields in the files are 89 5c 02 be e0 73 7e 69 1d 00 00 00 01 0c 00 00 and 89 5c 02 be e0 73 7e 69 23 00 00 00 01 1d 03 47, and the two Type Offset values are 0x1d and 0x23. What can you conclude, and what must you not conclude?
Your build links correctly and your tests pass, but a colleague notices that one translation unit's view of a shared type shows a member that the other translation unit's view does not. Both were compiled with the same compiler, the same flags and the same DWARF version, from the same header, on the same day. The two objects' debug info is enormous. Given this concept, what is the most likely mechanism, and what single change would convert the failure from silent into loud?
The general habit: a field whose only job is to let something be merged or skipped is a field nobody checks. When a silent wrong answer is possible, prefer the slower comparison that can fail, at least once, on purpose.
This is the payoff for the two sentences in the split-debugging concept that said a skeleton carries an 8-byte dwo_id where a type unit would carry a 16-byte signature and offset. That was a comparison of two headers nobody had looked at. It has now been looked at, and the 16 bytes have a structure — which means the two identifier schemes are more alike than the sentence implied, since both carry a position alongside the identity.
It is also the answer to a question the types concept had to leave open: following a DW_AT_type chain is straightforward inside one unit and genuinely awkward across them, because a reference can leave the unit you are reading. This concept supplies the join. The general rule across all of DWARF is that a reference is either an offset — cheap, local, fails silently when stale — or a signature — expensive, global, fails loudly. Version 5 moved a lot of the format from the first kind to the second, and the reason is exactly that second property.
unit_type is a value and values compose, so version 5 could add kinds without adding sections. It has since added three more: DW_UT_partial for a unit whose abbrev table is shared with a unit elsewhere, and DW_UT_split_compile and DW_UT_split_type for the skeleton and split halves of split debugging. Only the type unit carries a signature, and only the split compile unit carries a dwo_id — which is the whole reason those two header fields are optional, and a reader that reads them unconditionally reads sixteen bytes of the next unit.
The remaining unit kind that appears in real files and is not yet decoded here is DW_UT_partial. It is the rarest of the four and it exists for a narrow reason: a producer that emits many units sharing one abbrev table can say "read my abbreviations from over there" instead of duplicating them. It is the unit-kind version of the indexed-string idea, and the same trade — smaller output, a global lookup, a reference that fails loudly if the other unit is missing.
What comes next is a different kind of question entirely. So far every concept has asked where is this thing. The call-site information asks what happened here: not what a variable is, or what a type is, but what the program was doing at a return address. That is a different question about the same DIE tree, and it is the last of the four unit-shaped features this module covers.
Next: Call Sites.