llvm / llvm/llvm-project

[DebugInfo][DWARF] DWARFTypePrinter::appendScopes omits DW_TAG_partial_unit, so a type's qualified name is prefixed with the unit's file name

Open
#219,635 1 comment 0 reactions 0 assignees View on GitHub
debuginfo
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## Summary

`DWARFTypePrinter::appendScopes` builds a type's qualified name by walking up its parents and prefixing each enclosing scope. It stops when it reaches a unit root, and it recognizes one by tag: `DW_TAG_compile_unit`, `DW_TAG_type_unit`, `DW_TAG_skeleton_unit`. `DW_TAG_partial_unit`, the fourth unit root tag and the one `dwz` produces, is not in that list, so the walk does not stop there. It treats the root as an ordinary enclosing scope and emits the root's `DW_AT_name` — a source file path — as the outermost component of the name.

A `struct Foo` at unit scope is therefore named `dwz-common.h::Foo`. The same input with a `DW_TAG_compile_unit` root, the only differences being that tag and its matching unit type, names it `Foo`.

`dwz` is a standard step in Fedora, RHEL and Debian debuginfo packaging, so this is the ordinary shape of a distribution debuginfo file rather than a corner case.

## Reproducer

No linking is involved; reading the object is enough. The two variants below differ only in the root DIE's tag and the unit type, supplied with `--defsym`.

```asm
.section __TEXT,__text,regular,pure_instructions
.globl _foo
_foo:
Lfunc_begin0:
retq
Lfunc_end0:

.section __DWARF,__debug_abbrev,regular,debug
Lsection_abbrev:
.byte 1 ## Abbreviation Code
.byte ROOT ## root tag
.byte 1 ## DW_CHILDREN_yes
.byte 37 ## DW_AT_producer
.byte 8 ## DW_FORM_string
.byte 19 ## DW_AT_language
.byte 5 ## DW_FORM_data2
.byte 3 ## DW_AT_name
.byte 8 ## DW_FORM_string
.byte 0, 0

.byte 2 ## Abbreviation Code
.byte 46 ## DW_TAG_subprogram
.byte 0 ## DW_CHILDREN_no
.byte 3 ## DW_AT_name
.byte 8 ## DW_FORM_string
.byte 73 ## DW_AT_type
.byte 19 ## DW_FORM_ref4
.byte 17 ## DW_AT_low_pc
.byte 1 ## DW_FORM_addr
.byte 18 ## DW_AT_high_pc
.byte 6 ## DW_FORM_data4
.byte 0, 0

.byte 3 ## Abbreviation Code
.byte 19 ## DW_TAG_structure_type
.byte 0 ## DW_CHILDREN_no
.byte 3 ## DW_AT_name
.byte 8 ## DW_FORM_string
.byte 11 ## DW_AT_byte_size
.byte 11 ## DW_FORM_data1
.byte 0, 0

.byte 0 ## EOM(3)

.section __DWARF,__debug_info,regular,debug
Lsection_info:
.long Lcu_end - Lcu_start ## Length of Unit
Lcu_start:
.short 5 ## DWARF version number
.byte UNITTYPE ## DW_UT_compile / DW_UT_partial
.byte 8 ## Address Size (in bytes)
.long 0 ## Offset Into Abbrev. Section

.byte 1 ## Abbrev [1] root
.asciz "hand-written" ## DW_AT_producer
.short 0x0004 ## DW_AT_language (DW_LANG_C_plus_plus)
.asciz "dwz-common.h" ## DW_AT_name

.byte 2 ## Abbrev [2] DW_TAG_subprogram
.asciz "foo" ## DW_AT_name
.long Lfootype - Lsection_info ## DW_AT_type
.quad Lfunc_begin0 ## DW_AT_low_pc
.long Lfunc_end0 - Lfunc_begin0 ## DW_AT_high_pc

Lfootype:
.byte 3 ## Abbrev [3] DW_TAG_structure_type
.asciz "Foo" ## DW_AT_name
.byte 8 ## DW_AT_byte_size

.byte 0 ## End Of Children Mark (root)
Lcu_end:
```

```console
$ llvm-mc -triple x86_64-apple-darwin -filetype=obj --defsym ROOT=60 --defsym UNITTYPE=3 hash.s -o pu.o
$ llvm-dwarfdump --debug-info pu.o
0x00000029: DW_TAG_subprogram
DW_AT_name ("foo")
DW_AT_type (0x0000003e "dwz-common.h::Foo")
```

The control, with only the root tag and unit type changed:

```console
$ llvm-mc -triple x86_64-apple-darwin -filetype=obj --defsym ROOT=17 --defsym UNITTYPE=1 hash.s -o cu.o
$ llvm-dwarfdump --debug-info cu.o
0x00000029: DW_TAG_subprogram
DW_AT_name ("foo")
DW_AT_type (0x0000003e "Foo")
```

## Analysis

https://github.com/llvm/llvm-project/blob/4cbcc02ed026f6f92c818656db4fca5a442870b5/llvm/include/llvm/DebugInfo/DWARF/DWARFTypePrinter.h#L837-L854

