// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "bytes" "encoding/binary" "os" "os/exec" "path/filepath" "strings" "testing" "sourcedock.dev/petrbalvin/gasm-sdk/parser" ) // goobjView is a minimal parsed view of a GOOBJ payload, enough to check // the emitter's output block by block. type goobjView struct { t *testing.T b []byte offs [blkEnd + 1]uint32 strOff uint32 } func openGoobj(t *testing.T, data []byte) *goobjView { t.Helper() i := bytes.Index(data, []byte(goobjMagic)) if i < 0 { t.Fatal("no GOOBJ magic in output") } v := &goobjView{t: t, b: data[i:], strOff: uint32(i + 96)} for j := 0; j <= blkEnd; j++ { v.offs[j] = binary.LittleEndian.Uint32(v.b[20+4*j:]) } return v } func (v *goobjView) blk(i int) []byte { return v.b[v.offs[i]:v.offs[i+1]] } func (v *goobjView) str(off, ln uint32) string { return string(v.b[off : off+ln]) } type goobjSymView struct { name string abi uint16 typ uint8 flag uint8 flag2 uint8 size uint32 align uint32 } func (v *goobjView) syms(i int) []goobjSymView { var out []goobjSymView for x := v.blk(i); len(x) >= 21; x = x[21:] { le := binary.LittleEndian out = append(out, goobjSymView{ name: v.str(le.Uint32(x[4:]), le.Uint32(x[0:])), abi: le.Uint16(x[8:]), typ: x[10], flag: x[11], flag2: x[12], size: le.Uint32(x[13:]), align: le.Uint32(x[17:]), }) } return out } // TestGOObjectStructure checks the emitted object's blocks against the // ground truth captured from go tool asm: the symbol tables, the FuncInfo // contents, the pc-value tables, the relocation and the aux wiring. func TestGOObjectStructure(t *testing.T) { f, errs := parser.Parse("t_amd64.s", ` #include "textflag.h" TEXT ·addq(SB), NOSPLIT, $0-24 MOVQ a+0(FP), AX MOVQ b+8(FP), CX ADDQ CX, AX MOVQ AX, ret+16(FP) RET TEXT ·loadmask(SB), NOSPLIT, $0-8 VMOVDQU mask<>(SB), X0 VPMOVMSKB X0, AX MOVQ AX, ret+0(FP) RET GLOBL mask<>(SB), RODATA, $16 DATA mask<>+0(SB)/8, $0x0807060504030201 DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09 `) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFile(f) if err != nil { t.Fatalf("AssembleFile: %v", err) } obj, err := img.GOObject("testpkg", "t_amd64.s") if err != nil { t.Fatalf("GOObject: %v", err) } v := openGoobj(t, obj) if flags := binary.LittleEndian.Uint32(v.b[16:]); flags != 4 { t.Errorf("flags = %#x, want ObjFlagFromAssembly (4)", flags) } // Package defs: the static GLOBL, then per function the FuncInfo and the // two DWARF symbols (debug_line program, subprogram DIE). defs := v.syms(blkSymdef) if len(defs) != 7 { t.Fatalf("symdefs = %d, want 7", len(defs)) } // The linkname flag stays clear: the toolchain sets it only for // //go:linkname symbols, and an ordinary static GLOBL is not one. if defs[0].name != "mask" || defs[0].abi != 0xffff || defs[0].typ != kindSRODATA || defs[0].size != 16 || defs[0].flag2 != 0 { t.Errorf("mask symbol = %+v", defs[0]) } if defs[1].name != "" || defs[1].typ != kindSDATA || defs[1].size != 28 { t.Errorf("addq funcinfo symbol = %+v", defs[1]) } if defs[2].name != "" || defs[2].typ != kindSDWARFLINES || defs[2].size == 0 { t.Errorf("addq lines symbol = %+v", defs[2]) } if defs[3].name != "" || defs[3].typ != kindSDWARFFCN || defs[3].size == 0 { t.Errorf("addq DIE symbol = %+v", defs[3]) } if defs[4].name != "" || defs[4].typ != kindSDATA || defs[4].size != 28 { t.Errorf("loadmask funcinfo symbol = %+v", defs[4]) } // Non-package defs: four pc tables and the function, per function. nps := v.syms(blkNonpkgdef) if len(nps) != 10 { t.Fatalf("nonpkgdefs = %d, want 10", len(nps)) } fn := nps[4] if fn.name != "testpkg.addq" || fn.typ != kindSTEXT || fn.flag != symFlagNoSplit || fn.size != 19 { t.Errorf("addq symbol = %+v", fn) } for i, s := range []int{0, 1, 2, 3, 5, 6, 7, 8} { if nps[s].typ != kindSRODATA || nps[s].align != 1 || nps[s].name != "" { t.Errorf("pc table %d = %+v", i, nps[s]) } } // FuncInfo: args 24, FuncFlag Asm, one file, no inline tree. le := binary.LittleEndian data := v.blk(blkData) didx := v.blk(blkDataIdx) fi := data[16:44] if le.Uint32(fi[0:]) != 24 || le.Uint32(fi[4:]) != 0 || fi[8] != 0 || fi[9] != funcFlagAsm || le.Uint32(fi[16:]) != 1 || le.Uint32(fi[20:]) != 0 || le.Uint32(fi[24:]) != 0 { t.Errorf("funcinfo bytes %x", fi) } // The pc-value tables of addq (non-package indices 0-3, so global // indices 7-10): pcsp a flat zero over the whole function, pcinline a // flat -1, both with the pc delta in MinLC (1) units. pcsp := data[le.Uint32(didx[4*7:]):] if got := pcsp[:3]; !bytes.Equal(got, []byte{0x02, 19, 0x00}) { t.Errorf("pcsp = %x, want 021300", got) } pcinl := data[le.Uint32(didx[4*10:]):] if got := pcinl[:3]; !bytes.Equal(got, []byte{0x00, 19, 0x00}) { t.Errorf("pcinline = %x, want 001300", got) } // Relocations: the four DWARF address references (two per function, in // definition order), then the loadmask code's R_PCREL against the // GLOBL, with the field in the function code left zero. The loadmask // code's offset comes from the data index (7 defs + 9 non-package). relocs := v.blk(blkReloc) if len(relocs) != 5*23 { t.Fatalf("relocs = %d bytes, want 5 entries", len(relocs)) } // addq's DWARF references (defs 2 and 3) against the function, which // is non-package index 4. lr := relocs[:23] if int32(le.Uint32(lr[0:])) != 3 || lr[4] != 8 || le.Uint16(lr[5:]) != relocAddr || le.Uint32(lr[15:]) != pkgIdxNone || le.Uint32(lr[19:]) != 4 { t.Errorf("addq lines reloc = %x", lr) } dr := relocs[23:46] if dr[4] != 4 || le.Uint16(dr[5:]) != relocDWTXTADDRU4() || le.Uint32(dr[15:]) != pkgIdxNone || le.Uint32(dr[19:]) != 4 { t.Errorf("addq DIE reloc = %x", dr) } cr := relocs[4*23:] off := int32(le.Uint32(cr[0:])) if off != 4 || cr[4] != 4 || le.Uint16(cr[5:]) != relocPCRel || le.Uint64(cr[7:]) != 0 || le.Uint32(cr[15:]) != pkgIdxSelf || le.Uint32(cr[19:]) != 0 { t.Errorf("loadmask reloc = %x", cr) } lm := le.Uint32(didx[4*16:]) code := data[lm : lm+18] if !bytes.Equal(code[4:8], []byte{0, 0, 0, 0}) { t.Errorf("relocated field = %x, want zeroed", code[4:8]) } // Aux wiring: FuncInfo, the two DWARF symbols (package symbols), then // the four pc tables (non-package symbols). auxs := v.blk(blkAux) if len(auxs) != 2*7*9 { t.Fatalf("aux = %d bytes, want 14 entries", len(auxs)) } wantAux := []struct { typ uint8 pkg uint32 idx uint32 }{ {auxFuncInfo, pkgIdxSelf, 1}, {auxDwarfInfo, pkgIdxSelf, 3}, {auxDwarfLines, pkgIdxSelf, 2}, {auxPcsp, pkgIdxNone, 0}, {auxPcfile, pkgIdxNone, 1}, {auxPcline, pkgIdxNone, 2}, {auxPcinline, pkgIdxNone, 3}, {auxFuncInfo, pkgIdxSelf, 4}, {auxDwarfInfo, pkgIdxSelf, 6}, {auxDwarfLines, pkgIdxSelf, 5}, {auxPcsp, pkgIdxNone, 5}, {auxPcfile, pkgIdxNone, 6}, {auxPcline, pkgIdxNone, 7}, {auxPcinline, pkgIdxNone, 8}, } for i, w := range wantAux { e := auxs[i*9:] if e[0] != w.typ || le.Uint32(e[1:]) != w.pkg || le.Uint32(e[5:]) != w.idx { t.Errorf("aux[%d] = {%d,%d,%d}, want {%d,%d,%d}", i, e[0], le.Uint32(e[1:]), le.Uint32(e[5:]), w.typ, w.pkg, w.idx) } } } // decodePCValues decodes a pc-value table into (pc, value) steps. The // table ends with a final unsigned pc delta covering the rest of the // function, followed by a zero byte that carries no value delta. func decodePCValues(b []byte) (pcs, vals []int64) { val, n := binary.Varint(b) b = b[n:] val-- // the first delta is against the implicit -1 var pc int64 pcs = append(pcs, pc) vals = append(vals, val) for { pcd, n := binary.Uvarint(b) b = b[n:] if pcd == 0 { // zero pc delta terminates the table break } pc += int64(pcd) if len(b) == 1 && b[0] == 0 { // final coverage, no value change break } vd, n := binary.Varint(b) b = b[n:] val += vd pcs = append(pcs, pc) vals = append(vals, val) } return pcs, vals } // TestGOObjectPcspFrame checks the pcsp table of a frame-pointer function: // the prologue raises the stack delta to 8+frame, the RET's epilogue // restores it to zero. func TestGOObjectPcspFrame(t *testing.T) { f, errs := parser.Parse("frame_amd64.s", ` #include "textflag.h" TEXT ·framed(SB), NOSPLIT, $8-0 MOVQ BP, AX RET `) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFile(f) if err != nil { t.Fatalf("AssembleFile: %v", err) } fn := img.Funcs[0] pcs, vals := decodePCValues(pcspTable(fn, 1)) // Prologue: PUSHQ BP (1 byte, +8), MOVQ SP, BP (3 bytes, no change), // SUBQ $8, SP (4 bytes, +16 in total); the RET's epilogue unwinds // ADDQ $8, SP (+8) then POPQ BP (0). wantPCs := []int64{0, 1, 8} wantVals := []int64{0, 8, 16} if len(pcs) < len(wantPCs) { t.Fatalf("pcsp pcs = %v vals = %v", pcs, vals) } for i := range wantPCs { if pcs[i] != wantPCs[i] || vals[i] != wantVals[i] { t.Errorf("pcsp[%d] = (%d,%d), want (%d,%d); all: %v %v", i, pcs[i], vals[i], wantPCs[i], wantVals[i], pcs, vals) } } // The last two steps unwind the epilogue to zero. n := len(pcs) if vals[n-1] != 0 || vals[n-2] != 8 { t.Errorf("epilogue steps = %v %v, want …8, 0", pcs, vals) } // The table covers the whole function. if last := pcs[n-1]; last >= int64(fn.Size) { t.Errorf("last pc %d beyond function size %d", last, fn.Size) } } // TestGOObjectExternalRejected checks that a reference to a symbol no GLOBL // defines is reported: GOOBJ emission resolves only file-local symbols so // far. func TestGOObjectExternalRejected(t *testing.T) { f, errs := parser.Parse("ext_amd64.s", ` #include "textflag.h" TEXT ·useext(SB), NOSPLIT, $0-8 MOVQ elsewhere(SB), AX MOVQ AX, ret+0(FP) RET `) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFile(f) if err != nil { t.Fatalf("AssembleFile: %v", err) } if _, err := img.GOObject("p", "ext_amd64.s"); err == nil || !strings.Contains(err.Error(), "external") { t.Errorf("error = %v, want an external-symbol error", err) } } // TestGOObjectLinkAndRun is the end-to-end check: assemble the test // functions to a GOOBJ, swap it into a go build in place of the toolchain's // assembly object, link, and run; the output must match the baseline // binary the Go assembler produced. Skipped when no Go toolchain is // available. func TestGOObjectLinkAndRun(t *testing.T) { goBin, err := exec.LookPath("go") if err != nil { t.Skip("no Go toolchain available") } dir := t.TempDir() const asmSrc = ` #include "textflag.h" TEXT ·addq(SB), NOSPLIT, $0-24 MOVQ a+0(FP), AX MOVQ b+8(FP), CX ADDQ CX, AX MOVQ AX, ret+16(FP) RET TEXT ·loadmask(SB), NOSPLIT, $0-8 VMOVDQU mask<>(SB), X0 VPMOVMSKB X0, AX MOVQ AX, ret+0(FP) RET GLOBL mask<>(SB), RODATA, $16 DATA mask<>+0(SB)/8, $0x0807060504030201 DATA mask<>+8(SB)/8, $0x800f0e0d0c0b0a09 ` const mainSrc = `package main func addq(a, b int64) int64 func loadmask() int64 func main() { println(addq(41, 1)) println(loadmask()) } ` if err := os.WriteFile(filepath.Join(dir, "main_amd64.s"), []byte(asmSrc), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module goobjtest\n\ngo 1.27\n"), 0o644); err != nil { t.Fatal(err) } // Baseline build with the toolchain's assembler; keep the work // directory and the commands the build used. cmd := exec.Command(goBin, "build", "-x", "-work", "-o", "app", ".") cmd.Dir = dir buildLog, err := cmd.CombinedOutput() if err != nil { t.Fatalf("baseline build: %v\n%s", err, buildLog) } var work string var asmObj, pkgArch, linkLine string for line := range strings.SplitSeq(string(buildLog), "\n") { switch { case strings.HasPrefix(line, "WORK="): work = strings.TrimPrefix(line, "WORK=") case strings.Contains(line, "/asm ") && strings.Contains(line, "-o ") && strings.Contains(line, "main_amd64.s") && !strings.Contains(line, "-gensymabis"): asmObj = fieldAfter(line, "-o") case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"): pkgArch = strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1]) pkgArch = strings.Fields(strings.SplitN(pkgArch, "#", 2)[0])[0] case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"): linkLine = line } } if work == "" || asmObj == "" || pkgArch == "" || linkLine == "" { t.Fatalf("could not locate the build steps:\n%s", buildLog) } asmObj = strings.ReplaceAll(asmObj, "$WORK", work) pkgArch = strings.ReplaceAll(pkgArch, "$WORK", work) // The baseline's answer. baseOut, err := exec.Command(filepath.Join(dir, "app")).CombinedOutput() if err != nil { t.Fatalf("run baseline: %v\n%s", err, baseOut) } // Assemble the same source with gasm and swap the object in. pf, perrs := parser.Parse(filepath.Join(dir, "main_amd64.s"), asmSrc) if len(perrs) > 0 { t.Fatalf("parse: %v", perrs) } img, err := AssembleFile(pf) if err != nil { t.Fatalf("AssembleFile: %v", err) } obj, err := img.GOObject("main", filepath.Join(dir, "main_amd64.s")) if err != nil { t.Fatalf("GOObject: %v", err) } // Rebuild the package archive with our object in place of the // toolchain's (go tool pack has no replace-in-place that dedupes, so // extract, substitute and repack). The archive member holding the // assembler's output is named after the asm object file, e.g. // main_amd64.o. extract := exec.Command(goBin, "tool", "pack", "x", pkgArch) membersDir := filepath.Join(dir, "members") if err := os.MkdirAll(membersDir, 0o755); err != nil { t.Fatal(err) } extract.Dir = membersDir if out, err := extract.CombinedOutput(); err != nil { t.Fatalf("pack x: %v\n%s", err, out) } member := filepath.Join(membersDir, filepath.Base(asmObj)) if err := os.Chmod(member, 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(member, obj, 0o644); err != nil { t.Fatal(err) } listCmd := exec.Command(goBin, "tool", "pack", "t", pkgArch) listOut, err := listCmd.CombinedOutput() if err != nil { t.Fatalf("pack t: %v\n%s", err, listOut) } newArch := filepath.Join(dir, "pkg.a") args := []string{"tool", "pack", "c", newArch} seen := map[string]bool{} for m := range strings.FieldsSeq(string(listOut)) { if seen[m] { continue } seen[m] = true if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil { t.Fatal(err) } args = append(args, filepath.Join(membersDir, m)) } pack := exec.Command(goBin, args...) pack.Dir = membersDir if out, err := pack.CombinedOutput(); err != nil { t.Fatalf("pack c: %v\n%s", err, out) } // Link with our archive. The link line carries a GOROOT assignment // and $WORK placeholders; run it through the shell with the // GOEXPERIMENT the toolchain expects (the linker compares the object // header against its own, experiments included). goExp, _ := exec.Command(goBin, "env", "GOEXPERIMENT").Output() linkLine = strings.ReplaceAll(linkLine, "$WORK", work) linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "_pkg_.a"), newArch) linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "exe", "a.out"), filepath.Join(dir, "app2")) link := exec.Command("sh", "-c", linkLine) link.Dir = dir link.Env = append(os.Environ(), "GOEXPERIMENT="+strings.TrimSpace(string(goExp))) if out, err := link.CombinedOutput(); err != nil { t.Fatalf("link with gasm object: %v\n%s", err, out) } got, err := exec.Command(filepath.Join(dir, "app2")).CombinedOutput() if err != nil { t.Fatalf("run gasm-linked binary: %v\n%s", err, got) } if !bytes.Equal(got, baseOut) { t.Errorf("gasm-linked output %q, want baseline %q", got, baseOut) } } // fieldAfter returns the whitespace-delimited field following the first // occurrence of flag in line. func fieldAfter(line, flag string) string { fields := strings.Fields(line) for i, f := range fields { if f == flag && i+1 < len(fields) { return fields[i+1] } } return "" } // TestGOObjectExternalPackageLink is the cross-package end-to-end check: a // GOOBJ whose code references a real external package symbol (runtime's // morestack, a plain reference rather than the builtin noctxt form) must // carry a package index that points past the blkPkgIdx table's dummy entry // 0, and the object must link against the real runtime. Pre-fix, the // relocations carried block index 0, which the loader never fills, so the // reference resolved against whatever object was loaded first and the link // failed. The binary is not run: morestack returns to the call site's // stack check, which a hand-written caller has none of. func TestGOObjectExternalPackageLink(t *testing.T) { goBin, err := exec.LookPath("go") if err != nil { t.Skip("no Go toolchain available") } dir := t.TempDir() const asmSrc = ` #include "textflag.h" TEXT ·fn(SB), NOSPLIT, $0-0 CALL ·helper(SB) RET TEXT ·helper(SB), NOSPLIT, $0-0 RET ` const mainSrc = `package main func fn() func helper() func main() { fn() helper() } ` if err := os.WriteFile(filepath.Join(dir, "main_amd64.s"), []byte(asmSrc), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainSrc), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module extlink\n\ngo 1.27\n"), 0o644); err != nil { t.Fatal(err) } // Capture the build the toolchain performs and re-run only its link // step with our object swapped into the package archive, mirroring // TestGOObjectLinkAndRun. build := exec.Command(goBin, "build", "-x", "-work", "-o", filepath.Join(dir, "prog"), ".") build.Dir = dir buildLog, err := build.CombinedOutput() if err != nil { t.Fatalf("baseline build: %v\n%s", err, buildLog) } var work, linkLine, asmObj, pkgArch string for line := range strings.SplitSeq(string(buildLog), "\n") { switch { case strings.HasPrefix(line, "WORK="): work = strings.TrimPrefix(line, "WORK=") case strings.Contains(line, "/asm ") && strings.Contains(line, "main_amd64.s") && !strings.Contains(line, "-gensymabis"): asmObj = fieldAfter(line, "-o") case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"): pkgArch = strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1]) pkgArch = strings.Fields(strings.SplitN(pkgArch, "#", 2)[0])[0] case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"): linkLine = line } } if work == "" || asmObj == "" || pkgArch == "" || linkLine == "" { t.Skipf("could not parse build log (work=%q asmObj=%q)", work, asmObj) } defer os.RemoveAll(work) asmObj = strings.ReplaceAll(asmObj, "$WORK", work) pkgArch = strings.ReplaceAll(pkgArch, "$WORK", work) // Assemble the source with gasm, then retarget fn's internal call at // a real external package symbol: the reloc's qualified name drives // the export-data resolution the way a source-level runtime·sym(SB) // reference would. f, errs := parser.Parse("main_amd64.s", asmSrc) if len(errs) > 0 { t.Fatalf("parse: %v", errs) } img, err := AssembleFile(f) if err != nil { t.Fatalf("AssembleFile: %v", err) } fn := &img.Funcs[0] for i := range fn.Relocs { fn.Relocs[i].Name = "runtime\u00b7morestack" fn.Relocs[i].External = true } img.Externals = []string{"runtime\u00b7morestack"} obj, err := img.GOObject("main", "main_amd64.s") if err != nil { t.Fatalf("GOObject: %v", err) } // Structural check: the blkPkgIdx block reserves entry 0 for the // dummy invalid package and places runtime at entry 1, and fn's call // relocation carries PkgIdx 1. v := openGoobj(t, obj) pkgBlk := v.blk(blkPkgIdx) if len(pkgBlk) != 2*8 { t.Fatalf("blkPkgIdx = %d bytes, want two entries", len(pkgBlk)) } le := binary.LittleEndian strEntry := func(i int) string { e := pkgBlk[i*8 : (i+1)*8] return v.str(le.Uint32(e[4:]), le.Uint32(e[0:])) } if s := strEntry(0); s != "" { t.Errorf("blkPkgIdx[0] = %q, want the dummy empty package", s) } if s := strEntry(1); s != "runtime" { t.Errorf("blkPkgIdx[1] = %q, want runtime", s) } relocs := v.blk(blkReloc) // fn is the last non-package symbol (two functions, four pc tables // each); its one reloc is the final record. fnRec := relocs[len(relocs)-23:] if pIdx := le.Uint32(fnRec[15:]); pIdx != 1 { t.Errorf("external reloc PkgIdx = %d, want 1 (runtime)", pIdx) } // Swap the object into the package archive and link with cmd/link; // the link line consumes the archive, not the loose object file. membersDir := filepath.Join(dir, "members") if err := os.MkdirAll(membersDir, 0o755); err != nil { t.Fatal(err) } extract := exec.Command(goBin, "tool", "pack", "x", pkgArch) extract.Dir = membersDir if out, err := extract.CombinedOutput(); err != nil { t.Fatalf("pack x: %v\n%s", err, out) } member := filepath.Join(membersDir, filepath.Base(asmObj)) if err := os.Chmod(member, 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(member, obj, 0o644); err != nil { t.Fatal(err) } listCmd := exec.Command(goBin, "tool", "pack", "t", pkgArch) listOut, err := listCmd.CombinedOutput() if err != nil { t.Fatalf("pack t: %v\n%s", err, listOut) } newArch := filepath.Join(dir, "pkg.a") args := []string{"tool", "pack", "c", newArch} seen := map[string]bool{} for m := range strings.FieldsSeq(string(listOut)) { if seen[m] { continue } seen[m] = true if err := os.Chmod(filepath.Join(membersDir, m), 0o644); err != nil { t.Fatal(err) } args = append(args, filepath.Join(membersDir, m)) } pack := exec.Command(goBin, args...) pack.Dir = membersDir if out, err := pack.CombinedOutput(); err != nil { t.Fatalf("pack c: %v\n%s", err, out) } linkLine = strings.ReplaceAll(linkLine, "$WORK", work) linkLine = strings.ReplaceAll(linkLine, pkgArch, newArch) linkLine = strings.ReplaceAll(linkLine, filepath.Join(work, "b001", "exe", "a.out"), filepath.Join(dir, "prog2")) linkCmd := exec.Command("sh", "-c", "cd "+dir+" && "+linkLine) if out, err := linkCmd.CombinedOutput(); err != nil { t.Fatalf("re-link with gasm object: %v\n%s", err, out) } // The call must have resolved to the real runtime symbol. dump, err := exec.Command(goBin, "tool", "objdump", "-s", "main.fn", filepath.Join(dir, "prog2")).CombinedOutput() if err != nil { t.Fatalf("objdump main.fn: %v\n%s", err, dump) } if !bytes.Contains(dump, []byte("runtime.morestack")) { t.Errorf("main.fn does not call runtime.morestack:\n%s", dump) } }