feat(asm): resolve external GOOBJ symbols from archive data
Test / vet (push) Successful in 1m2s
Test / test (push) Failing after 2m5s
Test / build (push) Skipped

This commit is contained in:
2026-08-08 16:18:56 +02:00
parent 2a27a3a52b
commit 2d1176e045
5 changed files with 547 additions and 67 deletions
+47 -4
View File
@@ -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
+348
View File
@@ -0,0 +1,348 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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]) != "!<arch>\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
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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"])
}
+66 -21
View File
@@ -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
+20 -42
View File
@@ -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 <pkg>` 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.