```cpp
template
void DWARFTypePrinter::appendScopes(DieType D) {
if (D.getTag() == dwarf::DW_TAG_compile_unit)
return;
if (D.getTag() == dwarf::DW_TAG_type_unit)
return;
if (D.getTag() == dwarf::DW_TAG_skeleton_unit)
return;
if (D.getTag() == dwarf::DW_TAG_subprogram)
return;
if (D.getTag() == dwarf::DW_TAG_lexical_block)
return;
D = D.resolveTypeUnitReference();
if (DieType P = D.getParent())
appendScopes(P);
appendUnqualifiedName(D);
OS << "::";
}
```

Three unit root tags are enumerated and the fourth is absent. Since `DWARFContext::compile_units()` filters out only type units, a partial unit is handed to every consumer of this header on exactly the same path a full compilation unit is.

The output is not confined to `llvm-dwarfdump`. `appendScopes` is reached from both public entry points of the printer, and they have three callers:

1. `dumpTypeQualifiedName` (`llvm/lib/DebugInfo/DWARF/DWARFDie.cpp:882`) calls `appendQualifiedName`, which is what produced the console output above.
2. `makeSimpleTemplateNameWithParams` (`llvm/lib/DWARFLinker/Classic/DWARFLinkerDeclContext.cpp:20`) calls `appendAndTerminateTemplateParameters`, which reaches `appendQualifiedName` for every `DW_TAG_template_type_parameter` (`DWARFTypePrinter.h:619`). The result becomes the DeclContext name the classic DWARFLinker uniques on, so a template instantiation whose type argument lives under a partial unit root carries the file name inside its ODR key and does not unique against the same instantiation from another object.
3. LLDB computes simple template names the same way in `DWARFASTParserClang.cpp:963`.

I have reproduced 1 directly. 2 and 3 are the call chain rather than a measurement; I have not built inputs for them.

## Specification

DWARF Version 5, February 13, 2017.

Section 3.1.1 "Full and Partial Compilation Unit Entries" (page 60) makes both tags unit roots:

> A full compilation unit is represented by a debugging information entry with the tag `DW_TAG_compile_unit`. A partial compilation unit is represented by a debugging information entry with the tag `DW_TAG_partial_unit`.

The same section then states, in as many words, that a partial unit is not the containing scope of what it owns:

> A full or partial compilation unit entry owns debugging information entries that represent all or part of the declarations made in the corresponding compilation. In the case of a partial compilation unit, the containing scope of its owned declarations is indicated by imported unit entries in one or more other compilation unit entries that refer to that partial compilation unit (see Section 3.2.5 on page 74).

Section 3.2.5 "Imported Unit Entries" (page 74) says the same thing from the other side:

> An imported unit entry contains a `DW_AT_import` attribute whose value is a reference to the normal or partial compilation unit whose declarations logically belong at the place of the imported unit entry.

So a scope walk that treats the partial unit root as an enclosing scope is not merely picking an unhelpful name, it is contradicting the one place the specification says the scope comes from. The root contributes no scope component of its own under any reading.

What the root's name actually is, from section 3.1.1 item 2 (page 60):

> A `DW_AT_name` attribute whose value is a null-terminated string containing the full or relative path name (relative to the value of the `DW_AT_comp_dir` attribute, see below) of the primary source file from which the compilation unit was derived.

A path name, not an identifier, and it is the same attribute for both tags — which is why nothing downstream can tell the difference once it has been emitted as a scope.

Finally, section 3.2.2 "Namespace Entries" (page 71) settles what the correct name is for a type sitting directly under a unit root:

> The C++ global namespace (the namespace referred to by `::f`, for example) is not explicitly represented in DWARF with a namespace entry (thus mirroring the situation in C++ source). Global items may be simply declared with no reference to a namespace.

The type in the reproducer is at global scope. DWARF deliberately gives that scope no entry, so there is nothing above `Foo` to name, and the unit root is not a stand-in for it.

For completeness on why this shape exists at all, section 3.1.1 (page 60) points at the technique `dwz` implements:

> In a compilation employing the DWARF space compression and duplicate elimination techniques from Appendix E.1 on page 365, multiple compilation units using the tags `DW_TAG_compile_unit`, `DW_TAG_partial_unit` and/or `DW_TAG_type_unit` are used to represent portions of an object file.

## Expected behavior

The walk stops at any unit root, so the type is named `Foo` whatever tag its unit root carries.

`dwarf::isUnitType()` already answers precisely this question for all four unit tags and would replace the three separate comparisons. Note, that it is overloaded on `uint8_t` as well as on `dwarf::Tag`, and an argument of any other integer type binds the `uint8_t` overload silently, comparing a tag number against the `DW_UT_*` codes. `DieType::getTag()` returns `dwarf::Tag` here, so the correct overload is selected, but the trap is worth knowing about before writing the patch.

## Versions

Reproduced on stock 22.1.8 (Fedora 44, `/usr/bin/llvm-dwarfdump`); the console output above is from that build. The code site is unchanged on `main` as of 4cbcc02ed026f6f92c818656db4fca5a442870b5, and a build of that tree produces the same names.

This report was produced with AI assistance; I have reviewed it and am accountable for it.

Contributor guide

Open the contributing guide

Research direction

Start in llvm/include/llvm/DebugInfo/DWARF/DWARFTypePrinter.h at DWARFTypePrinter::appendScopes, then inspect dwarf::isUnitType and the DWARFDie.cpp caller mentioned in the report. Run the supplied llvm-mc and llvm-dwarfdump reproducer with partial and compile unit roots; done means both forms produce the qualified name Foo without the unit file name.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.