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.
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:
The system has to answer several semantic questions before codegen:
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-dwarfthe 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:
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-dwarfghostscope-compiler/src/ebpf/dwarf_bridge.rsIf one path handled a case and another did not, the same source variable could behave differently depending on where it appeared in the script.
EvaluationResultwas also too low-level. It told us something about expression evaluation, but not enough about user-facing access semantics.Examples:
Fallback behavior could hide incorrect reads. During incremental development, fallback paths made more scripts compile, but they also allowed wrong reads.
Problematic outcomes included:
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-dwarfshould build a semantic read plan at a specific probe PC.Diagram:
DWARF DIE / location list / type / lexical scope
-> PC-context semantic analysis
-> read plan or diagnostic
-> compiler lowering
-> eBPF runtime read
The important shift is:
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:
Conceptually, the contract has three layers:
Read identity:
PC context:
Read strategy:
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:
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_regis intentional terminology. It means the DWARF register number defined by the target's DWARF ABI.It does not mean:
pt_regsindexpt_regsraxorx0The plan layer should stay architecture-neutral:
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:
It also records:
This lets
ghostscope-dwarfcommunicate 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-dwarfOwns source-variable semantics:
ghostscope-compilerOwns code generation:
ghostscope-dwarffor read plansghostscope-platformfor register-to-runtime mappingThe 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:
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:
Clearer Diagnostics
Planning happens before eBPF lowering, so many failures can become static compile-time diagnostics.
The intended split is:
Cleaner Compiler Boundary
The compiler no longer needs to know as much about DWARF internals. It consumes a plan.
That reduces duplicated logic between:
Better Foundation For Future Features
A stable read-plan boundary gives us a better place to add:
Non-Goals
This proposal does not include:
Those should build on top of the read-plan boundary rather than bypass it.