Skip to content

Proposal: cross-platform source-level debugging for native, embedded, WASI, and browser WebAssembly #2164

Description

@cpunion

Summary

LLGo should provide one source-level debugging architecture across:

  • native Darwin and Linux;
  • bare-metal and embedded targets;
  • WASI runtimes;
  • browser WebAssembly.

The architecture must not assume that one debugger is available everywhere. It should produce standards-based debug artifacts and a versioned LLGo runtime-description contract, then expose them through the debugger used by each platform:

Platform Debug artifact Execution control Go/LLGo presentation
Native Darwin/Linux Mach-O/ELF + DWARF LLDB initially LLDB Go language extension + LLGo runtime adapter
Embedded host-side ELF + DWARF; derived .bin/.hex/.uf2 for flashing GDB Remote through OpenOCD, J-Link, QEMU, or another target probe GDB adapter first; LLDB through gdb-remote where supported
WASI runtime Wasm + DWARF runtime guest-debug stub, initially Wasmtime Wasm-aware LLDB plus LLGo runtime adapter
Browser Wasm embedded or external DWARF; name section/source map as fallback browser DevTools/CDP WebAssembly Language Extension for LLGo

This proposal is an umbrella. The native LLDB language work remains the focused sub-proposal:

#2154

Motivation

The existing native DWARF work has established a useful foundation, but the current target-build policy prevents that foundation from reaching embedded and WebAssembly artifacts:

  • every -target build currently sets DebugInfo.AlwaysOmit = true;
  • target link flags unconditionally include -S;
  • an explicit -ldflags=-w=false is rejected for those targets;
  • target configuration already has GDB, OpenOCD, and emulator fields, but the GDB command is not propagated into the build result and LLGo has no debug-session command.

Issue #2154 alone cannot address these gaps. An LLDB language extension handles source-language and runtime presentation, but:

  • embedded debugging normally uses GDB Remote through a probe or emulator;
  • a browser cannot load an LLDB plugin;
  • WASI guest debugging depends on runtime and LLDB Wasm support;
  • runtime stack symbolization through pclntab is different from source-level variable and type inspection through DWARF.

The implementation therefore needs a debugger-independent artifact and runtime contract before adding platform-specific frontends.

Design principles

1. Separate the three debugging layers

  1. Compiler and artifact metadata
    • DWARF source locations, scopes, variables, and types;
    • Wasm name section and optional source maps;
    • pclntab for runtime stack symbolization.
  2. Execution control
    • native process control;
    • GDB Remote/OpenOCD/J-Link/QEMU;
    • Wasmtime guest debug stub;
    • browser DevTools/CDP.
  3. Go and LLGo presentation
    • Go expressions and source-language behavior;
    • string, slice, map, channel, interface, function, and goroutine views;
    • LLGo runtime-version adapters.

No frontend should own metadata production, target flashing, or runtime layout definitions.

2. Keep DWARF, native symbols, and pclntab independent

  • Go -w controls DWARF generation/retention.
  • Go -s controls native symbol-table removal and implies -w unless an explicit -w=false overrides it.
  • LLGo's pclntab mode independently controls runtime symbolization data.

The configurable pclntab implementation is complete for its current native scope:

#2120

An external pclntab is optionally loaded by the running program. External DWARF is instead located and loaded by a debugger. They may share executable identity and artifact-reporting infrastructure, but they must not share runtime loading behavior or flags.

3. Generate a full debug artifact before deriving deployment artifacts

For embedded targets, the canonical debug artifact is the final linked ELF containing DWARF and target addresses. Flashable .bin, .hex, .uf2, or vendor images are derived from that ELF.

Debug sections are host-side, non-loadable data and should not consume MCU flash or RAM. LLGo should not suppress DWARF generation merely because the deployment format does not carry debug sections.

This follows TinyGo's separation between the linked executable used by GDB and the converted flash binary:

https://github.com/tinygo-org/tinygo/blob/888d0c0f978dd5b4bd457644baa597501154fdff/builder/build.go#L39-L50

For Wasm:

  • development builds may embed DWARF in the module;
  • deployable debug builds may place DWARF in a sidecar referenced by the final Wasm module;
  • production builds may omit DWARF while retaining the name section, pclntab, a source map, or an offline symbol file as explicitly selected.

