Skip to content

[RFC] DWARF variable access via PC-context read plans #148

@swananan

Description

@swananan

Background

GhostScope's core value is source-aware userspace tracing: attach to a live process at a function, source line, or instruction, then read source-level variables without stopping the target.

To do that, GhostScope must turn DWARF debug information into executable eBPF reads.

For example, when a script asks to print a nested source variable at a source line:

trace foo.c:42 {
    print req.headers_in.content_length_n;
}

The system has to answer several semantic questions before codegen:

  • Is the base variable visible at this exact probe PC?
  • Is it a local, a parameter, a global, or a shadowed outer variable?
  • Is the value in memory, in a DWARF register, computed from a DWARF expression, or optimized out?
  • Is the base an address-backed aggregate, or an optimized value-backed aggregate?
  • Does a link-time address need PIE/shared-library rebasing?
  • Can this access be lowered into the current eBPF backend safely?
  • If not, should the user see a compile-time diagnostic, an optimized-out marker, or a runtime read failure?

Historically, GhostScope answered these questions across multiple layers. The DWARF crate produced evaluation/location-like results, and the compiler still had to interpret enough DWARF semantics to decide how to lower them.

This RFC proposes making ghostscope-dwarf the semantic owner of source variable access decisions.

Previous Architecture: Evaluation-Oriented Output

The previous architecture was centered around evaluating DWARF locations and passing the resulting shape to the compiler.

Diagram:

DWARF DIE / location list / location expression
-> evaluate DWARF expression
-> evaluation-shaped result
-> compiler-specific adaptation
-> eBPF lowering

That evaluation-shaped result described what a DWARF expression appeared to produce: an address, a register value, a computed value, an optimized-out marker, or an unsupported form.

That information is necessary, but it is not a complete compiler contract.

For example, knowing that a DWARF expression produced a computed value does not answer:

  • Is this computed value a scalar or an aggregate?
  • Can a member offset be applied to it?
  • Should it be read from memory, used directly, or rejected?
  • Is it a link-time address that needs runtime rebasing?
  • Is it supported by the current runtime/backend capabilities?
  • If it fails, what should the user see?

Those questions are source-variable access semantics, not just expression evaluation results.

Problems With The Old Model

DWARF semantics leaked into the compiler. The compiler had to interpret low-level DWARF-derived states and decide how to lower them.

That meant DWARF-specific reasoning existed in multiple places:

  • ghostscope-dwarf
  • ghostscope-compiler/src/ebpf/dwarf_bridge.rs
  • codegen paths for print, expressions, aliases, member access, and builtins

If one path handled a case and another did not, the same source variable could behave differently depending on where it appeared in the script.

EvaluationResult was also too low-level. It told us something about expression evaluation, but not enough about user-facing access semantics.

Examples:

  • Absolute addresses need ASLR/PIE rebasing when used as runtime addresses.
  • Optimized-out variables should be preserved for print metadata, but rejected as normal value expressions.
  • A value-backed aggregate cannot safely accept a field offset unless value extraction is implemented.
  • A variable visible in an inner lexical scope should not silently fall back to an outer same-named variable if the inner location is unsupported.

Fallback behavior could hide incorrect reads. During incremental development, fallback paths made more scripts compile, but they also allowed wrong reads.

Problematic outcomes included:

  • A shadowed variable could fall back to an outer variable.
  • A duplicate global/static could bind to the first matching module entry.
  • A direct-address trace could fail codegen but still appear processed.
  • A value-backed aggregate field could be lowered as if it were memory-backed.

These failures are worse than compile errors because they can produce traces that look valid but read the wrong data.

Diagnostics were not first-class. Unsupported DWARF forms often surfaced late, after compiler adaptation or eBPF lowering had already started.

The system often knew that something failed, but not always which variable, PC, access path, DWARF capability, or lowering rule caused it.

Proposal

Move source-variable access to a PC-context read-plan architecture.

Instead of exposing raw evaluation-like results as the main compiler input, ghostscope-dwarf should build a semantic read plan at a specific probe PC.

