You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Proposal: Runtime Funcinfo Metadata and Fast Runtime Lookup
Goal
Give LLGo Go-compatible runtime introspection — runtime.Caller, Callers, CallersFrames, FuncForPC, Func.FileLine, runtime.Stack, panic tracebacks — without DWARF, llvm-symbolizer or libunwind on the primary path, with DCE safety, controlled binary size, and performance at or beyond Go.
Architecture (three layers + a conformance gate)
Metadata model (runtime: add Go-style funcinfo find index #2012, Stage 1) — Go-1.26-style compact function records + shared string pools + statement-level pcline records, emitted as DCE-safe sections: ELF SHF_LINK_ORDER (honored by --gc-sections), Mach-O live_support + one linker-private atom symbol per record (verified under ld64/lld -dead_strip, incl. LTO). findfunctab geometry follows Go (4KiB buckets × 16 subbuckets) with one deliberate deviation: uint16 subbucket deltas — LLGo has no 16-byte MINFUNC guarantee, so Go's uint8 could overflow; uint16 makes it structurally impossible. Runtime falls back to first-use table construction when the link-phase table is absent.
Link-phase table (runtime: link-phase ftab/findfunctab generation (Stage 2, P1–P3) #2016, Stage 2 P1–P3) — after linking, internal/pclnpost deduplicates records against the symbol table (FNV: a record is canonical iff its anchor's owning symbol hashes to its symbolID, or is that symbol's __llgo_stub. wrapper — LTO inline copies fail exactly), sorts, and rewrites the entry section in place with a prebuilt ftab+findfunctab the runtime adopts zero-copy. Mach-O specifics proved out: chained-fixup chain surgery, BIND-node decoding through the imports table (exported-function anchors are binds, not rebases), the header base slot spliced back as a live rebase so dyld writes the slid address, ad-hoc re-sign only when the input was signed. Overflow spills the blob into the larger stub section behind a LLGOFTB2 redirect — rows are never dropped (a gap-y table silently mis-names function-value pcs). Startup pre-warm (adopt + page-touch + one synthetic lookup) mirrors Go, whose pclntab is warm by main.
Physical unwinder (runtime: LLGo-owned frame-pointer unwinder (Stage 5) #2019, Stage 5) — every Go function on supported targets keeps the frame-pointer chain ("frame-pointer"="non-leaf"); Caller/Callers/CallersFrames/Stack and the unrecovered-panic dump walk [fp]/[fp+w] directly, symbolized through layer 2's tables with Go's pc-1 return-address convention. The walk is bounded to the program's own text (libc frames without FP discipline decode as wild pcs). Shadow-stack instrumentation is no longer emitted (LLGO_SHADOW_STACK=1 keeps the legacy emitters one release); linux binaries no longer link -lunwind (the clite fallback walks FP with dladdr names). Semantics are gc ground truth — physical stacks show every real frame; skip counts verified against go (interface chain MARK at skip 3, closure at 4). Two hardening rules: the compiler records its FP decision in a per-binary __llgo_fp_chain byte the runtime trusts (no inference from table presence), and every lookup path refines function records through one refinePCSymbolLine helper (statement record at pc, then pc-1) so FuncForPC/FileLine/CallersFrames cannot disagree on line attribution — the class of bug behind all six amd64 debug rounds.
User-visible conformance (runtime,cl: Go-style panic tracebacks and exact log/slog/testing locations, with gc-verified acceptance suite #2023, acceptance) — what application developers actually see, held to an agreed bar: format and user-code file:line match gc exactly; runtime-internal, patched-stdlib and startup frames may differ. Delivered on top of runtime: LLGo-owned frame-pointer unwinder (Stage 5) #2019: gc-style unrecovered-panic tracebacks (goroutine 1 [running]: + fn(...) + file:line +0x<pc-entry>, via a runtime hook with clite fallback); exact log.Lshortfile/slog AddSource/t.Errorf locations (previously ???:1 — the blanket stdlib tracking exclusion is gone, the same per-package reaches-runtime.Caller analysis now applies uniformly, callers of pc-consuming functions keep frames + statement anchors); main display naming regardless of module name, closures as gc's .funcN; main.main/init///go:noinline frames pinned (noinline is free there) so panic bottom frames are line-exact. Locked in by a gc-verified acceptance suite (test/go/caller_acceptance_test.go: panic, log/slog text+JSON, llgo test failure lines, goroutine/init/defer callers, FuncForPC names, errors-with-stack idiom). Cost: hello-world +368 bytes, plain.* untouched, no golden changes.
Data structures and lookup algorithm
compile time (per package) link phase (pclnpost, per binary) run time
-------------------------- --------------------------------- --------
func f() { emitted as 1. parse entry/stub anchor sections adopt zero-copy
funcinfo record ──┐ DCE-safe 2. FNV ownership: record canonical (validate magic,
pcline anchors ──┤ sections iff anchor's owning symbol hashes read dyld-slid
(per call site) │ (ELF to its symbolID (LTO inline base pointer)
} │ link-order / copies fail exactly) → dedup
string pools ──────┘ Mach-O 3. sort by entry, rewrite in place:
(pkg/name/file, live_support) ┌─────────────────────────────┐
content-deduped) │ header: magic, text base ●──┼─ spliced into fixup
│ ftab: {entryOff, funcIdx}[] │ chain as live rebase
records: {symPkg,symName,namePkg, │ findfunctab: 4KiB buckets │ (overflow → LLGOFTB2
nameName,fileRoot,fileName,line} │ × 16 × uint16 delta │ spill into stub sec)
as u16 indexes into string pools └─────────────────────────────┘
PC → location (the only symbolization rule, refinePCSymbolLine):
pc (return address) ── pc-1 ──► off = pc − base ← Go's convention: attribute
bucket = off >> 12 (4KiB) the call, not the next stmt
sub = (off >> 8) & 15
i = bucketBase[bucket] + uint16 subDelta[sub]
while ftab[i+1].entryOff ≤ off: i++ ← short forward scan
funcIndex → record → symbol / name / file / decl line
pcline nearest-below(pc, same entry), then (pc-1) ← statement-exact line for
→ statement line tracked frames
caches: 4-way set-assoc pc cache → per-ftab-row *Func → frameSymbolResult (per-pc memo)
Physical stack walk (fpCallers; same discipline in the clite C fallback):
stack (grows down) every Go function keeps x29/rbp chained
┌─────────────┐ ("frame-pointer"="non-leaf"):
│ caller frame│◄─┐ ret = [fp + wordsize] → pc[n++] (must lie in program text)
│ saved ret ─┼──┼─►pc prev = [fp] (strictly increasing, ≤1MiB stride,
│ saved fp ──┼──┘ fp = prev word-aligned, else stop)
└─────────────┘ availability = per-binary __llgo_fp_chain byte × tables present
Frame-keeping analysis (per package, cached): track a function — noinline + no-tail-call
statement anchors — iff it (1) transitively reaches runtime.Caller/Callers within its
package, or (2) statically calls another package's criterion-(1) function (the frame a
fixed Caller depth attributes), or (3) is main.main / init (program-unique, noinline is
free), or (4) carries //go:noinline anyway. Everything else stays fully optimizable.
#2019 — all green (48 checks incl. codecov), merge-ready; review cleanup folded in (#2022 closed)
5b
Acceptance & conformance
#2023 — based on #2019; first CI round all green, conformance round in CI
5c
goroot conformance
#2024 — based on #2023: //line spelling, --icf=none pc identity, 12 xfails retired
5d
Panic-site snapshots
#2026 — based on #2023: deferred callers & fault stacks see panic frames (C→Go fault chains, SIGFPE/SIGBUS recoverable); 3 more xfails retired
5e
Heap profiling
#2027 — based on #2024 (needs --icf=none): gc-shaped sampled memory profiles, exponential thresholds, raw counts; heapsampling green on darwin+linux arm64
5f
Fault stacks
#2028 — based on #2019: hardware faults recoverable (incl. SIGFPE) with fault-site C→Go stacks via dynamically-resolved libunwind (no link dependency; nongnu flavor names static C symbols from .symtab) + FP-chain fallback; overlaps #2026's fault half — second lander rebases
What each PR owns (with final numbers, best-of, mac/linux)
same geometry, uint16 deltas (no MINFUNC guarantee)
overflow structurally impossible
Platform support (three gates: FP attr → walker → symbolization)
Target
Status
darwin arm64/amd64, linux arm64/amd64
green in CI; AAPCS64 mandates FP, attr forces it on x86-64
embedded (ESP32…) / wasm
attr off by design (NeedsFramePointer): unwinder doesn't exist there (runtime gated !baremetal && !wasm); FP layout change also perturbed the conservative GC on ESP32-C3. Behavior = pre-Stage-5; LLGO_SHADOW_STACK=1 restores caller tracking — on wasm the shadow stack is the only possible mechanism
BSDs / android / ios
same ELF/Mach-O + SysV/AAPCS mechanics; expected to be a GOOS-whitelist change only
windows
needs COFF equivalents of the DCE-safe sections first (bigger than the unwinder); then either clang-kept RBP chains with the same walker, or SEH RtlVirtualUnwind. Currently attr off → graceful degradation
riscv64 / arm32
known gap: frame layout differs (RISC-V: ra at fp−8, prev fp at fp−16), walker offsets are arm64/x86-64-specific; the FP gate should whitelist arm64/amd64 explicitly until per-arch offsets land
Obstacle assessment for full support: BSD/android/ios — mechanical (GOOS whitelist). riscv/arm32 — per-arch walker offsets (~10 lines each; arm32 thumb/arm FP registers unify under clang's attr). Windows — bounded, path known: COFF IMAGE_COMDAT_SELECT_ASSOCIATIVE is the direct link-order analog, PE .reloc rewriting is simpler than the Mach-O chained-fixups we already handle, and clang keeps RBP chains on Win x64 so the same walker works (SEH optional); the real gate is LLGo's Windows runtime base, not funcinfo. wasm — physical unwinding is impossible by spec: full support means shadow stack as the permanent wasm backend (escape hatch exists; flip to default there), and wasm-ld section-GC semantics for the metadata need dedicated verification. Embedded — full support should be opt-in metadata (LLGO_FUNCINFO=0 exists) + P4d shrink given flash budgets; the conservative-GC/FP interaction is gated off today and only a precise-scanning project would lift it.
Fallback ladder guarantees no platform regresses below Stage 1: tables (FuncForPC/FileLine) work everywhere; Callers falls back shadow-stack → clite FP walk.
Follow-ups (priority order)
P4a zero-copy joined names (unsafe.String views over pre-joined strings): removes the 0.4–1.7µs per-function materialization — the only remaining structural gap vs Go on fresh-pc and >4k-target batch lookups; also enables per-funcIndex Func caching. Size cost ~+150–250KB stdlib (still ≪ Go's 1.41MB pclntab).
P4b prebuilt pcline + pcvalue-style instruction lines: kills the first-use pcline build (~55–400µs once at scale), the two remaining first-use cells in runtime: LLGo-owned frame-pointer unwinder (Stage 5) #2019, and the last line-exactness gap — plain user functions mid-panic-traceback show declaration-adjacent lines because statement anchors structurally require the owning function to survive (ELF link-order symbol), so full coverage needs a metadata mechanism instead of body asm.
P4c !pcsections as the site mechanism: removes the ±% inline/layout perturbation of body-embedded site asm.
P4d section shrink: name pool dominates at extreme function counts (96×96: 15.4 vs Go 9 MiB; typical scenarios already ≤ Go).
4b. Foreign-function hole sentinels (pclnpost sees the full symbol table): insert non-Go boundary rows so a C pc between Go functions stops resolving to the preceding Go record — fixes linux fault-stack C-frame naming/misattribution (darwin already corrected by a dladdr cross-check) and general FuncForPC on C pcs. Also evaluate ELF .addrsig + --icf=safe here if the +1–2% of --icf=none ever matters.
Stage 4 stub removal (stubs already merged into the prebuilt ftab; mainly deletes generation), Stage 3 LTO policy.
Unwinder arch ports per the platform table; inline-tree (with P4b) to lift noinline from tracked functions (stdlib.Work's residual ~23µs vs Go 6.2µs is noinline + LLGo baseline, not unwind cost).
Benchmarks: benchmark/runtime_funcinfo (in-tree since runtime: link-phase ftab/findfunctab generation (Stage 2, P1–P3) #2016) covers hot/deep/multipkg/cold/stdlib/plain plus -scales/-depths/-bigsizes; codecov ignores benchmark/chore/cltest; coverage uploads from both OSes (single-platform uploads mark the other OS's branches dead).
Proposal: Runtime Funcinfo Metadata and Fast Runtime Lookup
Goal
Give LLGo Go-compatible runtime introspection —
runtime.Caller,Callers,CallersFrames,FuncForPC,Func.FileLine,runtime.Stack, panic tracebacks — without DWARF,llvm-symbolizeror libunwind on the primary path, with DCE safety, controlled binary size, and performance at or beyond Go.Architecture (three layers + a conformance gate)
SHF_LINK_ORDER(honored by--gc-sections), Mach-Olive_support+ one linker-private atom symbol per record (verified under ld64/lld-dead_strip, incl. LTO). findfunctab geometry follows Go (4KiB buckets × 16 subbuckets) with one deliberate deviation: uint16 subbucket deltas — LLGo has no 16-byte MINFUNC guarantee, so Go's uint8 could overflow; uint16 makes it structurally impossible. Runtime falls back to first-use table construction when the link-phase table is absent.internal/pclnpostdeduplicates records against the symbol table (FNV: a record is canonical iff its anchor's owning symbol hashes to its symbolID, or is that symbol's__llgo_stub.wrapper — LTO inline copies fail exactly), sorts, and rewrites the entry section in place with a prebuiltftab+findfunctab the runtime adopts zero-copy. Mach-O specifics proved out: chained-fixup chain surgery, BIND-node decoding through the imports table (exported-function anchors are binds, not rebases), the header base slot spliced back as a live rebase so dyld writes the slid address, ad-hoc re-sign only when the input was signed. Overflow spills the blob into the larger stub section behind aLLGOFTB2redirect — rows are never dropped (a gap-y table silently mis-names function-value pcs). Startup pre-warm (adopt + page-touch + one synthetic lookup) mirrors Go, whose pclntab is warm bymain."frame-pointer"="non-leaf");Caller/Callers/CallersFrames/Stackand the unrecovered-panic dump walk[fp]/[fp+w]directly, symbolized through layer 2's tables with Go'spc-1return-address convention. The walk is bounded to the program's own text (libc frames without FP discipline decode as wild pcs). Shadow-stack instrumentation is no longer emitted (LLGO_SHADOW_STACK=1keeps the legacy emitters one release); linux binaries no longer link-lunwind(the clite fallback walks FP with dladdr names). Semantics are gc ground truth — physical stacks show every real frame; skip counts verified against go (interface chain MARK at skip 3, closure at 4). Two hardening rules: the compiler records its FP decision in a per-binary__llgo_fp_chainbyte the runtime trusts (no inference from table presence), and every lookup path refines function records through onerefinePCSymbolLinehelper (statement record at pc, then pc-1) soFuncForPC/FileLine/CallersFramescannot disagree on line attribution — the class of bug behind all six amd64 debug rounds.goroutine 1 [running]:+fn(...)+file:line +0x<pc-entry>, via a runtime hook with clite fallback); exactlog.Lshortfile/slogAddSource/t.Errorflocations (previously???:1— the blanket stdlib tracking exclusion is gone, the same per-package reaches-runtime.Caller analysis now applies uniformly, callers of pc-consuming functions keep frames + statement anchors);maindisplay naming regardless of module name, closures as gc's.funcN;main.main/init///go:noinlineframes pinned (noinline is free there) so panic bottom frames are line-exact. Locked in by a gc-verified acceptance suite (test/go/caller_acceptance_test.go: panic, log/slog text+JSON,llgo testfailure lines, goroutine/init/defer callers, FuncForPC names, errors-with-stack idiom). Cost: hello-world +368 bytes,plain.*untouched, no golden changes.Data structures and lookup algorithm
PC → location (the only symbolization rule,
refinePCSymbolLine):Physical stack walk (
fpCallers; same discipline in the clite C fallback):Frame-keeping analysis (per package, cached): track a function — noinline + no-tail-call
package, or (2) statically calls another package's criterion-(1) function (the frame a
fixed Caller depth attributes), or (3) is main.main / init (program-unique, noinline is
free), or (4) carries //go:noinline anyway. Everything else stays fully optimizable.
Stage / PR status
main+LTO mac crash worth root-causing there)!pcsections, section shrink, stub removal--icf=nonepc identity, 12 xfails retiredWhat each PR owns (with final numbers, best-of, mac/linux)
Reference bar: Go 1.26 hot.Caller0 155/241ns; per-fresh-pc FuncForPC 40–125ns (zero-copy pclntab views) vs LLGo 0.4–1.7µs (string materialization — the headline P4 item).
plain.*(fib/json/sort/map) is identical across all variants: code that never introspects pays nothing. Scale sweeps (48×48/96×96 pkgs×methods, depth 128/512, big methods 32×200/16×2000) hardened #2016 (overflow spill, per-row Func cache — 96×96 batch lookups 8–19ms → 147µs +LTO, beating Go's 179µs) and quantified the shadow-stack tax (~160ns/frame) that #2019 removed.Differences from Go's implementation (by design or accepted)
noinlinesiglongjmpunwinds first; #2026 snapshots the panic pcs and splices them into caller-info views (validity tied to the recovering frame's liveness)Callerdepth through the gopanic-equivalent frame; panicking-statement lines in untracked functions await P4bmain.,.funcN), linker symbols keep module path /$NFuncForPCreports the surviving symbol (issue58300; #1924 reimpl)fn(args…)+file:line +0xofffn(...)+ same shape; offset values differ (different codegen)goroutine N [state],runtime.main/goexittail, Stack(all=true)goroutine 1 [running], walk stops at program text, Stack(all) = current only__llgo_fp_chainbyte + funcinfo tables; embedded/wasm keep tables, shadow-stack escape hatchPlatform support (three gates: FP attr → walker → symbolization)
NeedsFramePointer): unwinder doesn't exist there (runtime gated!baremetal && !wasm); FP layout change also perturbed the conservative GC on ESP32-C3. Behavior = pre-Stage-5;LLGO_SHADOW_STACK=1restores caller tracking — on wasm the shadow stack is the only possible mechanismRtlVirtualUnwind. Currently attr off → graceful degradationObstacle assessment for full support: BSD/android/ios — mechanical (GOOS whitelist). riscv/arm32 — per-arch walker offsets (~10 lines each; arm32 thumb/arm FP registers unify under clang's attr). Windows — bounded, path known: COFF
IMAGE_COMDAT_SELECT_ASSOCIATIVEis the direct link-order analog, PE.relocrewriting is simpler than the Mach-O chained-fixups we already handle, and clang keeps RBP chains on Win x64 so the same walker works (SEH optional); the real gate is LLGo's Windows runtime base, not funcinfo. wasm — physical unwinding is impossible by spec: full support means shadow stack as the permanent wasm backend (escape hatch exists; flip to default there), and wasm-ld section-GC semantics for the metadata need dedicated verification. Embedded — full support should be opt-in metadata (LLGO_FUNCINFO=0exists) + P4d shrink given flash budgets; the conservative-GC/FP interaction is gated off today and only a precise-scanning project would lift it.Fallback ladder guarantees no platform regresses below Stage 1: tables (FuncForPC/FileLine) work everywhere; Callers falls back shadow-stack → clite FP walk.
Follow-ups (priority order)
unsafe.Stringviews over pre-joined strings): removes the 0.4–1.7µs per-function materialization — the only remaining structural gap vs Go on fresh-pc and >4k-target batch lookups; also enables per-funcIndex Func caching. Size cost ~+150–250KB stdlib (still ≪ Go's 1.41MB pclntab).!pcsectionsas the site mechanism: removes the ±% inline/layout perturbation of body-embedded site asm.4b. Foreign-function hole sentinels (pclnpost sees the full symbol table): insert non-Go boundary rows so a C pc between Go functions stops resolving to the preceding Go record — fixes linux fault-stack C-frame naming/misattribution (darwin already corrected by a dladdr cross-check) and general
FuncForPCon C pcs. Also evaluate ELF.addrsig+--icf=safehere if the +1–2% of--icf=noneever matters.noinlinefrom tracked functions (stdlib.Work's residual ~23µs vs Go 6.2µs is noinline + LLGo baseline, not unwind cost).PR housekeeping / merge order
benchmark/runtime_funcinfo(in-tree since runtime: link-phase ftab/findfunctab generation (Stage 2, P1–P3) #2016) covers hot/deep/multipkg/cold/stdlib/plain plus-scales/-depths/-bigsizes; codecov ignores benchmark/chore/cltest; coverage uploads from both OSes (single-platform uploads mark the other OS's branches dead).Acceptance criteria