feat(gasm): audit-instructions for arm64, riscv64 and loong64
Assisted-by: GLM 5.3 Flash
This commit is contained in:
+6
-4
@@ -43,10 +43,12 @@ Unreleased changes on the `development` branch.
|
|||||||
- **`gasm verify --args`.** Scalar arguments (`name=value`, decimal or
|
- **`gasm verify --args`.** Scalar arguments (`name=value`, decimal or
|
||||||
`0x` hex) can now be supplied to a `--call` invocation alongside `--buf`
|
`0x` hex) can now be supplied to a `--call` invocation alongside `--buf`
|
||||||
buffers, closing the gap where only buffers could be supplied.
|
buffers, closing the gap where only buffers could be supplied.
|
||||||
- **`gasm audit-instructions`.** Black-box diff of the amd64 encoder
|
- **`gasm audit-instructions`.** Black-box diff of a gasm encoder
|
||||||
against the installed `go tool asm`: superset encodings (gasm-only,
|
against the installed `go tool asm`, for amd64, arm64, riscv64 and
|
||||||
shippable via `gasm asm --format goobj`), known-but-unencodable names
|
loong64 (`gasm audit-instructions <arch>`): superset encodings
|
||||||
(the backlog) and go-only names (feature gaps).
|
(gasm-only, shippable via `gasm asm --format goobj`),
|
||||||
|
known-but-unencodable names (the backlog) and go-only names
|
||||||
|
(feature gaps).
|
||||||
- **`gasm scaffold differential`.** Prints a differential test skeleton
|
- **`gasm scaffold differential`.** Prints a differential test skeleton
|
||||||
for every `// func` signature in a kernel file: random seed states, the
|
for every `// func` signature in a kernel file: random seed states, the
|
||||||
kernel call and a portable reference (`<name>Portable`), compared
|
kernel call and a portable reference (`<name>Portable`), compared
|
||||||
|
|||||||
+138
-25
@@ -16,15 +16,15 @@ import (
|
|||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
// cmdAuditInstructions cross-checks the gasm amd64 encoder against the Go
|
// cmdAuditInstructions cross-checks a gasm encoder against the Go toolchain's
|
||||||
// toolchain's own assembler, probed black-box: every mnemonic in the gasm
|
// own assembler, probed black-box: every mnemonic in the gasm table is offered
|
||||||
// table is offered to go tool asm in a battery of representative operand
|
// to go tool asm in its bare form, and a mnemonic counts as known to Go when
|
||||||
// shapes, and a mnemonic counts as known to Go when at least one shape
|
// the error is anything but "unrecognized instruction" (a wrong-shape error
|
||||||
// produces an error other than "unrecognized instruction" (a wrong-shape
|
// still proves the mnemonic exists in Go's tables). The audit answers three
|
||||||
// error still proves the mnemonic exists in Go's tables). The audit
|
// questions at a glance:
|
||||||
// answers three questions at a glance:
|
|
||||||
//
|
//
|
||||||
// - which mnemonics gasm can encode that go tool asm does not know
|
// - which mnemonics gasm can encode that go tool asm does not know
|
||||||
// (superset encodings, usable only through the gasm goobj path);
|
// (superset encodings, usable only through the gasm goobj path);
|
||||||
@@ -33,36 +33,46 @@ import (
|
|||||||
// - which mnemonics go tool asm knows that gasm cannot encode (feature
|
// - which mnemonics go tool asm knows that gasm cannot encode (feature
|
||||||
// gaps).
|
// gaps).
|
||||||
//
|
//
|
||||||
// Derived families (Jcc, CMOVcc, SETcc) exist on both sides by construction
|
// The amd64 derived families (Jcc, CMOVcc, SETcc) exist on both sides by
|
||||||
// and are excluded from the diff.
|
// construction and are excluded from the diff; the other architectures list
|
||||||
|
// their conditional branches outright.
|
||||||
func cmdAuditInstructions(args []string) error {
|
func cmdAuditInstructions(args []string) error {
|
||||||
fs := newCommand("audit-instructions", "gasm audit-instructions", `
|
fs := newCommand("audit-instructions", "gasm audit-instructions [amd64|arm64|riscv64|loong64]", `
|
||||||
Compare the gasm amd64 encoder against go tool asm and print the diff:
|
Compare the gasm encoder for the given architecture (default amd64) against
|
||||||
superset encodings (gasm-only, shippable via gasm asm --format goobj),
|
go tool asm and print the diff: superset encodings (gasm-only, shippable via
|
||||||
known-but-unencodable names (the backlog) and go-only names (feature gaps).
|
gasm asm --format goobj), known-but-unencodable names (the backlog) and go-
|
||||||
The Go side is probed black-box with a battery of operand shapes per
|
only names (feature gaps). The Go side is probed black-box with a battery
|
||||||
mnemonic, so the audit tracks whatever toolchain `+"`go env GOROOT`"+` names.
|
of bare mnemonics, so the audit tracks whatever toolchain `+"`go env GOROOT`"+`
|
||||||
|
provides.
|
||||||
`)
|
`)
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(fs.Args()) > 0 {
|
archName := "amd64"
|
||||||
return fmt.Errorf("audit-instructions takes no file arguments")
|
switch n := len(fs.Args()); {
|
||||||
|
case n > 1:
|
||||||
|
return fmt.Errorf("audit-instructions takes at most one architecture argument")
|
||||||
|
case n == 1:
|
||||||
|
archName = strings.ToLower(fs.Arg(0))
|
||||||
|
}
|
||||||
|
a, err := auditArch(archName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tab := arch.ForArch(arch.AMD64)
|
tab := arch.ForArch(a)
|
||||||
var names []string
|
var names []string
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for _, in := range tab.Instructions() {
|
for _, in := range tab.Instructions() {
|
||||||
name := strings.ToUpper(in.Name)
|
name := strings.ToUpper(in.Name)
|
||||||
if derivedFamily(name) || seen[name] {
|
if a == arch.AMD64 && derivedFamily(name) || seen[name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[name] = true
|
seen[name] = true
|
||||||
names = append(names, name)
|
names = append(names, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
goKnown, err := probeGoAsm(names)
|
goKnown, err := probeGoAsm(goarchName(a), names)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -70,7 +80,7 @@ mnemonic, so the audit tracks whatever toolchain `+"`go env GOROOT`"+` names.
|
|||||||
var superset, backlog, shared []string
|
var superset, backlog, shared []string
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
switch {
|
switch {
|
||||||
case !asm.Encodable(name):
|
case !gasmEncodable(a, name):
|
||||||
backlog = append(backlog, name)
|
backlog = append(backlog, name)
|
||||||
case !goKnown[name]:
|
case !goKnown[name]:
|
||||||
superset = append(superset, name)
|
superset = append(superset, name)
|
||||||
@@ -86,7 +96,7 @@ mnemonic, so the audit tracks whatever toolchain `+"`go env GOROOT`"+` names.
|
|||||||
slices.Sort(shared)
|
slices.Sort(shared)
|
||||||
|
|
||||||
w := os.Stdout
|
w := os.Stdout
|
||||||
fmt.Fprintf(w, "gasm table (amd64, families excluded): %d mnemonics\n", len(names))
|
fmt.Fprintf(w, "gasm table (%s, families excluded): %d mnemonics\n", archName, len(names))
|
||||||
fmt.Fprintf(w, "gasm encodable: %d go tool asm recognized: %d\n", len(shared)+len(superset), countTrue(goKnown))
|
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, "shared: %d\n", len(shared))
|
||||||
fmt.Fprintf(w, "\nSuperset encodings (gasm-only; ship via gasm asm --format goobj):\n")
|
fmt.Fprintf(w, "\nSuperset encodings (gasm-only; ship via gasm asm --format goobj):\n")
|
||||||
@@ -102,6 +112,34 @@ mnemonic, so the audit tracks whatever toolchain `+"`go env GOROOT`"+` names.
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// auditArch resolves the audit's architecture argument.
|
||||||
|
func auditArch(name string) (arch.Arch, error) {
|
||||||
|
switch strings.ToLower(name) {
|
||||||
|
case "amd64":
|
||||||
|
return arch.AMD64, nil
|
||||||
|
case "arm64":
|
||||||
|
return arch.ARM64, nil
|
||||||
|
case "riscv64", "riscv":
|
||||||
|
return arch.RISCV, nil
|
||||||
|
case "loong64", "loong":
|
||||||
|
return arch.LOONG64, nil
|
||||||
|
}
|
||||||
|
return arch.Unknown, fmt.Errorf("unknown architecture %q: want amd64, arm64, riscv64 or loong64", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// goarchName maps an arch identifier onto its GOARCH spelling.
|
||||||
|
func goarchName(a arch.Arch) string {
|
||||||
|
switch a {
|
||||||
|
case arch.ARM64:
|
||||||
|
return "arm64"
|
||||||
|
case arch.RISCV:
|
||||||
|
return "riscv64"
|
||||||
|
case arch.LOONG64:
|
||||||
|
return "loong64"
|
||||||
|
}
|
||||||
|
return "amd64"
|
||||||
|
}
|
||||||
|
|
||||||
func countTrue(m map[string]bool) int {
|
func countTrue(m map[string]bool) int {
|
||||||
n := 0
|
n := 0
|
||||||
for _, v := range m {
|
for _, v := range m {
|
||||||
@@ -115,7 +153,8 @@ func countTrue(m map[string]bool) int {
|
|||||||
// derivedFamily reports whether a mnemonic belongs to a family both
|
// derivedFamily reports whether a mnemonic belongs to a family both
|
||||||
// assemblers construct from condition codes rather than list exhaustively
|
// assemblers construct from condition codes rather than list exhaustively
|
||||||
// (JEQ/CMOVLGT/SETNE and friends). Such names never probe cleanly, so
|
// (JEQ/CMOVLGT/SETNE and friends). Such names never probe cleanly, so
|
||||||
// including them in the diff would be noise.
|
// including them in the diff would be noise. amd64 only: the other
|
||||||
|
// architectures list their conditional branches outright.
|
||||||
func derivedFamily(name string) bool {
|
func derivedFamily(name string) bool {
|
||||||
if strings.HasPrefix(name, "J") && name != "JMP" && name != "JMPQ" {
|
if strings.HasPrefix(name, "J") && name != "JMP" && name != "JMPQ" {
|
||||||
return true
|
return true
|
||||||
@@ -133,7 +172,7 @@ var unrecognizedRe = regexp.MustCompile(`unrecognized instruction`)
|
|||||||
// verdict on the mnemonic alone, so a single bare-instruction probe per
|
// verdict on the mnemonic alone, so a single bare-instruction probe per
|
||||||
// mnemonic decides recognition; the combined file still reports every line's
|
// mnemonic decides recognition; the combined file still reports every line's
|
||||||
// error even when others fail.
|
// error even when others fail.
|
||||||
func probeGoAsm(names []string) (map[string]bool, error) {
|
func probeGoAsm(goarch string, names []string) (map[string]bool, error) {
|
||||||
dir, err := os.MkdirTemp("", "gasm-audit")
|
dir, err := os.MkdirTemp("", "gasm-audit")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -164,7 +203,7 @@ func probeGoAsm(names []string) (map[string]bool, error) {
|
|||||||
return nil, fmt.Errorf("go tool asm not found at %s", asmBin)
|
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 := exec.Command(asmBin, "-p", "probe", "-o", filepath.Join(dir, "probe.o"), probePath)
|
||||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH, "GOOS="+runtime.GOOS)
|
cmd.Env = append(os.Environ(), "GOARCH="+goarch, "GOOS="+runtime.GOOS)
|
||||||
out, _ := cmd.CombinedOutput()
|
out, _ := cmd.CombinedOutput()
|
||||||
|
|
||||||
result := map[string]bool{}
|
result := map[string]bool{}
|
||||||
@@ -188,6 +227,80 @@ func probeGoAsm(names []string) (map[string]bool, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// probeShapes lists representative operand shapes for the encodability
|
||||||
|
// probe. The assemblers report an unknown mnemonic and a known mnemonic
|
||||||
|
// with no supported form alike ("unsupported <arch> instruction"), so only
|
||||||
|
// a shape that assembles cleanly counts, and the backlog over-approximates:
|
||||||
|
// a name whose real forms the battery misses lands there. amd64 keeps its
|
||||||
|
// exact table-driven check.
|
||||||
|
func probeShapes(a arch.Arch) []string {
|
||||||
|
switch a {
|
||||||
|
case arch.ARM64:
|
||||||
|
return []string{
|
||||||
|
"X0, X1, X2", "X0, X1", "X0", "$1, X0", "X0, (X1)", "(X0), X1",
|
||||||
|
"X0, (X1, 8)", "(SP), X0", "F0, F1, F2", "F0, F1", "F0",
|
||||||
|
"V0.B16, V1.B16, V2.B16", "p2", "X0, p2", "X0, X1, p2",
|
||||||
|
// The conditional select family spells the condition first
|
||||||
|
// and takes R register spellings.
|
||||||
|
"EQ, R0, R1, R2", "EQ, R0, R1", "EQ, R0",
|
||||||
|
"GE, F0, F1, F2", "NE, F0, F1, $0",
|
||||||
|
}
|
||||||
|
case arch.RISCV:
|
||||||
|
return []string{
|
||||||
|
"X5, X6, X7", "X5, X6", "X5", "$1, X5", "X5, (X6)", "$1, X5, X6",
|
||||||
|
"(X5), X6", "F0, F1, F2", "F0, F1", "p2", "X1, p2", "X0, p2",
|
||||||
|
"X5, X6, p2", "p2(SB)",
|
||||||
|
}
|
||||||
|
case arch.LOONG64:
|
||||||
|
return []string{
|
||||||
|
"R4, R5, R6", "R4, R5", "R4", "$1, R4", "R4, (R5)", "(R4), R5",
|
||||||
|
"F0, F1, F2", "F0, F1", "p2", "R1, p2", "R4, p2",
|
||||||
|
"$1, R4, R5, R6", "$65536, R4", "R4, R5, p2", "p2(SB)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gasmEncodable reports whether the gasm encoder for a can emit the
|
||||||
|
// mnemonic, decided by trial assembly over the shape battery.
|
||||||
|
func gasmEncodable(a arch.Arch, name string) bool {
|
||||||
|
switch a {
|
||||||
|
case arch.ARM64, arch.RISCV, arch.LOONG64:
|
||||||
|
default:
|
||||||
|
return asm.Encodable(name)
|
||||||
|
}
|
||||||
|
for _, shape := range probeShapes(a) {
|
||||||
|
if gasmAssembles(a, name, shape) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// gasmAssembles reports whether a one-instruction probe file containing name
|
||||||
|
// with the given operand shape assembles without error.
|
||||||
|
func gasmAssembles(a arch.Arch, name, shape string) bool {
|
||||||
|
src := "TEXT ·p(SB), NOSPLIT, $0\n\t" + name
|
||||||
|
if shape != "" {
|
||||||
|
src += " " + shape
|
||||||
|
}
|
||||||
|
src += "\n\tRET\np2:\n\tRET\n"
|
||||||
|
f, errs := parser.Parse("probe.s", src)
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
switch a {
|
||||||
|
case arch.ARM64:
|
||||||
|
_, err = asm.AssembleFileARM64(f)
|
||||||
|
case arch.RISCV:
|
||||||
|
_, err = asm.AssembleFileRISCV(f)
|
||||||
|
case arch.LOONG64:
|
||||||
|
_, err = asm.AssembleFileLOONG64(f)
|
||||||
|
}
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
// sanitize makes a mnemonic safe for use in a Go symbol name.
|
// sanitize makes a mnemonic safe for use in a Go symbol name.
|
||||||
func sanitize(name string) string {
|
func sanitize(name string) string {
|
||||||
return strings.NewReplacer(".", "_", "$", "_").Replace(name)
|
return strings.NewReplacer(".", "_", "$", "_").Replace(name)
|
||||||
|
|||||||
+42
-1
@@ -3,7 +3,11 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
|
)
|
||||||
|
|
||||||
func TestDerivedFamily(t *testing.T) {
|
func TestDerivedFamily(t *testing.T) {
|
||||||
for _, n := range []string{"JEQ", "JLT", "JCC", "CMOVLGT", "SETNE", "SETA"} {
|
for _, n := range []string{"JEQ", "JLT", "JCC", "CMOVLGT", "SETNE", "SETA"} {
|
||||||
@@ -23,3 +27,40 @@ func TestSanitize(t *testing.T) {
|
|||||||
t.Errorf("sanitize: got %q", got)
|
t.Errorf("sanitize: got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuditArch(t *testing.T) {
|
||||||
|
for in, want := range map[string]arch.Arch{
|
||||||
|
"amd64": arch.AMD64, "arm64": arch.ARM64,
|
||||||
|
"riscv64": arch.RISCV, "riscv": arch.RISCV,
|
||||||
|
"loong64": arch.LOONG64, "LOONG": arch.LOONG64,
|
||||||
|
} {
|
||||||
|
got, err := auditArch(in)
|
||||||
|
if err != nil || got != want {
|
||||||
|
t.Errorf("auditArch(%q) = %v, %v; want %v", in, got, err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := auditArch("mips"); err == nil {
|
||||||
|
t.Error("auditArch(mips) must fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGasmEncodable(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
a arch.Arch
|
||||||
|
yes string
|
||||||
|
no string
|
||||||
|
}{
|
||||||
|
{arch.AMD64, "ADDQ", "NOSUCHMNEMONIC"},
|
||||||
|
{arch.ARM64, "ADD", "NOSUCHMNEMONIC"},
|
||||||
|
{arch.RISCV, "ADD", "NOSUCHMNEMONIC"},
|
||||||
|
{arch.LOONG64, "ADDV", "NOSUCHMNEMONIC"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
if !gasmEncodable(c.a, c.yes) {
|
||||||
|
t.Errorf("%s: %s should be encodable", c.a, c.yes)
|
||||||
|
}
|
||||||
|
if gasmEncodable(c.a, c.no) {
|
||||||
|
t.Errorf("%s: %s should not be encodable", c.a, c.no)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+11
-7
@@ -154,14 +154,18 @@ each function's labels, their offsets, and the block boundaries. This is
|
|||||||
the static structure; for runtime execution counts, use `gasm verify
|
the static structure; for runtime execution counts, use `gasm verify
|
||||||
--fuzz` which exercises the code paths.
|
--fuzz` which exercises the code paths.
|
||||||
|
|
||||||
## `gasm audit-instructions`
|
## `gasm audit-instructions [amd64|arm64|riscv64|loong64]`
|
||||||
|
|
||||||
Compare the gasm amd64 encoder against the installed `go tool asm` and
|
Compare the gasm encoder for the given architecture (default amd64)
|
||||||
print the diff: superset encodings (gasm-only spellings, shippable via
|
against the installed `go tool asm` and print the diff: superset
|
||||||
`gasm asm --format goobj`), known-but-unencodable names (the encoder
|
encodings (gasm-only spellings, shippable via `gasm asm --format goobj`),
|
||||||
backlog) and go-only names (feature gaps). The Go side is probed
|
known-but-unencodable names (the encoder backlog) and go-only names
|
||||||
black-box with a battery of operand shapes per mnemonic, so the audit
|
(feature gaps). The Go side is probed black-box with a battery of operand
|
||||||
tracks whatever toolchain `go env GOROOT` provides.
|
shapes per mnemonic, so the audit tracks whatever toolchain
|
||||||
|
`go env GOROOT` provides. On non-amd64 architectures the backlog is an
|
||||||
|
over-approximation: a name counts as encodable only when a probe shape
|
||||||
|
assembles cleanly, so a name whose real forms the battery misses lands
|
||||||
|
in the backlog.
|
||||||
|
|
||||||
## `gasm scaffold differential <file.s>`
|
## `gasm scaffold differential <file.s>`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user