diff --git a/asm/aarch64_goobj_test.go b/asm/aarch64_goobj_test.go index 738b431..4526988 100644 --- a/asm/aarch64_goobj_test.go +++ b/asm/aarch64_goobj_test.go @@ -178,26 +178,10 @@ func main() { if err != nil { t.Fatalf("baseline build: %v\n%s", err, buildLog) } - var work, linkLine, asmObj 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_arm64.s") && !strings.Contains(line, "-gensymabis"): - asmObj = fieldAfter(line, "-o") - case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"): - linkLine = line - } - } - if work == "" || asmObj == "" { - t.Skipf("could not parse build log (work=%q asmObj=%q)", work, asmObj) - } - defer os.RemoveAll(work) + st := parseBuildLog(t, buildLog, "main_arm64.s") + defer os.RemoveAll(st.work) - // Expand $WORK in the object path. - asmObj = strings.ReplaceAll(asmObj, "$WORK", work) - - // Read the toolchain-produced object and assemble the same source with gasm. + // Assemble the same source with gasm and substitute the object. src, err := os.ReadFile(filepath.Join(dir, "main_arm64.s")) if err != nil { t.Fatal(err) @@ -210,30 +194,16 @@ func main() { if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } - gasmObj, err := img.GOObjectAARCH64("a64link", "main_arm64.s") + // The package path is "main", the prefix the Go code's references carry. + gasmObj, err := img.GOObjectAARCH64("main", "main_arm64.s") if err != nil { t.Fatalf("GOObjectAARCH64: %v", err) } - - // Replace the toolchain-produced object with gasm's. - if err := os.WriteFile(asmObj, gasmObj, 0o644); err != nil { - t.Fatalf("write gasm object: %v", err) - } - - // Re-link. - if linkLine == "" { - t.Skip("could not find link command in build log") - } - // Expand $WORK in the link command. - linkLine = strings.ReplaceAll(linkLine, "$WORK", work) - linkCmd := exec.Command("bash", "-c", "cd "+dir+" && "+linkLine) - linkCmd.Env = append(os.Environ(), "GOARCH=arm64") - if out, err := linkCmd.CombinedOutput(); err != nil { - t.Fatalf("re-link with gasm object: %v\n%s", err, out) - } + substituteAndRelink(t, goBin, dir, st, filepath.Join(dir, "prog2"), + gasmObj, "GOARCH=arm64") // Verify the binary exists and contains the symbol. - binPath := filepath.Join(dir, "prog") + binPath := filepath.Join(dir, "prog2") if _, err := os.Stat(binPath); err != nil { t.Fatalf("binary not found: %v", err) } @@ -296,23 +266,8 @@ func main() { if err != nil { t.Fatalf("baseline build: %v\n%s", err, buildLog) } - var work, linkLine, asmObj 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_arm64.s") && !strings.Contains(line, "-gensymabis"): - asmObj = fieldAfter(line, "-o") - case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"): - linkLine = line - } - } - if work == "" || asmObj == "" || linkLine == "" { - t.Skipf("could not parse build log (work=%q asmObj=%q link=%q)", work, asmObj, linkLine) - } - defer os.RemoveAll(work) - asmObj = strings.ReplaceAll(asmObj, "$WORK", work) - linkLine = strings.ReplaceAll(linkLine, "$WORK", work) + st := parseBuildLog(t, buildLog, "main_arm64.s") + defer os.RemoveAll(st.work) src, err := os.ReadFile(filepath.Join(dir, "main_arm64.s")) if err != nil { @@ -326,19 +281,13 @@ func main() { if err != nil { t.Fatalf("AssembleFileARM64: %v", err) } - gasmObj, err := img.GOObjectAARCH64("a64dlink", "main_arm64.s") + gasmObj, err := img.GOObjectAARCH64("main", "main_arm64.s") if err != nil { t.Fatalf("GOObjectAARCH64: %v", err) } - if err := os.WriteFile(asmObj, gasmObj, 0o644); err != nil { - t.Fatalf("write gasm object: %v", err) - } - linkCmd := exec.Command("bash", "-c", "cd "+dir+" && "+linkLine) - linkCmd.Env = append(os.Environ(), "GOARCH=arm64") - if out, err := linkCmd.CombinedOutput(); err != nil { - t.Fatalf("re-link with gasm object: %v\n%s", err, out) - } - binData, err := os.ReadFile(filepath.Join(dir, "prog")) + substituteAndRelink(t, goBin, dir, st, filepath.Join(dir, "prog2"), + gasmObj, "GOARCH=arm64") + binData, err := os.ReadFile(filepath.Join(dir, "prog2")) if err != nil { t.Fatal(err) } diff --git a/asm/arm64_assemble.go b/asm/arm64_assemble.go index 3e4ff81..e9478de 100644 --- a/asm/arm64_assemble.go +++ b/asm/arm64_assemble.go @@ -4003,6 +4003,7 @@ func AssembleFileARM64(f *ast.File) (*Image, error) { Size: d.size, Static: d.static, Rodata: d.rodata, + Noptr: d.noptr, Dupok: d.dupok, }) } diff --git a/asm/goobj.go b/asm/goobj.go index 00e5ab9..23fc39b 100644 --- a/asm/goobj.go +++ b/asm/goobj.go @@ -63,6 +63,7 @@ const ( const ( kindSTEXT = 1 kindSRODATA = 3 + kindSNOPTRDATA = 5 kindSDATA = 7 kindSDWARFFCN = 14 kindSDWARFLINES = 20 @@ -305,9 +306,16 @@ func (img *Image) emitGOObject(pkgPath, srcPath string, pre []byte, minLC int, r if !d.Static { name = pkgPath + "." + name } + // RODATA implies no pointers, so it wins over NOPTR: the kind is + // SRODATA either way, exactly as the toolchain chooses it. Plain + // NOPTR data is SNOPTRDATA, which the linker keeps out of the GC's + // type scan; a plain SDATA symbol would demand Go type information + // no assembly file can supply, and the link would fail. typ := uint8(kindSDATA) if d.Rodata { typ = kindSRODATA + } else if d.Noptr { + typ = kindSNOPTRDATA } flag := uint8(0) if d.Dupok { diff --git a/asm/goobj_test.go b/asm/goobj_test.go index f0b5d7d..8639e2e 100644 --- a/asm/goobj_test.go +++ b/asm/goobj_test.go @@ -514,6 +514,137 @@ func fieldAfter(line, flag string) string { return "" } +// buildLogSteps is what a substitution test needs from a `go build -x -work` +// log: the work directory, the assembler's object, the package archive and +// the link command line. +type buildLogSteps struct { + work string + asmObj string // $WORK expanded + pkgArch string // $WORK expanded + linkLine string // still carries $WORK placeholders +} + +// parseBuildLog extracts the build steps from a `go build -x -work` log. +// asmFile names the assembly file whose object the test substitutes. A +// missing step is a failure, not a skip: the toolchain changed shape and the +// substitution would silently test nothing. +func parseBuildLog(t *testing.T, log []byte, asmFile string) buildLogSteps { + t.Helper() + var st buildLogSteps + for line := range strings.SplitSeq(string(log), "\n") { + switch { + case strings.HasPrefix(line, "WORK="): + st.work = strings.TrimPrefix(line, "WORK=") + case strings.Contains(line, "/asm ") && strings.Contains(line, asmFile) && !strings.Contains(line, "-gensymabis"): + st.asmObj = fieldAfter(line, "-o") + case strings.Contains(line, "pack r") && strings.Contains(line, "_pkg_.a"): + rest := strings.TrimSpace(strings.SplitN(line, "pack r", 2)[1]) + st.pkgArch = strings.Fields(strings.SplitN(rest, "#", 2)[0])[0] + case strings.Contains(line, "/link ") && strings.Contains(line, "-importcfg"): + st.linkLine = line + } + } + if st.work == "" || st.asmObj == "" || st.pkgArch == "" || st.linkLine == "" { + t.Fatalf("could not locate the build steps (work=%q asmObj=%q pkgArch=%q link=%q):\n%s", + st.work, st.asmObj, st.pkgArch, st.linkLine, log) + } + st.asmObj = strings.ReplaceAll(st.asmObj, "$WORK", st.work) + st.pkgArch = strings.ReplaceAll(st.pkgArch, "$WORK", st.work) + return st +} + +// substituteAndRelink swaps the gasm object into the package archive the +// baseline build produced and re-runs the captured link line against the +// rebuilt archive, writing the binary to outBin (the -x log's link step +// always targets the action graph's internal a.out, which the helper +// redirects; the copy to the -o target is a separate build action the helper +// does not need). The archive handed to the linker is proven to carry the +// gasm object byte for byte, so a build-layout change that skipped the +// substitution fails here instead of passing vacuously. +func substituteAndRelink(t *testing.T, goBin, dir string, st buildLogSteps, outBin string, gasmObj []byte, extraEnv ...string) { + t.Helper() + + // Extract the archive, overwrite the assembler's member with the gasm + // object and repack (go tool pack has no replace-in-place). + membersDir := filepath.Join(dir, "members") + if err := os.MkdirAll(membersDir, 0o755); err != nil { + t.Fatal(err) + } + extract := exec.Command(goBin, "tool", "pack", "x", st.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(st.asmObj)) + if _, err := os.Stat(member); err != nil { + t.Fatalf("the assembler's archive member was not extracted: %v", err) + } + if err := os.Chmod(member, 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(member, gasmObj, 0o644); err != nil { + t.Fatal(err) + } + listCmd := exec.Command(goBin, "tool", "pack", "t", st.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, 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) + } + + // Prove the substitution: the archive the linker is about to consume + // holds the gasm object, byte for byte. + checkDir := filepath.Join(dir, "check") + if err := os.MkdirAll(checkDir, 0o755); err != nil { + t.Fatal(err) + } + check := exec.Command(goBin, "tool", "pack", "x", newArch) + check.Dir = checkDir + if out, err := check.CombinedOutput(); err != nil { + t.Fatalf("pack x (verification): %v\n%s", err, out) + } + got, err := os.ReadFile(filepath.Join(checkDir, filepath.Base(st.asmObj))) + if err != nil { + t.Fatalf("read the substituted member back: %v", err) + } + if !bytes.Equal(got, gasmObj) { + t.Fatal("the repacked archive does not carry the gasm object") + } + + // Re-link. The line carries a GOROOT assignment and $WORK placeholders; + // GOEXPERIMENT must match the toolchain's own, because the linker + // compares the object header against its configuration. + goExp, _ := exec.Command(goBin, "env", "GOEXPERIMENT").Output() + linkLine := strings.ReplaceAll(st.linkLine, "$WORK", st.work) + linkLine = strings.ReplaceAll(linkLine, filepath.Join(st.work, "b001", "_pkg_.a"), newArch) + linkLine = strings.ReplaceAll(linkLine, filepath.Join(st.work, "b001", "exe", "a.out"), outBin) + env := append(os.Environ(), "GOEXPERIMENT="+strings.TrimSpace(string(goExp))) + env = append(env, extraEnv...) + link := exec.Command("sh", "-c", linkLine) + link.Dir = dir + link.Env = env + if out, err := link.CombinedOutput(); err != nil { + t.Fatalf("link with the gasm object: %v\n%s", err, out) + } +} + // 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 diff --git a/asm/link.go b/asm/link.go index 0d87ad4..0133505 100644 --- a/asm/link.go +++ b/asm/link.go @@ -134,7 +134,8 @@ type DataSymbol struct { Offset int // byte offset within Data Size int Static bool // the <> marker: file-local, not exported - Rodata bool // the RODATA flag: read-only data + Rodata bool // the RODATA flag: read-only data (implies no pointers) + Noptr bool // the NOPTR flag: data with no pointers, kept out of GC scanning Dupok bool // the DUPOK flag: duplicate-OK // Relocs carries the symbol-valued DATA initialisers ("DATA s+0(SB)/8, // $other(SB)"): fields of this symbol's data that hold another symbol's @@ -269,6 +270,7 @@ func AssembleFile(f *ast.File, opts ...AssembleOption) (*Image, error) { Size: len(d.buf), Static: d.static, Rodata: d.rodata, + Noptr: d.noptr, Dupok: d.dupok, }) img.Data = append(img.Data, d.buf...) @@ -413,6 +415,7 @@ func AssembleFileRISCV(f *ast.File) (*Image, error) { Size: d.size, Static: d.static, Rodata: d.rodata, + Noptr: d.noptr, Dupok: d.dupok, }) } @@ -485,6 +488,7 @@ func AssembleFileLOONG64(f *ast.File) (*Image, error) { Size: d.size, Static: d.static, Rodata: d.rodata, + Noptr: d.noptr, Dupok: d.dupok, }) } @@ -546,6 +550,7 @@ type dataSym struct { size int static bool rodata bool + noptr bool dupok bool // relocs are the symbol-valued DATA fields, in declaration order; Off // is relative to the symbol's data start. @@ -587,12 +592,14 @@ func collectData(f *ast.File) ([]dataSym, error) { switch f { case "RODATA": ds.rodata = true + case "NOPTR": + ds.noptr = true case "DUPOK": ds.dupok = true default: // Legacy numeric flag constants (runtime/textflag.h): - // DUPOK is 2, RODATA is 8; combinations arrive as one - // number (e.g. 10 = RODATA|DUPOK). + // DUPOK is 2, RODATA is 8, NOPTR is 16; combinations arrive + // as one number (e.g. 10 = RODATA|DUPOK). if n, err := strconv.Atoi(f); err == nil { if n&2 != 0 { ds.dupok = true @@ -600,6 +607,9 @@ func collectData(f *ast.File) ([]dataSym, error) { if n&8 != 0 { ds.rodata = true } + if n&16 != 0 { + ds.noptr = true + } } } } diff --git a/asm/link_test.go b/asm/link_test.go index 66f30cf..df48fb0 100644 --- a/asm/link_test.go +++ b/asm/link_test.go @@ -361,23 +361,8 @@ func main() { if err != nil { t.Fatalf("baseline build: %v\n%s", err, buildLog) } - var work, linkLine, asmObj 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, "/link ") && strings.Contains(line, "-importcfg"): - linkLine = line - } - } - if work == "" || asmObj == "" || linkLine == "" { - t.Skipf("could not parse build log (work=%q asmObj=%q link=%q)", work, asmObj, linkLine) - } - defer os.RemoveAll(work) - asmObj = strings.ReplaceAll(asmObj, "$WORK", work) - linkLine = strings.ReplaceAll(linkLine, "$WORK", work) + st := parseBuildLog(t, buildLog, "main_amd64.s") + defer os.RemoveAll(st.work) // Assemble the same source with gasm and substitute the object. src, err := os.ReadFile(filepath.Join(dir, "main_amd64.s")) @@ -392,21 +377,18 @@ func main() { if err != nil { t.Fatalf("AssembleFile: %v", err) } - gasmObj, err := img.GOObject("dlink", "main_amd64.s") + // The package path is "main": the linker resolves the Go code's + // references against main., so the object must define the symbols + // under that prefix whatever the module is called. + gasmObj, err := img.GOObject("main", "main_amd64.s") if err != nil { t.Fatalf("GOObject: %v", err) } - if err := os.WriteFile(asmObj, gasmObj, 0o644); err != nil { - t.Fatalf("write gasm object: %v", err) - } - linkCmd := exec.Command("bash", "-c", "cd "+dir+" && "+linkLine) - if out, err := linkCmd.CombinedOutput(); err != nil { - t.Fatalf("re-link with gasm object: %v\n%s", err, out) - } + substituteAndRelink(t, goBin, dir, st, filepath.Join(dir, "prog2"), gasmObj) // The linked program must run and find the right function behind the // data word. - out, err := exec.Command(filepath.Join(dir, "prog")).CombinedOutput() + out, err := exec.Command(filepath.Join(dir, "prog2")).CombinedOutput() if err != nil { t.Fatalf("linked program failed: %v\n%s", err, out) }