Diagram:

DWARF DIE / location list / type / lexical scope

  • probe PC
  • source access path
    -> PC-context semantic analysis
    -> read plan or diagnostic
    -> compiler lowering
    -> eBPF runtime read

The important shift is:

  • Old question: what did the DWARF expression evaluate to?
  • New question: what is the safe, typed way to read this source variable at this PC?

The read plan does not try to hide physical storage. It still needs to say whether the value lives in memory, a DWARF register, an implicit value, a computed expression, or pieces.

That is why the location part of a read plan looks similar to the old evaluation output. The physical vocabulary is mostly the same because DWARF is describing the same machine state. The difference is that the location is now inside a larger semantic contract.

Read Plan Contract

A read plan is a compiler-facing contract for one source-variable read.

It should answer:

  • Which source variable was selected.
  • Which PC and inline context made it visible.
  • Which lexical scope won shadowing resolution.
  • Which module/global candidate was selected.
  • Which source type and layout are being read.
  • Which access path is being applied.
  • Which physical storage shape backs the value.
  • Whether the value is available, optimized out, or unsupported.
  • Whether the current backend/runtime can lower the read.
  • If not lowerable, which diagnostic should be reported.

Conceptually, the contract has three layers:

Read identity:

  • variable name
  • declaration identity
  • type identity
  • source type/layout
  • lexical scope depth
  • parameter/artificial flags
  • provenance

PC context:

  • probe PC
  • PC range selected from location lists
  • inline context when available
  • preferred module for global/static lookup

Read strategy:

  • location shape
  • availability
  • runtime requirements
  • helper strategy
  • required DWARF registers
  • stack/verifier risk

Location Shapes

The location part is intentionally concrete. It is where the plan says what the compiler must actually read.

Supported or planned shapes include:

  • Memory address expression: read user memory at a known or computed address.
  • Absolute address value: a direct pointer-like value that may require ASLR/PIE rebasing when used as a runtime address.
  • DWARF register value: use the value currently held in a DWARF register.
  • DWARF-register-relative address: read memory at register value plus byte offset.
  • Frame-base-relative address: read memory relative to a resolved frame base.
  • Computed value: run a lowered sequence of DWARF-derived compute steps.
  • Computed address: compute an address, then read user memory there.
  • Implicit value: use bytes embedded directly in DWARF.
  • Pieces: represent a value split across multiple storage pieces.
  • Optimized out or unknown: preserve unavailability instead of guessing.

This is the point where the similarity with the old evaluation model is expected. A read plan should not invent fake high-level storage. It should preserve low-level storage accurately, while adding enough context to make lowering deterministic and safe.

Cross-Platform Register Semantics

dwarf_reg is intentional terminology. It means the DWARF register number defined by the target's DWARF ABI.

It does not mean:

  • a Linux pt_regs index
  • a byte offset inside pt_regs
  • a textual architecture register name such as rax or x0

The plan layer should stay architecture-neutral:

  • A DWARF register value means "use this DWARF register number as a value."
  • A DWARF-register-relative address means "use this DWARF register number as a base address, then add a byte offset."
  • Lowering maps target architecture plus DWARF register number to the concrete runtime register source.

In the current implementation, practical register lowering is x86_64-oriented. The platform crate provides the mapping boundary, but the generic mapping still routes to x86_64 today. ARM64/RISC-V mappings need to be added before claiming complete cross-platform register lowering.

Lowering View

The read plan also supports a derived lowering view for the eBPF compiler.

That view classifies the read as one of:

  • direct value
  • user-memory read
  • composite value
  • unavailable value

It also records:

  • runtime requirements, such as user-memory reads or CFI recovery
  • helper mode, such as normal user-memory read or no helper needed
  • required DWARF registers
  • estimated stack usage
  • verifier risk
  • final availability after applying runtime/backend capabilities

This lets ghostscope-dwarf communicate more than a location. It can say that a read needs user-memory helpers, that a frame recovery feature is required, or that a plan exceeds the current BPF stack budget.

