Conversation
The abbreviation table held exactly one shape -- a compile unit with no
children -- so a c17 binary described its files and nothing inside them. gdb
fell back to the ELF symbol table for function names, which is why backtraces
named functions while `info args`, `info locals`, `print x` and `ptype` all came
back empty.
There are now DIEs for functions, for the parameters and locals inside them, and
for the types they need: base types, pointers, arrays, and aggregates by size.
A variable's location is built from the same address computation the loads and
stores go through -- `stack_mem` on x86-64, `loc_addr_parts` on aarch64 -- and
emitted as `DW_OP_breg<n> <offset>`, naming the base register directly rather
than going through `DW_AT_frame_base`. That keeps it true for an over-aligned
frame, whose locals are not addressed from the frame pointer at all, and it
means a location cannot disagree with the code that reads the variable.
(gdb) info locals
arr = {5, 0, 0, 0}
c = 5
A variable with no memory to point at gets no location rather than a made-up
one, and gdb says "optimized out" on its own.
Two things bit while building it, both worth the comments they now carry.
aarch64's register enum omits x18 -- platform-reserved, never allocated -- so
every variant after X17 sits one below its register number, and a DWARF number
taken from the discriminant made the frame pointer x29 come out as 28. And a
`DW_FORM_block1` states its own length, which cannot be measured between labels
here: a label keyed by register and offset collides the moment two functions put
a variable in the same place, which it did, in about three hundred tests.
Known gaps, both stated rather than papered over:
- A local that SSA promoted into registers has no stack home and so no
location. At -O0 that is most of them, because c17 promotes at every
optimization level. Describing those needs either location lists or a
decision not to promote under -g; the second is a codegen change and wants
its own commit.
- Aggregates carry `DW_AT_byte_size` and no members, so gdb prints a struct
as `{<No data fields>}`. A member's name is a `StringId` and no string
table reaches the backend, so member DIEs need the `CodeGenerator` trait
widened first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR enhances the cc backend’s DWARF emission so debuggers can see subprogram DIEs, variable DIEs (params + locals), and the minimal set of type DIEs needed to interpret them, with variable locations derived from the backend’s real addressing computations (x86-64 stack_mem, aarch64 loc_addr_parts).
Changes:
- Collect per-function debug metadata during codegen (function extent + locals/params + locations) and emit it into
.debug_info. - Expand DWARF abbreviation/info generation to include subprograms, variables (with/without locations), and basic/pointer/array/aggregate types.
- Add target-specific DWARF register numbering helpers and
.sleb128directive support; add tests covering subprogram DIE presence and location base register correctness.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| cc/tests/codegen/debug_info.rs | Adds regression tests for subprogram DIE emission and correct base register usage in variable locations. |
| cc/arch/x86_64/regalloc.rs | Defines System V AMD64 DWARF register numbering for debug location expressions. |
| cc/arch/x86_64/frame.rs | Collects per-function DIE info (including variable locations) and emits a function-end label for DW_AT_high_pc. |
| cc/arch/x86_64/codegen.rs | Switches debug-info emission to pass collected function DIEs + unit info into DWARF generator. |
| cc/arch/lir.rs | Adds .sleb128 directive emission needed for DWARF location expressions. |
| cc/arch/dwarf.rs | Major DWARF expansion: new abbrev shapes, type DIE collection/emission, function/variable DIE emission, and SLEB128 length logic. |
| cc/arch/codegen.rs | Stores collected per-function debug DIEs in CodeGenBase. |
| cc/arch/aarch64/regalloc.rs | Defines AAPCS64 DWARF register numbering with explicit handling for the missing x18 enum variant. |
| cc/arch/aarch64/frame.rs | Collects per-function DIE info (including variable locations) and emits a function-end label for DW_AT_high_pc. |
| cc/arch/aarch64/codegen.rs | Switches debug-info emission to pass collected function DIEs + unit info into DWARF generator. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
`debug_name` strips the linearizer's uniquing suffix so a debugger shows `c`
rather than `c.4`. But there are no `DW_TAG_lexical_block` DIEs yet, so every
local and parameter is a sibling directly under the subprogram -- and once two
of them are shadowed into the same `DW_AT_name`, a debugger has to pick one.
gdb picks the last. So on ordinary C:
int f(int a) {
int x = 10;
{ int x = 20; { int x = 30; } }
return x; /* the outer x, 10 */
}
`print x` at the return answered **30**, and `info locals` listed three `x`s.
gcc answers 10. A wrong value, silently, with nothing to suggest it was wrong.
Where a stripped name collides, every DIE carrying it keeps its suffix instead,
so `x.2`, `x.6` and `x.10` name one variable each: all three stay reachable and
`print x` says "No symbol" rather than picking one. Refusing beats answering
wrongly. Names that are not shadowed are unaffected and stay plain.
The real answer is lexical-block DIEs, which need scope information the IR does
not currently keep -- `LocalVar::decl_block` is a CFG block, not a lexical
scope. This closes the wrong-answer path until that exists.
Reported by GitHub Copilot on the review of a11caac; reproduced against gcc
before fixing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The abbreviation table held exactly one shape -- a compile unit with no children -- so a c17 binary described its files and nothing inside them. gdb fell back to the ELF symbol table for function names, which is why backtraces named functions while
info args,info locals,print xandptypeall came back empty.There are now DIEs for functions, for the parameters and locals inside them, and for the types they need: base types, pointers, arrays, and aggregates by size.
A variable's location is built from the same address computation the loads and stores go through --
stack_memon x86-64,loc_addr_partson aarch64 -- and emitted asDW_OP_breg<n> <offset>, naming the base register directly rather than going throughDW_AT_frame_base. That keeps it true for an over-aligned frame, whose locals are not addressed from the frame pointer at all, and it means a location cannot disagree with the code that reads the variable.A variable with no memory to point at gets no location rather than a made-up one, and gdb says "optimized out" on its own.
Two things bit while building it, both worth the comments they now carry. aarch64's register enum omits x18 -- platform-reserved, never allocated -- so every variant after X17 sits one below its register number, and a DWARF number taken from the discriminant made the frame pointer x29 come out as 28. And a
DW_FORM_block1states its own length, which cannot be measured between labels here: a label keyed by register and offset collides the moment two functions put a variable in the same place, which it did, in about three hundred tests.Known gaps, both stated rather than papered over:
DW_AT_byte_sizeand no members, so gdb prints a struct as{<No data fields>}. A member's name is aStringIdand no string table reaches the backend, so member DIEs need theCodeGeneratortrait widened first.