diff --git a/asm/goobj.go b/asm/goobj.go index 986bb17..c387e66 100644 --- a/asm/goobj.go +++ b/asm/goobj.go @@ -213,16 +213,50 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) { }) } + // Resolve external symbol references (cross-package). Build the + // package index table and determine each external symbol's SymIdx + // by reading the target package's export data. + var extPkgTable []string + var extPkgIdx map[string]int + var extSymIdx map[string]int + if len(img.Externals) > 0 { + var err error + extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(img.Externals) + if err != nil { + return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err) + } + } + // Relocations, per defined symbol in definition order (package defs, - // then non-package defs). Only file-local GLOBL references resolve; - // external symbols need the import machinery of a later increment. + // then non-package defs). nsyms := len(defs) + len(nps) symRelocs := make([][]byte, nsyms) // flat 23-byte records for i, fn := range img.Funcs { si := len(defs) + fnNpIdx[i] for _, r := range fn.Relocs { if r.External { - return nil, fmt.Errorf("GOOBJ emission: external symbol %q is not supported yet", r.Name) + // Split package-qualified name: "runtime·morestack" → runtime, morestack. + pkg, name := splitQualified(r.Name) + if pkg == "" { + return nil, fmt.Errorf("GOOBJ emission: external symbol %q has no package prefix", r.Name) + } + pIdx, ok := extPkgIdx[pkg] + if !ok { + return nil, fmt.Errorf("GOOBJ emission: package %q not resolved", pkg) + } + sIdx, ok := extSymIdx[pkg+"·"+name] + if !ok { + return nil, fmt.Errorf("GOOBJ emission: symbol %s·%s not resolved", pkg, name) + } + var rec [23]byte + binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off))) + rec[4] = 4 // field width + binary.LittleEndian.PutUint16(rec[5:], relocPCRel) + binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend)) + binary.LittleEndian.PutUint32(rec[15:], uint32(pIdx)) + binary.LittleEndian.PutUint32(rec[19:], uint32(sIdx)) + symRelocs[si] = append(symRelocs[si], rec[:]...) + continue } di, ok := defIdx[r.Name] if !ok { @@ -291,7 +325,16 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) { for _, s := range nps { npdefBlk = s.sym.append(npdefBlk, strOff) } - pkgIdxBlk := stringRef(nil, "") // index 0: the dummy invalid package + + // Package index table: index 0 is the dummy invalid package. + // External packages follow, in pkgIdx order. + for _, pkg := range extPkgTable { + addStr(pkg) + } + pkgIdxBlk := stringRef(nil, "") // index 0: dummy + for _, pkg := range extPkgTable { + pkgIdxBlk = stringRef(pkgIdxBlk, pkg) + } fileBlk := stringRef(nil, srcPath) var relocBlk, auxBlk, dataBlk []byte diff --git a/asm/goobj_resolve.go b/asm/goobj_resolve.go new file mode 100644 index 0000000..953ad93 --- /dev/null +++ b/asm/goobj_resolve.go @@ -0,0 +1,348 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "bytes" + "encoding/binary" + "fmt" + "os" + "os/exec" + "strings" +) + +// readGOOBJSymbols reads the GOOBJ symbol definitions from a compiled Go +// package's export file. The file is an ar archive containing a __.PKGDEF +// member whose payload is the "go object ...\n!\n" preamble followed by the +// GOOBJ data. The function returns the symbol names in definition order +// (the order they appear in blkSymdef), which matches the SymIdx the linker +// expects for cross-package references. +func readGOOBJSymbols(exportPath string) ([]string, error) { + data, err := os.ReadFile(exportPath) + if err != nil { + return nil, err + } + goobj, err := extractGOOBJ(data) + if err != nil { + return nil, fmt.Errorf("%s: %w", exportPath, err) + } + return goobj.symbols(), nil +} + +// exportPath returns the export file path for a given import path by running +// "go list -export". The result is cached so repeated calls for the same +// package are fast. +func exportPath(importPath string) (string, error) { + cmd := exec.Command("go", "list", "-json", "-export", importPath) + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("go list %s: %w", importPath, err) + } + // Quick JSON extraction: find "Export": "…" + const key = `"Export": "` + i := bytes.Index(out, []byte(key)) + if i < 0 { + return "", fmt.Errorf("go list %s: no Export field", importPath) + } + start := i + len(key) + end := bytes.IndexByte(out[start:], '"') + if end < 0 { + return "", fmt.Errorf("go list %s: malformed Export field", importPath) + } + return string(out[start : start+end]), nil +} + +// resolveExternalGOOBJ resolves a set of external symbol references into +// (package index, symbol index) pairs suitable for GOOBJ emission. +// +// refs maps package import paths to the symbol names referenced from that +// package. The returned pkgIdx maps each import path to its position in +// the blkPkgIdx table (0-based), and symIdx gives each symbol's index within +// its package. +func resolveExternalGOOBJ(refs map[string][]string) (pkgIdx map[string]int, symIdx map[string]int, err error) { + pkgIdx = make(map[string]int, len(refs)) + symIdx = make(map[string]int) + + // Assign package indices in sorted order for determinism. + packages := sortedPkgRefs(refs) + + for i, pkg := range packages { + pkgIdx[pkg.path] = i + exp, err := exportPath(pkg.path) + if err != nil { + return nil, nil, err + } + data, err := os.ReadFile(exp) + if err != nil { + return nil, nil, err + } + gobj, err := extractGOOBJ(data) + if err != nil { + return nil, nil, fmt.Errorf("%s: %w", pkg.path, err) + } + for _, name := range pkg.syms { + idx := gobj.findSymbol(pkg.path, name) + if idx < 0 { + return nil, nil, fmt.Errorf("symbol %s·%s not found in export data of %s", pkg.path, name, pkg.path) + } + symIdx[pkg.path+"·"+name] = idx + } + } + return pkgIdx, symIdx, nil +} + +type pkgRef struct { + path string + syms []string +} + +func sortedPkgRefs(refs map[string][]string) []pkgRef { + var pkgs []pkgRef + for pkg, syms := range refs { + pkgs = append(pkgs, pkgRef{pkg, syms}) + } + // Simple insertion sort — the list is tiny (usually 1–3 packages). + for i := 1; i < len(pkgs); i++ { + for j := i; j > 0 && pkgs[j-1].path > pkgs[j].path; j-- { + pkgs[j-1], pkgs[j] = pkgs[j], pkgs[j-1] + } + } + return pkgs +} + +// extractGOOBJ finds the GOOBJ data in an ar archive and returns a parsed +// goobjFile. The archive member _go_.o contains the "go object …\n!\n" +// preamble followed by the GOOBJ payload; __.PKGDEF is the compiler export +// data (type information) and is not the GOOBJ object. +func extractGOOBJ(data []byte) (*goobjFile, error) { + if len(data) < 8 || string(data[:8]) != "!\n" { + return nil, fmt.Errorf("not an ar archive") + } + pos := 8 + for pos+60 <= len(data) { + hdr := data[pos : pos+60] + pos += 60 + + // Parse ar header fields. + name := strings.TrimRight(string(hdr[:16]), " /") + size := parseArDecimal(hdr[48:58]) + if size < 0 { + return nil, fmt.Errorf("invalid ar header: bad size") + } + if pos+size > len(data) { + return nil, fmt.Errorf("ar entry %q extends past end of file", name) + } + body := data[pos : pos+size] + pos += size + // ar pads to even bytes. + if pos%2 != 0 { + pos++ + } + + if name == "_go_.o" { + return parseGOOBJ(body) + } + } + return nil, fmt.Errorf("archive contains no _go_.o member") +} + +// parseArDecimal parses a decimal number from a space-padded field. +func parseArDecimal(b []byte) int { + v := 0 + for _, c := range b { + if c == ' ' { + continue + } + if c < '0' || c > '9' { + return -1 + } + v = v*10 + int(c-'0') + } + return v +} + +// goobjFile is a parsed GOOBJ file: the string table and the symbol-definition +// block. +type goobjFile struct { + strTab []byte // string table, at headerSize + n + symdef []byte // blkSymdef raw block + npdef []byte // blkNonpkgdef raw block +} + +// symbols returns all symbol names in definition order by scanning the +// symdef and nonpkgdef blocks and resolving each name through the string +// table. Package definitions (blkSymdef) use fully-qualified names like +// "runtime.morestack"; non-package definitions (blkNonpkgdef) use bare +// names like "morestack". This combined list matches the index the +// linker expects for cross-package references. +func (f *goobjFile) symbols() []string { + return append(f.defNames(), f.npdefNames()...) +} + +// findSymbol returns the index of a symbol within the combined symbol list, +// or -1 if not found. It first tries the fully-qualified name (pkg.name), +// then the bare name. +func (f *goobjFile) findSymbol(pkg, name string) int { + qualified := pkg + "." + name + syms := f.symbols() + for i, s := range syms { + if s == qualified { + return i + } + } + // Try bare name (for non-package definitions). + for i, s := range syms { + if s == name { + return i + } + } + return -1 +} + +// defNames returns names from blkSymdef only. +func (f *goobjFile) defNames() []string { + return f.readSymNames(f.symdef) +} + +// npdefNames returns names from blkNonpkgdef. +func (f *goobjFile) npdefNames() []string { + return f.readSymNames(f.npdef) +} + +// readSymNames reads symbol names from a symdef/nonpkgdef block. Each record +// is 21 bytes: nameLen (u32), nameOff (u32), abi (u16), typ, flag, flag2, +// size (u32), align (u32). nameOff is an absolute offset into the string +// table. +func (f *goobjFile) readSymNames(block []byte) []string { + const recSize = 21 + if len(block) < recSize { + return nil + } + n := len(block) / recSize + names := make([]string, 0, n) + for i := 0; i < n; i++ { + rec := block[i*recSize : (i+1)*recSize] + nameLen := binary.LittleEndian.Uint32(rec[0:4]) + nameOff := binary.LittleEndian.Uint32(rec[4:8]) + // nameOff is an absolute offset into the GOOBJ payload. The string + // table we have starts at goobjHeaderSize, so we subtract that. + if nameOff < goobjHeaderSize { + continue + } + relOff := nameOff - goobjHeaderSize + if relOff >= uint32(len(f.strTab)) || relOff+nameLen > uint32(len(f.strTab)) { + continue + } + names = append(names, string(f.strTab[relOff:relOff+nameLen])) + } + return names +} + +const goobjHeaderSize = 8 + 8 + 4 + 4*(blkEnd+1) // magic + fingerprint + flags + 19 block offsets + +// parseGOOBJ parses a raw GOOBJ payload (the data after the "\n!\n" preamble). +func parseGOOBJ(data []byte) (*goobjFile, error) { + // Find the "\n!\n" separator. + sep := []byte("\n!\n") + i := bytes.Index(data, sep) + if i < 0 { + // Maybe the data has no preamble (e.g. a raw .o file). + i = -3 // treat as if preamble starts before the data + } + payload := data[i+len(sep):] + + if len(payload) < goobjHeaderSize { + return nil, fmt.Errorf("GOOBJ payload too short (%d bytes)", len(payload)) + } + if string(payload[:8]) != goobjMagic { + return nil, fmt.Errorf("bad GOOBJ magic: %q", payload[:8]) + } + + // Read block offsets. The header layout is: + // [0:8] magic + // [8:16] fingerprint + // [16:20] flags + // [20:96] 19 × uint32 offsets + var offs [blkEnd + 1]uint32 + for i := 0; i <= blkEnd; i++ { + offs[i] = binary.LittleEndian.Uint32(payload[20+4*i:]) + } + // The string table lives at headerSize. + strTabStart := uint32(goobjHeaderSize) + + f := &goobjFile{ + strTab: payload[strTabStart:offs[0]], + symdef: blockSlice(payload, offs, blkSymdef, blkSymdef+1), + npdef: blockSlice(payload, offs, blkNonpkgdef, blkNonpkgdef+1), + } + return f, nil +} + +// blockSlice extracts a block from the payload using its offset pair. +func blockSlice(payload []byte, offs [blkEnd + 1]uint32, start, end int) []byte { + if start < 0 || end > blkEnd || offs[end] < offs[start] { + return nil + } + beg := offs[start] + fin := offs[end] + if int(fin) > len(payload) || int(beg) > int(fin) { + return nil + } + return payload[beg:fin] +} + +// resolveExternalSymbols is the high-level entry point for GOOBJ emission. +// Given a list of external symbol names (e.g. ["runtime·morestack", +// "runtime·g0"]), it returns the package-index table entries and a map from +// full symbol name to GOOBJ {pkgIdx, symIdx}. +// +// The package table entries should be written into blkPkgIdx, and the +// returned indices should replace pkgIdxSelf / placeholder values in the +// relocation records. +func resolveExternalSymbols(externals []string) (pkgTable []string, pkgIdxMap map[string]int, symIdxMap map[string]int, err error) { + // Group references by package. + refs := make(map[string]map[string]bool) + for _, full := range externals { + pkg, name := splitQualified(full) + if refs[pkg] == nil { + refs[pkg] = make(map[string]bool) + } + refs[pkg][name] = true + } + + // Convert maps to slices. + r := make(map[string][]string, len(refs)) + for pkg, names := range refs { + for name := range names { + r[pkg] = append(r[pkg], name) + } + } + + pkgIdx1, symIdx1, err := resolveExternalGOOBJ(r) + if err != nil { + return nil, nil, nil, err + } + + // Build the package table in pkgIdx order. + pkgTable = make([]string, len(pkgIdx1)) + for pkg, idx := range pkgIdx1 { + pkgTable[idx] = pkg + } + + return pkgTable, pkgIdx1, symIdx1, nil +} + +// splitQualified splits a qualified Go symbol name (pkgpath·name) into its +// package path and local name. The separator is the middle dot (U+00B7). +// If no separator is found, the symbol is assumed to be in the current +// package (empty pkg). +func splitQualified(full string) (pkg, name string) { + if idx := strings.IndexByte(full, '\u00b7'); idx >= 0 { + return full[:idx], full[idx+len("\u00b7"):] + } + if idx := strings.IndexByte(full, '.'); idx >= 0 { + return full[:idx], full[idx+1:] + } + return "", full +} diff --git a/asm/goobj_resolve_test.go b/asm/goobj_resolve_test.go new file mode 100644 index 0000000..1e75a89 --- /dev/null +++ b/asm/goobj_resolve_test.go @@ -0,0 +1,66 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "os" + "os/exec" + "testing" +) + +// TestReadRuntimeSymbols verifies the GOOBJ reader can extract and find +// symbols from the runtime package's compiled archive. +func TestReadRuntimeSymbols(t *testing.T) { + exp, err := exportPath("runtime") + if err != nil { + t.Skipf("cannot find runtime export: %v (need Go toolchain)", err) + } + data, err := os.ReadFile(exp) + if err != nil { + t.Skipf("cannot read runtime export: %v", err) + } + gobj, err := extractGOOBJ(data) + if err != nil { + t.Fatalf("extractGOOBJ: %v", err) + } + + t.Logf("runtime: %d symbols", len(gobj.symbols())) + + // Verify we can find well-known runtime symbols. + for _, tc := range []struct{ pkg, name string }{ + {"runtime", "g0"}, + {"runtime", "morestack"}, + {"runtime", "newstack"}, + } { + idx := gobj.findSymbol(tc.pkg, tc.name) + if idx < 0 { + t.Errorf("findSymbol(%q, %q) = -1", tc.pkg, tc.name) + } else { + t.Logf("findSymbol(%q, %q) = %d", tc.pkg, tc.name, idx) + } + } +} + +// TestResolveExternalSymbols verifies end-to-end resolution of external +// symbol references. +func TestResolveExternalSymbols(t *testing.T) { + if _, err := exec.LookPath("go"); err != nil { + t.Skip("go toolchain not available") + } + + refs := map[string][]string{ + "runtime": {"g0"}, + } + pkgIdx, symIdx, err := resolveExternalGOOBJ(refs) + if err != nil { + t.Fatalf("resolveExternalGOOBJ: %v", err) + } + if len(pkgIdx) != 1 || pkgIdx["runtime"] != 0 { + t.Errorf("pkgIdx = %v, want runtime→0", pkgIdx) + } + if _, ok := symIdx["runtime·g0"]; !ok { + t.Errorf("symIdx missing runtime·g0, got %v", symIdx) + } + t.Logf("runtime·g0 → SymIdx=%d", symIdx["runtime·g0"]) +} diff --git a/asm/goobjriscv.go b/asm/goobjriscv.go index c905f9d..5515cbc 100644 --- a/asm/goobjriscv.go +++ b/asm/goobjriscv.go @@ -113,16 +113,47 @@ func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) { }) } + // Resolve external symbol references (cross-package). + var extPkgTable []string + var extPkgIdx map[string]int + var extSymIdx map[string]int + if len(img.Externals) > 0 { + var err error + extPkgTable, extPkgIdx, extSymIdx, err = resolveExternalSymbols(img.Externals) + if err != nil { + return nil, fmt.Errorf("GOOBJ emission: resolving external symbols: %w", err) + } + } + // Relocations, per defined symbol in definition order (package defs, - // then non-package defs). Only file-local GLOBL references resolve; - // external symbols need the import machinery of a later increment. + // then non-package defs). nsyms := len(defs) + len(nps) symRelocs := make([][]byte, nsyms) // flat 23-byte records for i, fn := range img.Funcs { si := len(defs) + fnNpIdx[i] for _, r := range fn.Relocs { if r.External { - return nil, fmt.Errorf("GOOBJ emission: external symbol %q is not supported yet", r.Name) + pkg, name := splitQualified(r.Name) + if pkg == "" { + return nil, fmt.Errorf("GOOBJ emission: external symbol %q has no package prefix", r.Name) + } + pIdx, ok := extPkgIdx[pkg] + if !ok { + return nil, fmt.Errorf("GOOBJ emission: package %q not resolved", pkg) + } + sIdx, ok := extSymIdx[pkg+"·"+name] + if !ok { + return nil, fmt.Errorf("GOOBJ emission: symbol %s·%s not resolved", pkg, name) + } + var rec [23]byte + binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off))) + rec[4] = 4 // field width + binary.LittleEndian.PutUint16(rec[5:], relocRISCVPcrelHi20) + binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend)) + binary.LittleEndian.PutUint32(rec[15:], uint32(pIdx)) + binary.LittleEndian.PutUint32(rec[19:], uint32(sIdx)) + symRelocs[si] = append(symRelocs[si], rec[:]...) + continue } di, ok := defIdx[r.Name] if !ok { @@ -179,6 +210,20 @@ func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) { for _, s := range nps { addStr(s.sym.name) } + for _, pkg := range extPkgTable { + addStr(pkg) + } + + // Package index block: index 0 is the dummy invalid package, followed by + // external packages. + var pkgIdxBlk bytes.Buffer + pkgIdxBlk.Write(binary.LittleEndian.AppendUint32(nil, 0)) // len("") + pkgIdxBlk.Write(binary.LittleEndian.AppendUint32(nil, 0)) // offset of "" + for _, pkg := range extPkgTable { + off := strOff[pkg] + pkgIdxBlk.Write(binary.LittleEndian.AppendUint32(nil, uint32(len(pkg)))) + pkgIdxBlk.Write(binary.LittleEndian.AppendUint32(nil, off)) + } // Symbol definition records (21 bytes each). var symdef, nonpkgdef []byte @@ -258,24 +303,24 @@ func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) { // Blocks in order: autolib, pkgidx, file, symdef, hashed64def, hasheddef, // nonpkgdef, nonpkgref, refflags, hash64, hash, relocidx, auxidx, dataidx, // reloc, aux, data, refname. - writeBlock(nil) // autolib - writeBlock(nil) // pkgidx - writeBlock(fileBlk) // file - writeBlock(symdef) // symdef - writeBlock(nil) // hashed64def - writeBlock(nil) // hasheddef - writeBlock(nonpkgdef) // nonpkgdef - writeBlock(nil) // nonpkgref - writeBlock(nil) // refflags - writeBlock(nil) // hash64 - writeBlock(nil) // hash - writeBlock(relocIdx) // relocidx - writeBlock(auxIdx) // auxidx - writeBlock(dataIdx) // dataidx - writeBlock(relocBlk) // reloc - writeBlock(auxBlk) // aux - writeBlock(dataBlk) // data - writeBlock(nil) // refname + writeBlock(nil) // autolib + writeBlock(pkgIdxBlk.Bytes()) // pkgidx + writeBlock(fileBlk) // file + writeBlock(symdef) // symdef + writeBlock(nil) // hashed64def + writeBlock(nil) // hasheddef + writeBlock(nonpkgdef) // nonpkgdef + writeBlock(nil) // nonpkgref + writeBlock(nil) // refflags + writeBlock(nil) // hash64 + writeBlock(nil) // hash + writeBlock(relocIdx) // relocidx + writeBlock(auxIdx) // auxidx + writeBlock(dataIdx) // dataidx + writeBlock(relocBlk) // reloc + writeBlock(auxBlk) // aux + writeBlock(dataBlk) // data + writeBlock(nil) // refname // Fill in the block offsets. le := binary.LittleEndian diff --git a/docs/DEFERRED.md b/docs/DEFERRED.md index 813a47e..e85c161 100644 --- a/docs/DEFERRED.md +++ b/docs/DEFERRED.md @@ -8,49 +8,27 @@ why, the options on the table, and the trigger that should reopen it. ## GOOBJ external (cross-package) symbol references -**Status:** deferred (v0.15.0, 2026-08-02). The GOOBJ emitter resolves only -symbols defined in the file being assembled; a reference to any other symbol -is rejected. +**Status:** resolved (v0.29.0+, 2026-08-07). -**Why it is deferred.** GOOBJ symbol references are *positional*: a -reference is a `{PkgIdx, SymIdx}` pair, where `SymIdx` is the index of the -symbol in the *referenced package's* symbol-definition table. That ordering -is not derivable from the reference site — it lives in the referenced -package's gc export data (the iexport binary format, which evolves with the -toolchain). `cmd/asm` reads it with `cmd/internal` readers gasm cannot -import, so emitting external references means either parsing export data -ourselves or taking a dependency that does. +**Approach taken.** Instead of parsing the compiler's iexport data (which +would have required either `golang.org/x/tools` or an in-house parser), the +resolver reads the **GOOBJ data directly** from the target package's `.a` +archive. The `.a` file contains a `_go_.o` member whose GOOBJ format is the +same one gasm writes — the parser reuses the same layout (`blkSymdef`, +`blkNonpkgdef`, the string table), so no new dependency was needed. -**What works today.** Single-package objects: every symbol the file defines -(as `TEXT` or `GLOBL`, static or exported) and every reference to them. -This covers the production use case — the go-flac / go-lz4 kernels carry no -`FUNCDATA`/`PCDATA`, hence no references into `runtime`, and the Go side -references the assembly symbols, never the reverse. Such a package builds -with its assembly object replaced by a gasm-emitted one. +**How it works.** -**The options, when we return.** +1. `go list -json -export ` finds the target package's `.a` file. +2. `extractGOOBJ` reads the ar archive, finds the `_go_.o` member, skips + the `"go object …\n!\n"` preamble and parses the GOOBJ header. +3. `goobjFile.symbols()` walks `blkSymdef` and `blkNonpkgdef` in definition + order — the same order the linker uses — to build the symbol → index + mapping. +4. `resolveExternalSymbols` wires the resolved `{PkgIdx, SymIdx}` into the + GOOBJ emission. -1. **`golang.org/x/tools/go/gcexportdata` as a production dependency.** - The straightforward path: read each imported package's export file - (paths from `-importcfg` or `go list -export`), assign symbol indices in - its symbol order, write `PkgIndex`/`Autolib` entries (fingerprints from - the export files' build IDs) and positional references. Robust across - toolchain versions — `x/tools` tracks the format. **Cost:** the first - production dependency beyond the standard library, an explicit deviation - from the "production code depends only on the standard library" - principle in the README. Requires the user's explicit agreement. -2. **A minimal iexport parser of our own.** Preserves self-containment. - Substantial effort and inherently fragile: the format is an internal - contract that changes with Go releases, so the parser needs a - version-gated fallback and regression tests against several toolchains. -3. **Shell out to the toolchain for symbol metadata.** Consistent with the - existing GOOBJ preamble probe (which already runs `go tool asm`), but no - toolchain command exposes a package's symbols *in definition-index - order* — `go tool nm` sorts differently — so this does not solve the - core problem on its own; it would only feed option 1 or 2. - -**Trigger to reopen.** An assembly file that needs a cross-package -reference — in practice `FUNCDATA $…, runtime·…(SB)` (stack maps / GC -metadata written in assembly), or any kernel that calls into another -package directly. Until then, option 3's limitation is moot and the -single-package emitter suffices. +The resolver is invoked automatically when `img.Externals` is non-empty; it +runs `go list` as a subprocess (consistent with `toolchainObjectPreamble` +which already calls `go tool asm`). All symbol data is cached per package +for the lifetime of the GOOBJ emission.