The current branch should treat this as an internal contract, not a frozen public API. The stable invariant is:

Compiler consumes PC-context read plans, not raw DWARF evaluation results.

The exact fields can still evolve as diagnostics, pieces, CFI recovery, and value-backed aggregate extraction become richer.

Responsibility Split

ghostscope-dwarf

Owns source-variable semantics:

  • PC-specific lexical scope resolution
  • inline context handling
  • local, parameter, and global lookup
  • current-module preference for globals
  • ambiguous global detection
  • variable shadowing behavior
  • type and member layout
  • access-path planning for fields, arrays, and pointer dereference
  • DWARF location expression lowering into semantic location plans
  • optimized-out and unavailable diagnostics
  • ASLR-sensitive address semantics
  • lowering availability from runtime/backend capabilities

ghostscope-compiler

Owns code generation:

  • parse the GhostScope DSL
  • ask ghostscope-dwarf for read plans
  • reject unavailable plans in value-expression contexts
  • preserve optimized-out markers where user output expects them
  • lower supported plans into LLVM/eBPF instructions
  • use ghostscope-platform for register-to-runtime mapping
  • generate runtime read/status handling for eBPF

The compiler should not rediscover DWARF semantics from raw locations. It should lower the explicit plan it receives.

Before Vs After

Before:

script variable
-> query DWARF with probe PC
-> get evaluation-shaped result for the base variable/location
-> compiler adapts result
-> compiler applies access-path, member, alias, pointer, and fallback rules
-> compiler decides whether it is lowerable
-> eBPF codegen

Risk:

The same source variable could take different semantic paths depending on the expression context.

After:

script variable
-> query DWARF with probe PC and access path
-> get PC-context read plan or diagnostic
-> compiler lowers explicit plan
-> eBPF codegen

Benefit:

There is one semantic decision point before lowering.

Expected Value

More Correct Optimized-Code Behavior

Optimized builds are exactly where raw DWARF evaluation is not enough.

Variables may be:

  • optimized out
  • available only in a narrow PC range
  • moved between registers and stack
  • represented as computed values
  • split across pieces
  • visible only through inline callsite context
  • shadowed by same-named locals

A PC-context read plan forces lookup to answer availability at the selected PC before codegen.

Fewer Silent Wrong Reads

The architecture should prefer compile-time rejection over unsafe fallback.

Examples:

  • If an inner visible variable has a diagnostic, do not read an outer same-named variable.
  • If field access targets a value-backed aggregate, reject until extraction is implemented.
  • If duplicate globals cannot be disambiguated by current-module preference, report ambiguity.
  • If direct-address trace codegen fails, propagate the failure instead of returning success with zero uprobes.

Clearer Diagnostics

Planning happens before eBPF lowering, so many failures can become static compile-time diagnostics.

The intended split is:

  • Static DWARF, planning, or lowering problem: compile-time diagnostic.
  • Runtime memory, page, or permission problem: runtime status/error reporting.

Cleaner Compiler Boundary

The compiler no longer needs to know as much about DWARF internals. It consumes a plan.

That reduces duplicated logic between:

  • print
  • aliases
  • value expressions
  • builtin arguments
  • member access
  • address-of
  • direct address traces

Better Foundation For Future Features

A stable read-plan boundary gives us a better place to add:

  • richer compile-time diagnostics
  • runtime diagnostic side tables
  • broader DWARF expression support
  • value-backed aggregate extraction
  • piece and bit-piece support
  • more complete inline variable recovery
  • unwinding/CFI integration
  • broader architecture-specific register lowering

Non-Goals

This proposal does not include:

  • a full DWARF VM in eBPF
  • full optimized aggregate value extraction
  • runtime rich diagnostic transport
  • protocol wire-format changes
  • USDT probes
  • complete unwinding support
  • complete ARM64/RISC-V register lowering

Those should build on top of the read-plan boundary rather than bypass it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions