From ec1c521187f841dd5ce12d1f4ec4fe9a8e609fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Sun, 20 Sep 2026 22:02:19 +0200 Subject: [PATCH] feat(cmd): GOOS-aware headers, audit battery shapes and semicolon spacing Assisted-by: GLM 5.3 Flash --- cmd/gasm/asmhdr.go | 55 ++++++++++------- cmd/gasm/asmhdr_test.go | 133 ++++++++++++++++++++++++++++++++++++---- cmd/gasm/audit.go | 77 ++++++++++++++++++++--- cmd/gasm/main.go | 23 +++++-- docs/CLI.md | 14 ++++- docs/man/gasm-asm.1 | 15 ++++- format/format.go | 7 +++ format/format_test.go | 11 ++++ 8 files changed, 286 insertions(+), 49 deletions(-) diff --git a/cmd/gasm/asmhdr.go b/cmd/gasm/asmhdr.go index 275d02c..c76843f 100644 --- a/cmd/gasm/asmhdr.go +++ b/cmd/gasm/asmhdr.go @@ -27,7 +27,10 @@ import ( // GOROOT assembly includes it, and a standalone assembler has no compiler // to have produced it, so gasm generates the equivalent itself: the package // the .s file lives in is parsed and type-checked here, with the target -// architecture's own sizes, and the same defines are written out. +// architecture's own sizes, and the same defines are written out. The +// type-checking GOOS is selected by the caller: a GOOS-specific file +// (sys_darwin_arm64.s) needs its platform's defines, which a header from +// the ambient GOOS silently omits. // // The emitter mirrors cmd/compile's dumpasmhdr exactly: constants come out // as "const_NAME", struct entries as "NAME__size" followed by the fields in @@ -67,11 +70,15 @@ func goAsmHeaderResolved(asmDir string, dirs []string) bool { return false } -// generateGoAsmHeader type-checks the Go package in pkgDir for goarch, -// writes its go_asm.h equivalent into dir, and returns dir. The caller -// owns the directory and its removal. -func generateGoAsmHeader(pkgDir, goarch, dir string) (string, error) { - imp := newSourceImporter(goarch) +// generateGoAsmHeader type-checks the Go package in pkgDir for goos and +// goarch, writes its go_asm.h equivalent into dir, and returns dir. An +// empty goos means the ambient one. The caller owns the directory and its +// removal. +func generateGoAsmHeader(pkgDir, goos, goarch, dir string) (string, error) { + if goos == "" { + goos = build.Default.GOOS + } + imp := newSourceImporter(goos, goarch) if imp.sizes == nil { return "", fmt.Errorf("go_asm.h: unknown GOARCH %q", goarch) } @@ -89,7 +96,7 @@ func generateGoAsmHeader(pkgDir, goarch, dir string) (string, error) { } var b strings.Builder - fmt.Fprintf(&b, "// generated by gasm from package %s (GOARCH %s)\n\n", bp.Name, goarch) + fmt.Fprintf(&b, "// generated by gasm from package %s (GOOS %s, GOARCH %s)\n\n", bp.Name, goos, goarch) // Files in the build's own order and declarations in source order: the // same walk the compiler's reader makes, so the header reads the same // way the toolchain's does. Order carries no meaning to the assembler @@ -203,13 +210,14 @@ type sourceImporter struct { pkgs map[string]*types.Package } -// newSourceImporter returns the importer for one target architecture. +// newSourceImporter returns the importer for one target GOOS and GOARCH. // Cgo is disabled so the file set is deterministic and independent of the // host's C toolchain: cgo-tagged files drop out of the build exactly as // they do from a CGO_ENABLED=0 build, whose assembly is what gasm targets. -func newSourceImporter(goarch string) *sourceImporter { +func newSourceImporter(goos, goarch string) *sourceImporter { ctxt := new(build.Context) *ctxt = build.Default + ctxt.GOOS = goos ctxt.GOARCH = goarch ctxt.CgoEnabled = false return &sourceImporter{ @@ -291,7 +299,7 @@ func (im *sourceImporter) checkPackage(bp *build.Package, files []*ast.File) (*t // type-check must not be re-checked once per file. type asmhdrCache struct { root string - dirs map[string]string // "pkgDir\x00goarch" -> directory holding go_asm.h + dirs map[string]string // "pkgDir\x00goos\x00goarch" -> directory holding go_asm.h errs map[string]error } @@ -304,17 +312,22 @@ func newAsmhdrCache() (*asmhdrCache, error) { } // dirFor returns the directory holding the generated go_asm.h for pkgDir -// and goarch, generating it on first use. -func (c *asmhdrCache) dirFor(pkgDir, goarch string) (string, error) { - key := pkgDir + "\x00" + goarch +// under goos and goarch, generating it on first use. An empty goos means +// the ambient one, resolved here so that one package cannot generate twice +// under an explicit and an implicit spelling of the same GOOS. +func (c *asmhdrCache) dirFor(pkgDir, goos, goarch string) (string, error) { + if goos == "" { + goos = build.Default.GOOS + } + key := pkgDir + "\x00" + goos + "\x00" + goarch if dir, ok := c.dirs[key]; ok { return dir, nil } if err, ok := c.errs[key]; ok { return "", err } - dir := filepath.Join(c.root, fmt.Sprintf("h%d_%s", len(c.dirs), goarch)) - if _, err := generateGoAsmHeader(pkgDir, goarch, dir); err != nil { + dir := filepath.Join(c.root, fmt.Sprintf("h%d_%s_%s", len(c.dirs), goos, goarch)) + if _, err := generateGoAsmHeader(pkgDir, goos, goarch, dir); err != nil { c.errs[key] = err return "", err } @@ -327,10 +340,10 @@ func (c *asmhdrCache) close() { os.RemoveAll(c.root) } // ensureGoAsmHeader prepares the include directory a file that includes // go_asm.h needs: the generated header for the package in path's directory, -// for the file's target architecture. It reports a usage error when the -// architecture cannot be determined, and passes through the generator's -// diagnostics, which name the package. -func ensureGoAsmHeader(path string, target arch.Arch, cache *asmhdrCache) (string, func(), error) { +// for the file's target GOOS and architecture. It reports a usage error +// when the architecture cannot be determined, and passes through the +// generator's diagnostics, which name the package. +func ensureGoAsmHeader(path string, target arch.Arch, goos string, cache *asmhdrCache) (string, func(), error) { if path == "-" { return "", nil, errors.New("cannot generate go_asm.h for standard input (no package directory)") } @@ -338,14 +351,14 @@ func ensureGoAsmHeader(path string, target arch.Arch, cache *asmhdrCache) (strin return "", nil, errors.New("a file that includes go_asm.h needs a target architecture: name the file _.s or pass -GOARCH") } if cache != nil { - dir, err := cache.dirFor(filepath.Dir(path), goarchName(target)) + dir, err := cache.dirFor(filepath.Dir(path), goos, goarchName(target)) return dir, func() {}, err } root, err := os.MkdirTemp("", "gasm-asmhdr") if err != nil { return "", nil, err } - dir, err := generateGoAsmHeader(filepath.Dir(path), goarchName(target), root) + dir, err := generateGoAsmHeader(filepath.Dir(path), goos, goarchName(target), root) if err != nil { os.RemoveAll(root) return "", nil, err diff --git a/cmd/gasm/asmhdr_test.go b/cmd/gasm/asmhdr_test.go index 370010e..0bba0ff 100644 --- a/cmd/gasm/asmhdr_test.go +++ b/cmd/gasm/asmhdr_test.go @@ -22,12 +22,13 @@ func writePkg(t *testing.T, files map[string]string) string { return dir } -// generateFor generates the header for dir and returns its text. -func generateFor(t *testing.T, dir, goarch string) string { +// generateFor generates the header for dir and returns its text. An empty +// goos means the ambient one. +func generateFor(t *testing.T, dir, goos, goarch string) string { t.Helper() - hdrDir, err := generateGoAsmHeader(dir, goarch, t.TempDir()) + hdrDir, err := generateGoAsmHeader(dir, goos, goarch, t.TempDir()) if err != nil { - t.Fatalf("generateGoAsmHeader(%q, %s): %v", dir, goarch, err) + t.Fatalf("generateGoAsmHeader(%q, %s, %s): %v", dir, goos, goarch, err) } b, err := os.ReadFile(filepath.Join(hdrDir, "go_asm.h")) if err != nil { @@ -72,7 +73,7 @@ type aliased struct { type alias = aliased `}) - hdr := generateFor(t, dir, "amd64") + hdr := generateFor(t, dir, "", "amd64") want := []string{ "#define const_bufSize 1024", // iota resolves through go/types, one define per name. @@ -133,8 +134,8 @@ package perarch const flavour = 2 `, }) - amd64 := generateFor(t, dir, "amd64") - arm64 := generateFor(t, dir, "arm64") + amd64 := generateFor(t, dir, "", "amd64") + arm64 := generateFor(t, dir, "", "arm64") if !strings.Contains(amd64, "#define const_flavour 1\n") { t.Errorf("amd64 header misses const_flavour 1:\n%s", amd64) } @@ -149,19 +150,90 @@ const flavour = 2 if !strings.Contains(amd64, "#define layout__size 16\n") || !strings.Contains(amd64, "#define layout_p 8\n") { t.Errorf("amd64 layout wrong:\n%s", amd64) } - w386 := generateFor(t, dir, "386") + w386 := generateFor(t, dir, "", "386") if !strings.Contains(w386, "#define layout__size 8\n") || !strings.Contains(w386, "#define layout_p 4\n") { t.Errorf("386 layout wrong:\n%s", w386) } } +// TestGenerateGoAsmHeaderGOOS pins the GOOS half of the target: only the +// platform's own files type-check into the header, which is why +// sys_darwin_arm64.s cannot assemble against a linux-generated one. +func TestGenerateGoAsmHeaderGOOS(t *testing.T) { + dir := writePkg(t, map[string]string{ + "common.go": `package goosaware + +type shared struct { + a int32 +} +`, + "plat_darwin.go": `//go:build darwin + +package goosaware + +type platform struct { + trampoline_numer int64 +} +`, + "plat_windows.go": `//go:build windows + +package goosaware + +type platform struct { + callbackArgs__size int32 +} +`, + }) + darwin := generateFor(t, dir, "darwin", "arm64") + if !strings.Contains(darwin, "#define platform__size 8\n") || !strings.Contains(darwin, "#define platform_trampoline_numer 0\n") { + t.Errorf("darwin header misses the darwin layout:\n%s", darwin) + } + if strings.Contains(darwin, "callbackArgs") { + t.Errorf("darwin header must not carry the windows layout:\n%s", darwin) + } + windows := generateFor(t, dir, "windows", "arm64") + if !strings.Contains(windows, "#define platform_callbackArgs__size 0\n") { + t.Errorf("windows header misses the windows layout:\n%s", windows) + } + if strings.Contains(windows, "trampoline_numer") { + t.Errorf("windows header must not carry the darwin layout:\n%s", windows) + } + // The ambient GOOS is neither of the two, so only shared's defines are + // emitted; the shared type keeps its layout there. + ambient := generateFor(t, dir, "", "arm64") + if !strings.Contains(ambient, "#define shared__size 4\n") { + t.Errorf("ambient header misses the shared layout:\n%s", ambient) + } + if strings.Contains(ambient, "#define platform_") { + t.Errorf("ambient header must not carry either platform layout:\n%s", ambient) + } +} + +func TestGoosFromFilename(t *testing.T) { + for path, want := range map[string]string{ + "/x/sys_darwin_arm64.s": "darwin", + "/x/sys_windows_arm64.s": "windows", + "/x/asm_linux_amd64.s": "linux", + "/x/rt0_darwin_arm64.s": "darwin", + "/x/vgetrandom_zos_s390x.s": "zos", + "/x/rt0_js_wasm.s": "js", + "/x/memmove_amd64.s": "", + "/x/vlop_arm.s": "", + "/x/stubs.s": "", + } { + if got := goosFromFilename(path); got != want { + t.Errorf("goosFromFilename(%q) = %q, want %q", path, got, want) + } + } +} + func TestGenerateGoAsmHeaderErrors(t *testing.T) { t.Run("type error", func(t *testing.T) { dir := writePkg(t, map[string]string{"bad.go": `package bad const x = undefinedIdent `}) - _, err := generateGoAsmHeader(dir, "amd64", t.TempDir()) + _, err := generateGoAsmHeader(dir, "", "amd64", t.TempDir()) if err == nil { t.Fatal("generation must fail for a package that does not type-check") } @@ -174,7 +246,7 @@ const x = undefinedIdent }) t.Run("no go files", func(t *testing.T) { dir := t.TempDir() - _, err := generateGoAsmHeader(dir, "amd64", t.TempDir()) + _, err := generateGoAsmHeader(dir, "", "amd64", t.TempDir()) if err == nil { t.Fatal("generation must fail without Go files") } @@ -281,6 +353,45 @@ type header struct { } } +// TestRunCorpusAuditGOOS covers the filename-derived GOOS end to end: a +// kernel whose name names darwin must have its header type-checked with +// GOOS=darwin, so the darwin-only constant it offsets with is defined. The +// operand mirrors sys_darwin_arm64.s's trampoline, where a missing define +// leaves an unexpanded symbol in the offset and fails. +func TestRunCorpusAuditGOOS(t *testing.T) { + dir := t.TempDir() + write := func(name, src string) { + t.Helper() + if err := os.WriteFile(filepath.Join(dir, name), []byte(src), 0o644); err != nil { + t.Fatal(err) + } + } + write("pkg.go", "package corpus\n") + write("plat_darwin.go", "//go:build darwin\n\npackage corpus\n\nconst trampolineNumer = 8\n") + write("kern_darwin_arm64.s", "#include \"go_asm.h\"\n"+ + "GLOBL timebase<>(SB), NOPTR, $16\n"+ + "TEXT \xc2\xb7g(SB), NOSPLIT, $0-0\n"+ + "\tMOVD\ttimebase<>+const_trampolineNumer(SB), R0\n"+ + "\tRET\n") + + stats, err := runCorpusAudit(dir, nil) + if err != nil { + t.Fatalf("runCorpusAudit: %v", err) + } + var arm *corpusTally + for i, tg := range stats.targets { + if tg.name == "arm64" { + arm = stats.tallies[i] + } + } + if arm == nil { + t.Fatal("no arm64 tally") + } + if arm.attempted != 1 || arm.assembled != 1 { + t.Errorf("arm64 = %d/%d, want 1/1; reasons: %v", arm.assembled, arm.attempted, arm.reasons) + } +} + // TestGenerateGoAsmHeaderRuntime pins the generator against the real thing: // the runtime package, whose header the toolchain's own -asmhdr output was // sampled from. Skipped in short mode: it type-checks the whole package. @@ -288,7 +399,7 @@ func TestGenerateGoAsmHeaderRuntime(t *testing.T) { if testing.Short() { t.Skip("type-checks the whole runtime package") } - dir, err := generateGoAsmHeader("/usr/local/go/src/runtime", "amd64", t.TempDir()) + dir, err := generateGoAsmHeader("/usr/local/go/src/runtime", "", "amd64", t.TempDir()) if err != nil { t.Fatalf("generateGoAsmHeader(runtime): %v", err) } diff --git a/cmd/gasm/audit.go b/cmd/gasm/audit.go index d38337f..33ae9c1 100644 --- a/cmd/gasm/audit.go +++ b/cmd/gasm/audit.go @@ -5,6 +5,7 @@ package main import ( "fmt" + "maps" "os" "os/exec" "path/filepath" @@ -288,6 +289,11 @@ func probeShapes(a arch.Arch) []string { "V1.B16, [V2.B16], V3.B16", "V1.B8, [V2.B16, V3.B16], V4.B8", "$4, V1.B16, V2.B16, V3.B16", "$15, V1", "V1, V2, p2", "R0, R1, $1, $4, p2", + // The landing-pad kind, the compiler's PCDATA + // bookkeeping and the four-operand bitfield + // insert/extract family, as the toolchain's own + // testdata spells them. + "C", "$1, $0", "$0, R1, $1, R2", } case arch.RISCV: return []string{ @@ -307,6 +313,9 @@ func probeShapes(a arch.Arch) []string { "X5, X6, p2", "R5, R6, p2", "X5, E8, M8, TA, MA, X6", "$4, E32, M1, TA, MA, X1", "(X5), X6, V1, V2", + // The CSR immediate forms the toolchain's testdata spells: + // immediate, CSR name, destination. + "$2, TIME, X5", "", } case arch.LOONG64: @@ -323,6 +332,12 @@ func probeShapes(a arch.Arch) []string { "V1, V2, V3", "X1, X2, X3", "V1, V2", "X1, X2", "V1", "X1", // The vector compare-to-flag forms land in an FCC register. "V1, FCC0", "X1, FCC0", + // The compiler's bookkeeping pair and the raw spellings the + // toolchain's own testdata carries: JIRL rd, rj, offset (the + // form RET lowers to), the prefetch with a 32-bit address and + // hint, and the byte-shuffle quads. + "$1, $0", "R1, R5, 0", "0(R7), $5, $0", "(R7), $5, $0", + "V1, V2, V3, V4", "X1, X2, X3, X4", "", } } @@ -487,10 +502,47 @@ func otherPortFile(path string) bool { // goOSNames are the GOOS values go/build recognises in file names. var goOSNames = map[string]bool{ - "aix": true, "darwin": true, "dragonfly": true, "freebsd": true, - "ios": true, "js": true, "linux": true, "netbsd": true, + "aix": true, "android": true, "darwin": true, "dragonfly": true, + "freebsd": true, "hurd": true, "illumos": true, "ios": true, + "js": true, "linux": true, "nacl": true, "netbsd": true, "openbsd": true, "plan9": true, "solaris": true, "wasip1": true, - "windows": true, + "windows": true, "zos": true, +} + +// resolveGOOS validates a -GOOS flag value, mirroring the architecture +// check's surface: a usage error naming what the tool accepts. +func resolveGOOS(name string) (string, error) { + lower := strings.ToLower(name) + if goOSNames[lower] { + return lower, nil + } + return "", &usageError{fmt.Errorf("unknown GOOS %q: want one of %s", name, strings.Join(slices.Sorted(maps.Keys(goOSNames)), ", "))} +} + +// goosFromFilename returns the GOOS the file's name carries, by go/build's +// goodOSArchFile rule: the GOOS segment sits last, or last before the +// architecture segment (sys_darwin_arm64.s, vlop_arm.s carries none). An +// empty result means the name names no GOOS and the ambient one applies. +func goosFromFilename(path string) string { + base := path + if i := strings.LastIndexByte(base, '/'); i >= 0 { + base = base[i+1:] + } + base = strings.TrimSuffix(base, ".s") + // go/build ignores everything before the first underscore, so a GOOS + // segment is only ever looked for from there on. + i := strings.IndexByte(base, '_') + if i < 0 { + return "" + } + segs := strings.Split(base[i:], "_") + if n := len(segs); n >= 2 && goOSNames[segs[n-2]] && slices.Contains(goPortSuffixes, segs[n-1]) { + return segs[n-2] + } + if goOSNames[segs[len(segs)-1]] { + return segs[len(segs)-1] + } + return "" } // otherGOOSFile reports whether the file's name names a GOOS other than the @@ -543,6 +595,12 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) { return nil, err } + // The GOOS the header generation type-checks under follows the + // file's name when the name carries one; the ambient GOOS is the + // honest guess otherwise (a build tag naming another GOOS is + // invisible to a file-name rule). + goos := goosFromFilename(path) + var wanted []int // indexes into targets if a := arch.FromFilename(path); a != arch.Unknown { for i, tg := range targets { @@ -567,11 +625,12 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) { // A file that includes go_asm.h parses against a per-target header: // the defines differ per architecture (internal/cpu's layout, for - // one), so the parse cannot be shared the way a header-free file's - // can. A generation failure is a failure for every target, named - // for the package rather than a bare "include not found". A header - // already resolvable in the package directory or the -I list is - // left alone. + // one) and per GOOS (sys_darwin_arm64.s's trampoline constants, + // for another), so the parse cannot be shared the way a + // header-free file's can. A generation failure is a failure for + // every target, named for the package rather than a bare "include + // not found". A header already resolvable in the package + // directory or the -I list is left alone. if len(wanted) > 0 && needsGoAsmHeader(src) && !goAsmHeaderResolved(filepath.Dir(path), dirs) { if hdr == nil { if hdr, err = newAsmhdrCache(); err != nil { @@ -583,7 +642,7 @@ func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) { for _, i := range wanted { tg, t := targets[i], tallies[i] t.attempted++ - hdrDir, err := hdr.dirFor(pkgDir, goarchName(tg.a)) + hdrDir, err := hdr.dirFor(pkgDir, goos, goarchName(tg.a)) if err != nil { ok = false t.fail(path, corpusReason(err)) diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index 0ba70d7..f34f0e4 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -486,7 +486,7 @@ hover, document symbols, diagnostics and semantic-token highlighting. } func cmdAsm(args []string) int { - fs := newCommand("asm", "gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-o out] ", ` + fs := newCommand("asm", "gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-GOOS os] [-o out] ", ` Assemble FILE without the Go toolchain: every TEXT function is encoded to machine code and printed as a hex dump. Supported architectures: amd64 (including VEX/AVX2 and EVEX/AVX-512), arm64 (AArch64 integer, FP, @@ -507,18 +507,22 @@ and the format version from go version). A file that includes go_asm.h gets that header generated automatically from the package it lives in (the .go files beside it, type-checked for the target architecture, the toolchain's own defines), so GOROOT assembly -assembles without a compiler. A package that has no Go files for the -target or does not type-check is a hard error naming the package. +assembles without a compiler. -GOOS selects the type-checking GOOS for +that header: a GOOS-specific file (sys_darwin_arm64.s) needs its platform's +defines, which a header from the ambient GOOS silently omits. A package +that has no Go files for the target or does not type-check is a hard error +naming the package. `) out := fs.String("o", "", "write the output to this file") format := fs.String("format", "raw", "output format: raw (concatenated image), elf or goobj (Go object)") pkg := fs.String("p", "", "package path for --format goobj (qualifies the exported symbols)") archName := fs.String("GOARCH", "", "target architecture: amd64, arm64, riscv64 or loong64 (overrides the file-name suffix)") + goosName := fs.String("GOOS", "", "operating system for go_asm.h generation: a GOOS go/build recognises (default: the host's)") var dirs includeDirs fs.Var(&dirs, "I", "directory to search for #include files (may be repeated)") fs.Parse(args) if fs.NArg() != 1 { - fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-o out] ") + fmt.Fprintln(os.Stderr, "usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-GOOS os] [-o out] ") return 2 } // The format is validated before anything else, so a bogus value exits 2 @@ -539,6 +543,15 @@ target or does not type-check is a hard error naming the package. } targetArch = a } + goos := "" + if *goosName != "" { + g, err := resolveGOOS(*goosName) + if err != nil { + fmt.Fprintf(os.Stderr, "gasm asm: %v\n", err) + return 2 + } + goos = g + } src, err := readSource(path) if err != nil { fmt.Fprintln(os.Stderr, "gasm:", err) @@ -553,7 +566,7 @@ target or does not type-check is a hard error naming the package. // A go_asm.h that already resolves (placed by hand, or passed with -I) // is left alone. if needsGoAsmHeader(src) && !goAsmHeaderResolved(filepath.Dir(path), dirs) { - hdrDir, cleanup, err := ensureGoAsmHeader(path, targetArch, nil) + hdrDir, cleanup, err := ensureGoAsmHeader(path, targetArch, goos, nil) if err != nil { fmt.Fprintln(os.Stderr, "gasm asm:", err) return 1 diff --git a/docs/CLI.md b/docs/CLI.md index 1966ebb..45db2bd 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -141,7 +141,7 @@ gasm lint kernel_amd64.s ## asm ```text -Usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-o out] +Usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-GOOS os] [-o out] ``` | Flag | Default | Effect | @@ -150,6 +150,7 @@ Usage: gasm asm [--format raw|elf|goobj] [-I dir] [-p pkg] [-GOARCH arch] [-o ou | `-I` | empty | directory to search for `#include` files; may be repeated, searched in order after the source directory | | `-p` | empty | package path for `--format goobj`, qualifying the exported symbols | | `-GOARCH` | empty | target architecture: `amd64`, `arm64`, `riscv64` or `loong64`; overrides the file-name suffix | +| `-GOOS` | empty | operating system for the generated `go_asm.h`: any GOOS `go/build` recognises in file names; default is the host's | | `-o` | empty | write the output to this file instead of a hex dump on stdout | Supported architectures: amd64 (VEX/AVX2 and EVEX/AVX-512 included), arm64, @@ -163,6 +164,13 @@ system toolchain; `goobj` emits the Go toolchain's own object format, which installed: the object preamble is captured from `go tool asm` and the format version from `go version`. `raw` and `elf` need no toolchain at all. +A file that includes `go_asm.h` gets that header generated from the Go +files beside it, type-checked for the target. `-GOOS` selects the +type-checking GOOS for that header, because a GOOS-specific file needs its +platform's defines: `sys_darwin_arm64.s` fails against the ambient GOOS +(`machTimebaseInfo_numer` is missing from a linux type-check) and assembles +with `-GOOS darwin`. + Assembly preprocessing matches the toolchain's: `#define` macros (object and parameterised) expand at the point of use, `#undef`, `#ifdef`, `#ifndef`, `#else` and `#endif` behave as in `go tool asm`, `;` separates statements, @@ -390,7 +398,9 @@ With `--corpus` the audit changes shape: it assembles every `.s` file under DIR (default `GOROOT/src`) with the gasm encoder only, no toolchain probing. A file whose name carries a recognisable `_arch` suffix is attempted for that architecture; a file without one is attempted for all four, exactly as a -`GOARCH` build would compile it. The report gives the headline number (files +`GOARCH` build would compile it, and a name that names a GOOS +(`sys_darwin_arm64.s`) type-checks its generated `go_asm.h` for that GOOS. +The report gives the headline number (files that assemble for every target architecture), the per-architecture pass rates and the most common failure reasons with one representative file each, which drive the encodability backlog by frequency rather than by table order. A run diff --git a/docs/man/gasm-asm.1 b/docs/man/gasm-asm.1 index 0aae0c1..e6eb5ce 100644 --- a/docs/man/gasm-asm.1 +++ b/docs/man/gasm-asm.1 @@ -2,7 +2,7 @@ .SH NAME gasm-asm \- assemble Plan 9 assembly without the Go toolchain .SH SYNOPSIS -.B gasm asm [\-\-format raw|elf|goobj] [\-I dir] [\-p pkg] [\-GOARCH arch] [\-o out] +.B gasm asm [\-\-format raw|elf|goobj] [\-I dir] [\-p pkg] [\-GOARCH arch] [\-GOOS os] [\-o out] .SH DESCRIPTION Assemble FILE without the Go toolchain: every TEXT function is encoded to machine code and printed as a hex dump. Supported architectures: @@ -42,6 +42,15 @@ need no toolchain at all. Framed functions receive the stack-split guard and the trailing morestack block, byte-identical to the toolchain's output, so split functions link too. +.PP +A file that includes go_asm.h gets that header generated from the Go +files beside it, type-checked for the target. +.B \-GOOS +selects the type-checking GOOS for that header, because a GOOS-specific +file needs its platform's defines: sys_darwin_arm64.s fails against the +ambient GOOS (machTimebaseInfo_numer is missing from a linux type-check) +and assembles with +.BR "\-GOOS darwin" . .SH OPTIONS .TP .B \-\-format \fIraw|elf|goobj\fR @@ -59,6 +68,10 @@ Target architecture: amd64, arm64, riscv64 or loong64; overrides the file-name suffix, which is how the suffix-less majority of GOROOT's files (cpu_x86.s, stub.s, ...) become assemblable. .TP +.B \-GOOS \fIos\fR +Operating system for the generated go_asm.h: any GOOS go/build +recognises in file names; the default is the host's. +.TP .B \-o \fIfile\fR Write the output to this file instead of a hex dump on stdout. .SH EXIT STATUS diff --git a/format/format.go b/format/format.go index fbdbc52..105e112 100644 --- a/format/format.go +++ b/format/format.go @@ -241,6 +241,13 @@ func renderInstr(line []token.Token, width int) string { if line[0].Kind != token.Ident { return "\t" + mnem + " " + ops } + // A statement separator belongs to the statement it ends: when the + // operands open with a ';', the alignment padding would land between + // the mnemonic and its own separator (REP ; MOVSQ), so such a line + // renders with a single space whatever the function's width. + if strings.HasPrefix(ops, ";") { + return "\t" + mnem + " " + ops + } if width < len(mnem) { width = len(mnem) } diff --git a/format/format_test.go b/format/format_test.go index c8de510..b45340b 100644 --- a/format/format_test.go +++ b/format/format_test.go @@ -364,6 +364,17 @@ func TestSemicolonSeparators(t *testing.T) { in: "TEXT ·f(SB), $0\nBYTE $1;\nRET\n", want: "TEXT ·f(SB), $0\n\tBYTE $1;\n\tRET\n", }, + { + // The REP shape: a prefix-style zero-operand statement + // followed by the instruction it prefixes. The separator + // belongs to the statement it ends, so the function's + // alignment width (MOVSQ is the widest mnemonic here) must + // not open a gap before it: one space after the mnemonic + // whatever the neighbours' lengths. + name: "after a prefix-style statement", + in: "TEXT ·f(SB), $0\nMOVQ AX, BX\nREP; MOVSQ\nRET\n", + want: "TEXT ·f(SB), $0\n\tMOVQ AX, BX\n\tREP ; MOVSQ\n\tRET\n", + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) {