Skip to content

cc/arch: describe functions and their variables in DWARF - #665

Merged
jgarzik merged 2 commits into
mainfrom
updates
Aug 24, 2026
Merged

cc/arch: describe functions and their variables in DWARF#665
jgarzik merged 2 commits into
mainfrom
updates

Conversation

@jgarzik

@jgarzik jgarzik commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

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.

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>
@jgarzik
jgarzik requested a lite review from Copilot August 24, 2026 01:23
@jgarzik jgarzik self-assigned this Aug 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 .sleb128 directive 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.

Comment thread cc/arch/dwarf.rs Outdated
`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>
@jgarzik
jgarzik merged commit fe94e07 into main Aug 24, 2026
16 checks passed
@jgarzik
jgarzik deleted the updates branch August 24, 2026 02:20
@jgarzik
jgarzik restored the updates branch August 24, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants