feat(asm): resolve external GOOBJ symbols from archive data
This commit is contained in:
+47
-4
@@ -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,
|
// Relocations, per defined symbol in definition order (package defs,
|
||||||
// then non-package defs). Only file-local GLOBL references resolve;
|
// then non-package defs).
|
||||||
// external symbols need the import machinery of a later increment.
|
|
||||||
nsyms := len(defs) + len(nps)
|
nsyms := len(defs) + len(nps)
|
||||||
symRelocs := make([][]byte, nsyms) // flat 23-byte records
|
symRelocs := make([][]byte, nsyms) // flat 23-byte records
|
||||||
for i, fn := range img.Funcs {
|
for i, fn := range img.Funcs {
|
||||||
si := len(defs) + fnNpIdx[i]
|
si := len(defs) + fnNpIdx[i]
|
||||||
for _, r := range fn.Relocs {
|
for _, r := range fn.Relocs {
|
||||||
if r.External {
|
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]
|
di, ok := defIdx[r.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -291,7 +325,16 @@ func (img *Image) GOObject(pkgPath, srcPath string) ([]byte, error) {
|
|||||||
for _, s := range nps {
|
for _, s := range nps {
|
||||||
npdefBlk = s.sym.append(npdefBlk, strOff)
|
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)
|
fileBlk := stringRef(nil, srcPath)
|
||||||
|
|
||||||
var relocBlk, auxBlk, dataBlk []byte
|
var relocBlk, auxBlk, dataBlk []byte
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -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,
|
// Relocations, per defined symbol in definition order (package defs,
|
||||||
// then non-package defs). Only file-local GLOBL references resolve;
|
// then non-package defs).
|
||||||
// external symbols need the import machinery of a later increment.
|
|
||||||
nsyms := len(defs) + len(nps)
|
nsyms := len(defs) + len(nps)
|
||||||
symRelocs := make([][]byte, nsyms) // flat 23-byte records
|
symRelocs := make([][]byte, nsyms) // flat 23-byte records
|
||||||
for i, fn := range img.Funcs {
|
for i, fn := range img.Funcs {
|
||||||
si := len(defs) + fnNpIdx[i]
|
si := len(defs) + fnNpIdx[i]
|
||||||
for _, r := range fn.Relocs {
|
for _, r := range fn.Relocs {
|
||||||
if r.External {
|
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]
|
di, ok := defIdx[r.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -179,6 +210,20 @@ func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) {
|
|||||||
for _, s := range nps {
|
for _, s := range nps {
|
||||||
addStr(s.sym.name)
|
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).
|
// Symbol definition records (21 bytes each).
|
||||||
var symdef, nonpkgdef []byte
|
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,
|
// Blocks in order: autolib, pkgidx, file, symdef, hashed64def, hasheddef,
|
||||||
// nonpkgdef, nonpkgref, refflags, hash64, hash, relocidx, auxidx, dataidx,
|
// nonpkgdef, nonpkgref, refflags, hash64, hash, relocidx, auxidx, dataidx,
|
||||||
// reloc, aux, data, refname.
|
// reloc, aux, data, refname.
|
||||||
writeBlock(nil) // autolib
|
writeBlock(nil) // autolib
|
||||||
writeBlock(nil) // pkgidx
|
writeBlock(pkgIdxBlk.Bytes()) // pkgidx
|
||||||
writeBlock(fileBlk) // file
|
writeBlock(fileBlk) // file
|
||||||
writeBlock(symdef) // symdef
|
writeBlock(symdef) // symdef
|
||||||
writeBlock(nil) // hashed64def
|
writeBlock(nil) // hashed64def
|
||||||
writeBlock(nil) // hasheddef
|
writeBlock(nil) // hasheddef
|
||||||
writeBlock(nonpkgdef) // nonpkgdef
|
writeBlock(nonpkgdef) // nonpkgdef
|
||||||
writeBlock(nil) // nonpkgref
|
writeBlock(nil) // nonpkgref
|
||||||
writeBlock(nil) // refflags
|
writeBlock(nil) // refflags
|
||||||
writeBlock(nil) // hash64
|
writeBlock(nil) // hash64
|
||||||
writeBlock(nil) // hash
|
writeBlock(nil) // hash
|
||||||
writeBlock(relocIdx) // relocidx
|
writeBlock(relocIdx) // relocidx
|
||||||
writeBlock(auxIdx) // auxidx
|
writeBlock(auxIdx) // auxidx
|
||||||
writeBlock(dataIdx) // dataidx
|
writeBlock(dataIdx) // dataidx
|
||||||
writeBlock(relocBlk) // reloc
|
writeBlock(relocBlk) // reloc
|
||||||
writeBlock(auxBlk) // aux
|
writeBlock(auxBlk) // aux
|
||||||
writeBlock(dataBlk) // data
|
writeBlock(dataBlk) // data
|
||||||
writeBlock(nil) // refname
|
writeBlock(nil) // refname
|
||||||
|
|
||||||
// Fill in the block offsets.
|
// Fill in the block offsets.
|
||||||
le := binary.LittleEndian
|
le := binary.LittleEndian
|
||||||
|
|||||||
+20
-42
@@ -8,49 +8,27 @@ why, the options on the table, and the trigger that should reopen it.
|
|||||||
|
|
||||||
## GOOBJ external (cross-package) symbol references
|
## GOOBJ external (cross-package) symbol references
|
||||||
|
|
||||||
**Status:** deferred (v0.15.0, 2026-08-02). The GOOBJ emitter resolves only
|
**Status:** resolved (v0.29.0+, 2026-08-07).
|
||||||
symbols defined in the file being assembled; a reference to any other symbol
|
|
||||||
is rejected.
|
|
||||||
|
|
||||||
**Why it is deferred.** GOOBJ symbol references are *positional*: a
|
**Approach taken.** Instead of parsing the compiler's iexport data (which
|
||||||
reference is a `{PkgIdx, SymIdx}` pair, where `SymIdx` is the index of the
|
would have required either `golang.org/x/tools` or an in-house parser), the
|
||||||
symbol in the *referenced package's* symbol-definition table. That ordering
|
resolver reads the **GOOBJ data directly** from the target package's `.a`
|
||||||
is not derivable from the reference site — it lives in the referenced
|
archive. The `.a` file contains a `_go_.o` member whose GOOBJ format is the
|
||||||
package's gc export data (the iexport binary format, which evolves with the
|
same one gasm writes — the parser reuses the same layout (`blkSymdef`,
|
||||||
toolchain). `cmd/asm` reads it with `cmd/internal` readers gasm cannot
|
`blkNonpkgdef`, the string table), so no new dependency was needed.
|
||||||
import, so emitting external references means either parsing export data
|
|
||||||
ourselves or taking a dependency that does.
|
|
||||||
|
|
||||||
**What works today.** Single-package objects: every symbol the file defines
|
**How it works.**
|
||||||
(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.
|
|
||||||
|
|
||||||
**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 resolver is invoked automatically when `img.Externals` is non-empty; it
|
||||||
The straightforward path: read each imported package's export file
|
runs `go list` as a subprocess (consistent with `toolchainObjectPreamble`
|
||||||
(paths from `-importcfg` or `go list -export`), assign symbol indices in
|
which already calls `go tool asm`). All symbol data is cached per package
|
||||||
its symbol order, write `PkgIndex`/`Autolib` entries (fingerprints from
|
for the lifetime of the GOOBJ emission.
|
||||||
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.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user