Post-link transformations such as wasm-opt must either preserve/update debug offsets or run before the final debug artifact is validated.

4. Use a typed debug-artifact policy

The build layer should receive a typed policy rather than parse command strings:

  • embedded: DWARF remains in the executable or Wasm module;
  • external: the executable/module identifies a separate debugger-owned DWARF artifact;
  • host: retain a full host-side debug executable while producing a separate deployment artifact;
  • none: effective -w; do not generate LLGo DWARF.

Initial defaults:

  • native executable: embedded;
  • embedded target: host;
  • WASI: embedded;
  • browser development: embedded;
  • browser deployment: explicit external or none.

The exact user-facing spelling can be introduced in a dedicated flag PR, but it must be parsed through the shared typed flag/config path and must not overload -w or the pclntab option.

5. Keep the producer identity and runtime contract versioned

The initial compatibility path remains:

  • emit DW_AT_producer = LLGo;
  • emit DW_LANG_C while the selected consumer lacks Go-language support;
  • emit a small versioned LLGo debugger marker;
  • validate Go-oriented types, scopes, and locations independently of debugger behavior.

The marker should identify a debugger ABI/runtime schema, not only a compiler version. Native binaries may retain the existing symbol marker; Wasm should use an equivalent retained custom-section record when a symbol is not reliably discoverable.

Switching to DW_LANG_Go must be a per-consumer capability decision. Native LLDB support alone must not make embedded GDB or browser DevTools lose otherwise usable variables.

6. Share runtime layout definitions, not debugger implementation code

LLGo should define one versioned description of relevant runtime values and operations:

  • strings and slices;
  • interfaces;
  • maps and channels;
  • function values and closures;
  • goroutine state and stack ownership;
  • target pointer size, endianness, and ABI version.

LLDB, GDB, and browser adapters may have different implementation languages and APIs, but must be tested against the same fixtures and schema versions. Unsupported marker/schema versions should fail presentation clearly without preventing ordinary low-level debugging.

Platform plans

Native Darwin and Linux

Use the existing standards-tested DWARF and stock-LLDB compatibility path. Implement #2154 as two layers:

  1. a generic LLDB Go language layer;
  2. LLGo-specific runtime adapters selected by the versioned marker.

Continue using DW_LANG_C until the LLDB Go layer passes the full matrix. Linux GDB support may reuse the common runtime schema, but is not required to block the first native LLDB milestone.

Embedded and bare metal

  1. Replace the global target AlwaysOmit policy with explicit linker/format capabilities.
  2. Retain the final ELF with DWARF and derive the flash image from it.
  3. Activate the existing target GDB/OpenOCD/emulator configuration.
  4. Validate a manual GDB Remote session before adding orchestration.
  5. Add a command such as llgo debug -target=<target> that:
    • builds the debug ELF;
    • starts or connects to the configured OpenOCD/J-Link/QEMU server;
    • starts GDB or LLDB with the correct executable and remote command;
    • leaves flashing and server logs diagnosable.

The first acceptance targets should include one emulator and one real probe-supported Cortex-M or RISC-V target. ESP/Xtensa can follow after the generic flow is stable.

WASI runtimes

  1. Allow supported wasm-ld targets to retain DWARF.
  2. Verify final DWARF after all link/post-link transformations.
  3. Validate Wasmtime guest debugging using its debug stub and a Wasm-aware LLDB.
  4. Record the minimum supported Wasmtime and LLDB versions; LLGo's current LLVM/LLDB 19 must not be assumed to support the required Wasm debugging protocol.
  5. Reuse the LLGo runtime schema and marker when the debugger can read guest memory and values.

Wasmtime's current guest-debug flow is documented here:

https://docs.wasmtime.dev/examples-debugging-guest.html

Browser WebAssembly

The browser path should use DevTools rather than trying to embed LLDB:

  1. validate embedded DWARF in Chrome DevTools;
  2. validate a separate DWARF sidecar and source-path remapping;
  3. add an LLGo WebAssembly Language Extension for Go expressions and runtime values;
  4. retain source-map/name-section fallback for consumers without DWARF language-extension support.

Chrome's current Wasm debugger supports source stepping, breakpoints, variables, call stacks, complex values, linear memory, and external DWARF:

