runtime: add configurable pclntab packaging - #2120
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR adds a configurable LLGo policy for packaging runtime function/line metadata (pclntab) with embedded (default), external sidecar loading, and none omission, and completes the command/build boundary for Go-compatible -gcflags/-ldflags parsing (including typed -s/-w handling and removal of legacy LLGO_DEBUG* switches). It also introduces a versioned external sidecar format plus post-link analysis/detachment tooling, updates runtime symbolization entry points to lazily install external metadata on safe paths, and adds fixed binary-size workloads and updated documentation.
Changes:
- Add
-pclntab={embedded,external,none}support end-to-end (build config, post-link externalization, runtime lazy loader/probe, and sidecar format). - Move Go build/link flag parsing into
internal/goflagsand wire all relevant commands to apply typed linker/debug intent (-s/-w), removingLLGO_DEBUG/LLGO_DEBUG_SYMBOLS. - Add tests/fixtures/docs plus binary-size benchmark workloads to validate behavior and track size impact.
Reviewed changes
Copilot reviewed 73 out of 73 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| runtime/internal/runtime/z_rt.go | Invoke a runtime hook on recovered panics to clear auxiliary traceback state. |
| runtime/internal/lib/runtime/unwind_llgo.go | Install panic hooks and ensure external pclntab is loaded only on I/O-safe panic paths. |
| runtime/internal/lib/runtime/pprof_runtime_stub_llgo.go | Ensure external metadata is installed before consulting FuncForPC caches. |
| runtime/internal/lib/runtime/pclntab_static.go | Stub ensureRuntimePCLN/runtimePCLNReady for non-external builds. |
| runtime/internal/lib/runtime/pclntab_external_linux.go | Linux executable-path discovery for sidecar loading. |
| runtime/internal/lib/runtime/pclntab_external_darwin.go | Darwin executable-path discovery for sidecar loading. |
| runtime/internal/lib/runtime/pclntab_external_arm64.go | External sidecar GOARCH encoding for arm64. |
| runtime/internal/lib/runtime/pclntab_external_amd64.go | External sidecar GOARCH encoding for amd64. |
| runtime/internal/lib/runtime/fault_unwind_llgo.go | Track/clear “fault-active” state to preserve async-signal no-I/O behavior through fatal panics. |
| runtime/internal/lib/runtime/extern.go | Ensure metadata is installed before Caller/Callers fast paths; adjust Caller PC semantics. |
| internal/pclnpost/macho_fixture_test.go | Extend Mach-O fixtures to cover external pclntab sections and PAGEZERO base-discovery behavior. |
| internal/pclnpost/external.go | Add post-link external analysis, binary detachment, section clearing, and (Darwin) re-signing flow. |
| internal/pclnpost/elf_fixture_test.go | Extend ELF fixtures to include optional pcline + identity sections for externalization tests. |
| internal/pclnpost/binary.go | Strengthen binary loading/bounds checks; track pointer size/endianness and new sections. |
| internal/pclnpost/binary_test.go | Add tests for bounds helpers and Mach-O chained-fixups parsing robustness. |
| internal/pclnmap/pclnmap.go | Define and implement the versioned external sidecar encoding + validation rules. |
| internal/pclnmap/pclnmap_test.go | Add encode/parse determinism + corruption/overflow/misalignment tests for sidecar format. |
| internal/llgen/llgenf.go | Switch golden-test flags parsing to Go-compatible flags.txt via internal/goflags. |
| internal/llgen/llgenf_test.go | Verify flags.txt parsing/application and error-path diagnostics include the file path. |
| internal/goflags/normalize.go | Normalize Go build flags into canonical -flag=value form, rejecting removed -dbg. |
| internal/goflags/ldflags.go | Parse Go -ldflags argument-list syntax into typed build.LinkOptions (+ ignored tokens). |
| internal/goflags/ldflags_test.go | Validate precedence/order/quoting/errors for typed -s/-w parsing. |
| internal/goflags/gobuild.go | Apply normalized Go build flags atomically into build.Config (raw + typed semantics). |
| internal/goflags/gobuild_test.go | Test normalization + atomicity guarantees for ApplyBuildFlags. |
| internal/goflags/gcflags.go | Map supported -gcflags subset into typed frontend config (Go version / O0). |
| internal/goflags/flagfile.go | Parse flags.txt with comments/quoting/backslashes while preserving argument-list flags. |
| internal/goflags/flagfile_test.go | Test flags.txt parsing formats, comment rules, escapes, and error cases. |
| internal/crosscompile/crosscompile.go | Introduce typed linker debug-info capability policy (DebugInfoPolicy). |
| internal/crosscompile/crosscompile_test.go | Verify native and fixed-target debug-info policies are exported correctly. |
| internal/build/testdata/ldflagsstrip/main.go | New runtime symbolization fixture for -s/-w integration coverage. |
| internal/build/pclntab_external.go | Implement external sidecar writing + detachment in the build finalization phase. |
| internal/build/pcln_mode.go | Add PCLNMode typed config, legacy-env normalization, and external capability validation. |
| internal/build/pcln_mode_test.go | Test mode validity, legacy precedence, site enabling rules, and sidecar cleanup. |
| internal/build/outputs.go | Add sidecar output path (.pclntab) and export it via environment maps. |
| internal/build/outputs_test.go | Validate sidecar path derivation and env-map wiring. |
| internal/build/link_options.go | Add typed DWARF intent, safe default policy, and linker-arg selection for omission. |
| internal/build/link_options_test.go | Test DWARF omission/preserve behavior and native-input DWARF suppression integration (ELF). |
| internal/build/funcinfo_table.go | Emit external-mode identity slot + keep payload out of IR; prepare build-side sidecar data. |
| internal/build/funcinfo_table_test.go | Ensure external mode avoids embedding funcinfo payloads in LLVM IR and keeps identity/live markers. |
| internal/build/fingerprint.go | Include EmitDWARF and PCLN mode in build fingerprints/manifests. |
| internal/build/collect.go | Record effective DWARF emission and effective PCLN mode into fingerprints/manifests. |
| internal/build/collect_test.go | Test fingerprint coverage for DWARF and PCLN policy, plus env canonicalization rules. |
| internal/build/clean.go | Remove .pclntab sidecars alongside executables on clean. |
| internal/build/clean_test.go | Verify cleaning removes executable + sidecar artifacts but not unrelated files. |
| internal/build/build.go | Wire typed flags/policies through build orchestration, tags, linking, and PCLN finalization. |
| internal/build/build_test.go | Add integration tests for typed LinkOptions effects on symbolization and debug artifacts. |
| doc/design/pclntab-packaging.md | Document the pclntab packaging modes, boundaries, and ordering/identity semantics. |
| cmd/internal/test/test.go | Capture and apply Go build flags via internal/goflags for llgo test. |
| cmd/internal/test/test_test.go | Assert build-flag wiring is correctly bound for the test command. |
| cmd/internal/run/run.go | Capture/apply Go build flags independently for run and cmptest. |
| cmd/internal/run/run_test.go | Verify run and cmptest have independent build-flag collectors and register -ldflags. |
| cmd/internal/install/install.go | Capture and apply Go build flags for llgo install. |
| cmd/internal/install/install_test.go | Assert build-flag wiring is correctly bound for the install command. |
| cmd/internal/flags/pclntab.go | Add the -pclntab CLI flag and map it to typed build.PCLNMode. |
| cmd/internal/flags/gobuild.go | Implement Go build-flag capture + typed application (ApplyGoBuildFlags). |
| cmd/internal/flags/flags.go | Register -pclntab and plumb it into build.Config update flow. |
| cmd/internal/flags/flags_test.go | Test Go build-flag application atomicity, -pclntab parsing, and legacy precedence. |
| cmd/internal/build/build.go | Switch llgo build to typed Go build-flag parsing/application. |
| cmd/internal/build/build_test.go | Update build command tests to include -ldflags capture behavior. |
| cmd/internal/base/pass.go | Remove legacy PassBuildFlags helper (replaced by flags.CaptureGoBuildFlags). |
| cl/_testdata/debug/flags.txt | Migrate debug fixture to Go-compatible -ldflags=-w=false spelling. |
| benchmark/binary_size/README.md | Add fixed workloads and measurement guidance for size comparisons. |
| benchmark/binary_size/cprintf/main.go | Add smallest lib/c.Printf workload (LLGo-only tag). |
| benchmark/binary_size/println/main.go | Add built-in println workload. |
| benchmark/binary_size/fmtprintf/main.go | Add fmt.Printf workload. |
| benchmark/binary_size/texttemplate/main.go | Add heavier text/template workload. |
| benchmark/binary_size/nilfault/main.go | Add import-free nil-deref failure workload for runtime diagnostics. |
| _lldb/README.md | Update LLDB instructions to use -ldflags=-w=false and explain the temporary DWARF default. |
| _lldb/common.sh | Update LLDB test harness to build with -ldflags=-w=false instead of env vars. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Add an LLGo-specific policy for packaging runtime function/line metadata:
-pclntab=embeddedkeeps metadata in the executable and remains the default.-pclntab=externalwrites an optional<executable>.pclntabsidecar and loads it once, lazily, from allocation/I/O-safe symbolization paths.-pclntab=nonedoes not generate the metadata and does not link the sidecar probe/loader path.Proposal
#2113 has merged. GitHub reports this PR clean, and its effective diff now contains only the pclntab implementation.
Closes #2112.
The change also finishes the command/build boundary needed by that stack: Go-compatible build syntax is parsed in
internal/goflags,flags.txtuses Go-native flags, and the legacyLLGO_DEBUG/LLGO_DEBUG_SYMBOLSswitches are removed. Because the existing DWARF path still changes program correctness and LTO validity, an absent-wtemporarily defaults to omitting DWARF; explicit-w=falseenables the runnable debug path.User-visible semantics
PCLN placement is independent of Go's linker flags. In particular,
-sand-wnever silently select-pclntab=none.-w-s-simplies-w-s -w=false-wwins regardless of orderExplicit
-wsemantics remain Go-compatible:-simplies-w, and an explicit-wvalue wins regardless of order. The temporary absent-flag default is intentionally different from Go: LLGo currently behaves as if-w=true, whereas Go behaves as if-w=false. Thus the default,-w, and-shave the same sizes today;-w=falseand-s -w=falseform the DWARF-enabled pair. Actual native symbol-table deletion remains a later phase and must run after external PCLN analysis.Initial
externalsupport is restricted to native Darwin/Linux amd64/arm64 executables without-target. Unsupported generation, archive/shared, named-target, and platform combinations fail with a capability diagnostic rather than silently changing behavior.Runtime and artifact behavior
mmap.nonefollows a generation-time omission path. It does not generate metadata and does not contain the external probe/loader code.lib/cprograms retain the executable identity even when the LLGo runtime itself is not linked.Go flag and debug migration
cmd/internal/flagsto sharedinternal/goflags;internal/buildconsumes typed configuration and no longer parses command strings.flags.txtaccepts blank lines, comments, shell-style quoting/backslashes, multiple ordinary flags,-flag=value,-flag value, and single/double-dash spellings. Argument-list flags such as-ldflags=-s -w=falsepreserve the Go-style list. The removed private-dbgspelling reports a migration error.-sand-wlinker semantics are translated in this phase. Other-ldflagstokens remain in the normalized raw list for future support; unpatterned andall=forms are interpreted, while other package patterns are rejected rather than applied globally.-wis absent.-w=falseexplicitly enables the existing runnable debug path;ModeGenalso emits DWARF only for explicit-w=false.LLGO_DEBUGandLLGO_DEBUG_SYMBOLSfrom implementation, build fingerprints, LLDB scripts, and documentation. Use-w=false,-w, and-O0instead; no replacement environment variable is introduced.This reuses LLGo's existing runnable DWARF implementation. It currently disables LLVM/C-ABI optimization while emitting DWARF, and this PR deliberately does not attempt a broad DWARF repair. Broad CI exercises the DWARF-free default; the debug IR fixture and LLDB suite explicitly use
-w=false. The underlying follow-ups are recorded separately:crypto/x509OID equality changes under the DWARF-enabled codegen path.The earlier default-DWARF CI run https://github.com/xgo-dev/llgo/actions/runs/29549966202 made the scope concrete: normal Linux demos reached 101/103, full-LTO demos only 43/103,
crypto/x509.TestOIDchanged behavior, and recursive DI types overflowed the compiler stack. The affected normal cases pass with-w. This PR therefore keeps DWARF explicitly available and continuously tested, but does not make the provisional path the broad default. No new default-control environment variable is added; restoring Go's absent-flag default later is one typed build-policy change.Fixed size workloads
Add source-only workloads under
benchmark/binary_size/for:lib/c.Printfhello world;printlnhello world;fmt.Printfhello world;text/templateprogram;The
lib/cworkload is taggedllgoso ordinarygo install ./...does not try to link LLGo-only C intrinsics; LLGo supplies that build tag automatically.Collection/history/CI tracking is intentionally deferred to a separate PR. Every variant below was fully warmed first and then built at the same exact output path. Executable and sidecar byte sizes were read from the filesystem and external totals were summed independently. Normal outputs were executed, and all nil-fault variants produced a nonzero diagnostic. External nil-fault variants were tested both with and without the sidecar.
On both platforms, the default,
-w, and-shave matching sizes in every workload/mode combination.-w=falseand-s -w=falsealso match each other. This reconciles all reported executable and sidecar sizes with the effective DWARF policy.The smallest Darwin
cprintfcase also exposes initial fixed overhead clearly. Its external executable contains no LLGo runtime and has the same 100-byte__textas embedded, so the increase is not lazy-loader code. It comes primarily from making a 16 KiB__DATApage file-backed plus Mach-O re-signing/__LINKEDITgrowth. Its roughly 41 KiB sidecar currently retains the complete roughly 684-entry prebuilt func/symbol index although only two final sites survive. Live-record pruning and compact Darwin re-signing are follow-up size optimizations; external mode's initial contract is optional deployment, not a guarantee that executable plus sidecar is smaller than embedded.Darwin arm64 — default /
-w/-s(bytes)macOS 26.2, Go 1.26.5, LLVM/Clang/LLD 19.1.7.
Darwin arm64 —
-w=false/-s -w=false(bytes)Linux arm64 — default /
-w/-s(bytes)Ubuntu kernel 6.8, Go 1.26.5, LLVM/Clang/LLD 19.1.7.
Linux arm64 —
-w=false/-s -w=false(bytes)Local validation
Darwin arm64:
-w=false: 194/194 PASS in 27.491s.-woverride.internal/build219.234s, with command packages andinternal/llgenalso passing.internal/cabisuite after restricting the temporary default to linked modes: PASS, 102.472s, 96.4% coverage.go test -p 1 ./... -count=1: PASS on the preceding implementation commit; the final policy-only delta is covered by the focused and integration runs above.Native Linux arm64:
internal/goflags,internal/pclnmap, andinternal/pclnpost: PASS.internal/cabisuite: PASS, 40.085s, 82.7% coverage.nobody.go test -race ./internal/pclnmap ./internal/pclnpost: PASS.Local Linux/amd64 under QEMU was not a reliable test environment: the Go compiler/cgo/vet/assembler processes themselves hit nondeterministic SIGSEGVs. Native amd64 is therefore left to GitHub CI.
Changed-package coverage:
internal/build: 72.8%; every function inlink_options.gois 100% covered, and the new PCLN build/post-link functions range from 72.2% to 100%.internal/goflags: 99.2%cmd/internal/flags: 54.7%internal/llgen: 23.2%internal/pclnmap: 98.8%internal/pclnpost: 89.6%internal/cabi: 96.4% on Darwin and 82.7% on Linux; this caught and now covers theModeGen/linked-default boundary.The final review removed the now-redundant golden/LTO/caller test-specific
-woverrides and their helper test. It also removed a proposed general-sizetext/JSON schema expansion (the fixed workloads and measured PR data are sufficient here), deleted unreachable duplicate flag-lexer checks and redundant post-loadsection validation, and added malformed Mach-O chained-fixup bounds tests. Broad tests now exercise the real default, while LLDB remains the explicit-DWARF acceptance suite.go test -racepasses for the map/postprocessor packages,go vetpasses for all changed Go packages, andgit diff --checkis clean.GitHub CI coverage and timing
On final head
b6d8aa872, Codecov patch coverage is 90.50715%, above the 88.92% target, and thecodecov/patchcheck passes. The patch report contains 696 hits, 38 misses, and 35 partial lines; the bot combines misses and partials into its "73 lines" review message. The remaining gaps are concentrated in filesystem/signing failure returns, malformed cross-format file reads, and overflow guards; no I/O mock layer was added solely to inflate coverage.The default-DWARF-off Dev LTO job passes 103/103 demos in 18m04s. The earlier default-DWARF experiment passed only 43/103, so the temporary default restores the full-LTO acceptance rate while explicit
-w=falseremains covered by LLDB and focused tests.Execution time versus the #2113 baseline (runner queue time excluded):
All 47 non-skipped checks passed with no failures; the release publishing job was expectedly skipped for the PR. This includes all Ubuntu and macOS Go 1.24/1.26 shards, main, Targets, build-cache, docs, release-artifact, stdlib-coverage, Dev LTO, format, and Codecov checks. Runner queue delay is excluded from the table.