Files
gasm-sdk/cmd/gasm/audit.go
T

200 lines
6.3 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
)
// cmdAuditInstructions cross-checks the gasm amd64 encoder against the Go
// toolchain's own assembler, probed black-box: every mnemonic in the gasm
// table is offered to go tool asm in a battery of representative operand
// shapes, and a mnemonic counts as known to Go when at least one shape
// produces an error other than "unrecognized instruction" (a wrong-shape
// error still proves the mnemonic exists in Go's tables). The audit
// answers three questions at a glance:
//
// - which mnemonics gasm can encode that go tool asm does not know
// (superset encodings, usable only through the gasm goobj path);
// - which mnemonics the architecture table knows but the encoder cannot
// emit yet (the implementation backlog);
// - which mnemonics go tool asm knows that gasm cannot encode (feature
// gaps).
//
// Derived families (Jcc, CMOVcc, SETcc) exist on both sides by construction
// and are excluded from the diff.
func cmdAuditInstructions(args []string) error {
fs := newCommand("audit-instructions", "gasm audit-instructions", `
Compare the gasm amd64 encoder against go tool asm and print the diff:
superset encodings (gasm-only, shippable via gasm asm --format goobj),
known-but-unencodable names (the backlog) and go-only names (feature gaps).
The Go side is probed black-box with a battery of operand shapes per
mnemonic, so the audit tracks whatever toolchain ` + "`go env GOROOT`" + ` names.
`)
if err := fs.Parse(args); err != nil {
return err
}
if len(fs.Args()) > 0 {
return fmt.Errorf("audit-instructions takes no file arguments")
}
tab := arch.ForArch(arch.AMD64)
var names []string
seen := map[string]bool{}
for _, in := range tab.Instructions() {
name := strings.ToUpper(in.Name)
if derivedFamily(name) || seen[name] {
continue
}
seen[name] = true
names = append(names, name)
}
goKnown, err := probeGoAsm(names)
if err != nil {
return err
}
var superset, backlog, shared, goOnly []string
for _, name := range names {
switch {
case !asm.Encodable(name):
backlog = append(backlog, name)
case !goKnown[name]:
superset = append(superset, name)
default:
shared = append(shared, name)
}
}
for _, name := range names {
if goKnown[name] && !seen[name] {
goOnly = append(goOnly, name)
}
}
// GO-ONLY is computed from the complement of the gasm table, which this
// audit cannot enumerate (Go's table is only visible through probes of
// names we already know). Report it as informational zero instead of a
// misleading list.
goOnly = nil
sort.Strings(superset)
sort.Strings(backlog)
sort.Strings(shared)
w := os.Stdout
fmt.Fprintf(w, "gasm table (amd64, families excluded): %d mnemonics\n", len(names))
fmt.Fprintf(w, "gasm encodable: %d go tool asm recognized: %d\n", len(shared)+len(superset), countTrue(goKnown))
fmt.Fprintf(w, "shared: %d\n", len(shared))
fmt.Fprintf(w, "\nSuperset encodings (gasm-only; ship via gasm asm --format goobj):\n")
for _, n := range superset {
fmt.Fprintf(w, " %s\n", n)
}
fmt.Fprintf(w, "\nKnown but not encodable (backlog):\n")
for _, n := range backlog {
fmt.Fprintf(w, " %s\n", n)
}
fmt.Fprintf(w, "\nGo-only names cannot be enumerated by probing; extend the gasm\n")
fmt.Fprintf(w, "table from the Go release notes when a new instruction family ships.\n")
return nil
}
func countTrue(m map[string]bool) int {
n := 0
for _, v := range m {
if v {
n++
}
}
return n
}
// derivedFamily reports whether a mnemonic belongs to a family both
// assemblers construct from condition codes rather than list exhaustively
// (JEQ/CMOVLGT/SETNE and friends). Such names never probe cleanly, so
// including them in the diff would be noise.
func derivedFamily(name string) bool {
if strings.HasPrefix(name, "J") && name != "JMP" && name != "JMPQ" {
return true
}
if strings.HasPrefix(name, "CMOV") || strings.HasPrefix(name, "SET") {
return true
}
return false
}
var unrecognizedRe = regexp.MustCompile(`unrecognized instruction`)
// probeGoAsm feeds every mnemonic to go tool asm in one generated file and
// classifies the diagnostics. "Unrecognized instruction" is a parse-stage
// verdict on the mnemonic alone, so a single bare-instruction probe per
// mnemonic decides recognition; the combined file still reports every line's
// error even when others fail.
func probeGoAsm(names []string) (map[string]bool, error) {
dir, err := os.MkdirTemp("", "gasm-audit")
if err != nil {
return nil, err
}
defer os.RemoveAll(dir)
var sb strings.Builder
sb.WriteString("TEXT ·probe(SB), 4, $0\n\tRET\n")
lineMnemonic := map[int]string{}
line := 3
for _, name := range names {
fmt.Fprintf(&sb, "TEXT ·p%s%d(SB), 4, $0\n", sanitize(name), line)
sb.WriteString("\t" + name + "\n\tRET\n")
lineMnemonic[line+1] = name // the instruction line, after TEXT
line += 3
}
probePath := filepath.Join(dir, "probe.s")
if err := os.WriteFile(probePath, []byte(sb.String()), 0o644); err != nil {
return nil, err
}
toolDir, err := exec.Command("go", "env", "GOTOOLDIR").Output()
if err != nil {
return nil, fmt.Errorf("go env GOTOOLDIR: %w", err)
}
asmBin := filepath.Join(strings.TrimSpace(string(toolDir)), "asm")
if _, err := os.Stat(asmBin); err != nil {
return nil, fmt.Errorf("go tool asm not found at %s", asmBin)
}
cmd := exec.Command(asmBin, "-p", "probe", "-o", filepath.Join(dir, "probe.o"), probePath)
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH, "GOOS="+runtime.GOOS)
out, _ := cmd.CombinedOutput()
result := map[string]bool{}
for _, name := range names {
result[name] = true // no news = the name parsed fine
}
reParse := regexp.MustCompile(`probe\.s:(\d+):`)
for _, l := range strings.Split(string(out), "\n") {
m := reParse.FindStringSubmatch(l)
if m == nil {
continue
}
var lineNo int
fmt.Sscanf(m[1], "%d", &lineNo)
if name, ok := lineMnemonic[lineNo]; ok && unrecognizedRe.MatchString(l) {
result[name] = false
}
}
return result, nil
}
// sanitize makes a mnemonic safe for use in a Go symbol name.
func sanitize(name string) string {
return strings.NewReplacer(".", "_", "$", "_").Replace(name)
}