https://developer.chrome.com/docs/devtools/wasm

The language-extension API can map raw/source locations, enumerate variables, evaluate expressions, and read Wasm locals, globals, and linear memory:

https://github.com/ChromeDevTools/devtools-frontend/blob/69bfeaa8b2ce9a27d00dd85bad3b65d0aed8aa9c/docs/language_extension_api.md

Source maps are a fallback for locations and symbolication. They are not a replacement for DWARF variables, scopes, or type information.

Existing work and current dependency order

Status checked on 2026-07-23.

The platform work below should be split into reviewable PRs. Tasks at the same indentation level may proceed independently once their parent is usable.

  • Task 1: target debug capability and artifact model
    • Replace unconditional target AlwaysOmit/-S with linker- and format-specific capabilities.
    • Preserve a host debug artifact and derive embedded deployment formats from it.
    • Support embedded/external Wasm DWARF without changing pclntab selection.
    • Report all generated debug/deployment artifacts and sizes.
    • Test that an embedded host ELF with DWARF produces identical flashed loadable bytes to a stripped control.
  • Task 2: common LLGo debugger ABI
    • Specify and test the marker/schema version, target properties, and runtime layout adapters.
    • Provide shared fixtures for primitives, aliases, recursive types, aggregates, strings, slices, maps, channels, interfaces, functions, closures, and goroutines.
    • Reject non-LLGo and unsupported-schema targets without breaking raw debugging.
  • Task 3: native LLDB frontend
  • Task 4: embedded debugging
    • Propagate target GDB configuration and validate manual QEMU/OpenOCD GDB Remote sessions.
    • Add GDB formatters/runtime adapters backed by the common schema.
    • Add LLDB gdb-remote validation where the target architecture is supported.
    • Add llgo debug orchestration only after the manual artifact/transport contract is stable.
  • Task 5: WASI debugging
    • Retain and verify Wasm DWARF for supported WASI targets.
    • Validate Wasmtime guest debugging and pin a supported tool matrix.
    • Exercise the common LLGo marker/runtime adapter in guest memory.
  • Task 6: browser debugging
    • Validate Chrome DevTools with embedded DWARF.
    • Add and validate external DWARF packaging, identity, URL resolution, and source remapping.
    • Implement an LLGo WebAssembly Language Extension.
    • Test missing sidecars and non-Chrome fallback behavior.
  • Task 7: cross-platform acceptance and maintenance
    • Add debugger smoke tests that can run without physical hardware.
    • Add opt-in hardware/probe tests for representative targets.
    • Track debug artifact size separately from deployed/loadable size.
    • Document tool versions, launch recipes, path remapping, and optimized-out expectations.

Acceptance matrix

Every supported frontend must cover the applicable subset of:

  • source-file and line breakpoints;
  • stepping and call stacks;
  • parameters, named results, locals, globals, shadowing, and lexical scopes;
  • primitive, named, recursive, aggregate, and runtime-backed Go types;
  • optimized builds with explicit expectations for unavailable/optimized-out values;
  • panic, trap, division-by-zero, and invalid-memory source locations;
  • Go/C, Go/host, or Go/JavaScript boundary frames;
  • PIE/ASLR or Wasm code-offset relocation;
  • source-path remapping;
  • missing or stale sidecars;
  • non-LLGo marker rejection;
  • final-artifact DWARF verification after LTO and post-link processing.

The CI baseline should include native Darwin/Linux, one emulated embedded target, WASI/Wasmtime, and a headless Chromium fixture. Physical hardware is an additional opt-in lane rather than a requirement for every PR.

Related proposals and implementation

Non-goals

  • Making the runtime parse DWARF for runtime.Caller or panic stacks.
  • Treating source maps or pclntab as substitutes for DWARF variable/type information.
  • Requiring one debugger or one transport on every platform.
  • Globally switching to DW_LANG_Go before each consumer supports it.
  • Using Delve as the initial frontend; LLGo's LLVM ABI and runtime are not the gc toolchain contract Delve expects.
  • Solving WebAssembly scheduling, embedded GC, or target flashing beyond what a debug session needs.
  • Closing this umbrella from a partial platform PR. Subtasks should reference it; it should close only when the agreed platform scope and acceptance matrix are complete.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions