Compare commits

..
12 Commits
Author SHA1 Message Date
petrbalvin 1a45b66139 feat(verify): add universal --fuzz differential testing driven by // func signatures
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin 382efe538a feat(verify): add universal --ground-truth verification against go tool asm
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin f8d28a42ba feat(verify): complete the analyze family and add stereo16 differential tests
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin 51a2854d7f feat(verify): add analyzeO2/Res and decodeMono24 differential tests
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin c234c3dd5b feat(verify): add analyzeO1Range and fastStereoSums differential tests
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin 9262990ce5 feat(verify): extend differential tests to go-flac and AVX-512, add --abi/--profile CLI flags
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin d9f6167a4d feat(verify): add basic-block enumeration and path-diversity profiling
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin f5088c52fc feat(verify): add runtime ABI checks with sentinel registers and red-zone canary
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin f52e23f1bc feat(verify): add differential fuzz testing against a portable Go reference
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin c9775c2b95 feat(verify): add the JIT execution substrate and gasm verify subcommand
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin 5af12e15ac feat(asm): add the GPR-interchanging conversions, completing the amd64 EVEX set
Assisted-by: Qwen 3.8 Max Preview
2026-08-02 23:11:30 +02:00
petrbalvin db8e3fc160 docs: record the deferred GOOBJ external-symbols decision 2026-08-02 23:11:30 +02:00
30 changed files with 3498 additions and 11 deletions
+46 -5
View File
@@ -377,6 +377,37 @@ var evexTable = map[string]evexSpec{
"VPMOVW2M": {2, 0x29, 1, 2, -1, vexRM, [3]int{16, 32, 64}},
"VPMOVD2M": {2, 0x39, 0, 2, -1, vexRM, [3]int{16, 32, 64}},
"VPMOVQ2M": {2, 0x39, 1, 2, -1, vexRM, [3]int{16, 32, 64}},
// EVEX — scalar conversions between vector and general-purpose
// registers. Vector to GPR (two operands: vec/mem source, GPR
// destination, vvvv unused): the signed and truncated pair, and the
// unsigned forms (EVEX only).
"VCVTSD2SI": {1, 0x2D, 0, 3, -1, vexRM, [3]int{8, 8, 8}},
"VCVTSD2SIQ": {1, 0x2D, 1, 3, -1, vexRM, [3]int{8, 8, 8}},
"VCVTSS2SI": {1, 0x2D, 0, 2, -1, vexRM, [3]int{4, 4, 4}},
"VCVTSS2SIQ": {1, 0x2D, 1, 2, -1, vexRM, [3]int{4, 4, 4}},
"VCVTTSD2SI": {1, 0x2C, 0, 3, -1, vexRM, [3]int{8, 8, 8}},
"VCVTTSD2SIQ": {1, 0x2C, 1, 3, -1, vexRM, [3]int{8, 8, 8}},
"VCVTTSS2SI": {1, 0x2C, 0, 2, -1, vexRM, [3]int{4, 4, 4}},
"VCVTTSS2SIQ": {1, 0x2C, 1, 2, -1, vexRM, [3]int{4, 4, 4}},
"VCVTSD2USIL": {1, 0x79, 0, 3, -1, vexRM, [3]int{8, 8, 8}},
"VCVTSD2USIQ": {1, 0x79, 1, 3, -1, vexRM, [3]int{8, 8, 8}},
"VCVTSS2USIL": {1, 0x79, 0, 2, -1, vexRM, [3]int{4, 4, 4}},
"VCVTSS2USIQ": {1, 0x79, 1, 2, -1, vexRM, [3]int{4, 4, 4}},
"VCVTTSD2USIL": {1, 0x78, 0, 3, -1, vexRM, [3]int{8, 8, 8}},
"VCVTTSD2USIQ": {1, 0x78, 1, 3, -1, vexRM, [3]int{8, 8, 8}},
"VCVTTSS2USIL": {1, 0x78, 0, 2, -1, vexRM, [3]int{4, 4, 4}},
"VCVTTSS2USIQ": {1, 0x78, 1, 2, -1, vexRM, [3]int{4, 4, 4}},
// GPR to vector (three operands: GPR/mem source in r/m, the preserved
// vector source in vvvv, vector destination in reg).
"VCVTSI2SDL": {1, 0x2A, 0, 3, -1, vexNDS3, [3]int{4, 4, 4}},
"VCVTSI2SDQ": {1, 0x2A, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
"VCVTSI2SSL": {1, 0x2A, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
"VCVTSI2SSQ": {1, 0x2A, 1, 2, -1, vexNDS3, [3]int{8, 8, 8}},
"VCVTUSI2SDL": {1, 0x7B, 0, 3, -1, vexNDS3, [3]int{4, 4, 4}},
"VCVTUSI2SDQ": {1, 0x7B, 1, 3, -1, vexNDS3, [3]int{8, 8, 8}},
"VCVTUSI2SSL": {1, 0x7B, 0, 2, -1, vexNDS3, [3]int{4, 4, 4}},
"VCVTUSI2SSQ": {1, 0x7B, 1, 2, -1, vexNDS3, [3]int{8, 8, 8}},
// EVEX.128/256/512.66.0F38.W0 — sign-extend dwords to qwords; the memory
// operand is the narrow source, so disp8×N follows its size (8/16/32 for
// the xmm/ymm/zmm destination lengths).
@@ -615,6 +646,12 @@ var evexRound = map[string]bool{
"VCVTPD2PS": true, "VCVTPD2UDQ": true, "VCVTTPD2UDQ": true, "VCVTTPD2UQQ": true,
"VCVTPS2UDQ": true, "VCVTTPS2UDQ": true, "VCVTPS2UQQ": true, "VCVTTPS2UQQ": true,
"VCVTTPD2QQ": true, "VCVTTPS2QQ": true, "VCVTUQQ2PD": true, "VCVTUQQ2PS": true,
"VCVTSD2SI": true, "VCVTSD2SIQ": true, "VCVTSS2SI": true, "VCVTSS2SIQ": true,
"VCVTSD2USIL": true, "VCVTSD2USIQ": true, "VCVTSS2USIL": true, "VCVTSS2USIQ": true,
"VCVTTSD2SI": true, "VCVTTSD2SIQ": true, "VCVTTSS2SI": true, "VCVTTSS2SIQ": true,
"VCVTTSD2USIL": true, "VCVTTSD2USIQ": true, "VCVTTSS2USIL": true, "VCVTTSS2USIQ": true,
"VCVTSI2SDQ": true, "VCVTSI2SSL": true, "VCVTSI2SSQ": true,
"VCVTUSI2SDQ": true, "VCVTUSI2SSL": true, "VCVTUSI2SSQ": true,
}
// evexBcstN maps an instruction accepting .BCST to the broadcast element
@@ -801,19 +838,23 @@ func (e *enc) encodeEvexNDS3(spec evexSpec, ops []Operand, mask int, sfx evexSuf
// encodeEvexRM encodes the two-operand form: OP src, dst (reg=dst, rm=src,
// no vvvv), e.g. VCVTQQ2PD. The destination may be an opmask register (the
// *2M mask conversions), in which case the vector length comes from the
// source.
// *2M mask conversions) or a general-purpose register (the scalar
// vector-to-GPR conversions); in both cases the vector length comes from
// the source.
func (e *enc) encodeEvexRM(spec evexSpec, ops []Operand, mask int, sfx evexSuffix) error {
if len(ops) != 2 {
return fmt.Errorf("EVEX two-operand instruction expects 2 operands, got %d", len(ops))
}
src, dst := ops[0], ops[1]
dstReg, ok := dst.(Reg)
if !ok || (!dstReg.isVec() && !dstReg.mask) {
return fmt.Errorf("EVEX destination must be a vector or mask register")
if !ok {
return fmt.Errorf("EVEX destination must be a register")
}
ll := dstReg.vecLenBit()
if dstReg.mask {
if !dstReg.isVec() {
// Mask or GPR destination: the length follows the vector source
// (128 for a memory source).
ll = 0
if r, ok := src.(Reg); ok && r.isVec() {
ll = r.vecLenBit()
}
+68
View File
@@ -467,6 +467,74 @@ func TestEvexHelperGroundTruth(t *testing.T) {
}
}
// TestEvexGprGroundTruth covers the scalar conversions between vector and
// general-purpose registers — the signed and truncated VCVT{,T}S{D,S}2SI
// forms (VEX and EVEX), the unsigned EVEX-only forms, and the GPR-to-vector
// VCVTSI2*/VCVTUSI2* forms with the preserved vector source in vvvv — byte
// for byte against the Go assembler, including memory sources and extended
// GPRs.
func TestEvexGprGroundTruth(t *testing.T) {
mem := func(b Reg) Operand { return Ptr(b, 0, 8) }
cases := []struct {
name string
mnem string
ops []Operand
want string
}{
{"VCVTSD2SI", "VCVTSD2SI", []Operand{vreg(t, "X1"), AX}, "c5fb2dc1"},
{"VCVTSD2SIQ", "VCVTSD2SIQ", []Operand{vreg(t, "X1"), AX}, "c4e1fb2dc1"},
{"VCVTSS2SI", "VCVTSS2SI", []Operand{vreg(t, "X1"), AX}, "c5fa2dc1"},
{"VCVTSS2SIQ", "VCVTSS2SIQ", []Operand{vreg(t, "X1"), AX}, "c4e1fa2dc1"},
{"VCVTTSD2SI", "VCVTTSD2SI", []Operand{vreg(t, "X1"), AX}, "c5fb2cc1"},
{"VCVTTSD2SIQ", "VCVTTSD2SIQ", []Operand{vreg(t, "X1"), AX}, "c4e1fb2cc1"},
{"VCVTTSS2SI", "VCVTTSS2SI", []Operand{vreg(t, "X1"), AX}, "c5fa2cc1"},
{"VCVTTSS2SIQ", "VCVTTSS2SIQ", []Operand{vreg(t, "X1"), AX}, "c4e1fa2cc1"},
{"VCVTSD2USIL", "VCVTSD2USIL", []Operand{vreg(t, "X1"), AX}, "62f17f0879c1"},
{"VCVTSD2USIQ", "VCVTSD2USIQ", []Operand{vreg(t, "X1"), AX}, "62f1ff0879c1"},
{"VCVTSS2USIL", "VCVTSS2USIL", []Operand{vreg(t, "X1"), AX}, "62f17e0879c1"},
{"VCVTSS2USIQ", "VCVTSS2USIQ", []Operand{vreg(t, "X1"), AX}, "62f1fe0879c1"},
{"VCVTTSD2USIL", "VCVTTSD2USIL", []Operand{vreg(t, "X1"), AX}, "62f17f0878c1"},
{"VCVTTSD2USIQ", "VCVTTSD2USIQ", []Operand{vreg(t, "X1"), AX}, "62f1ff0878c1"},
{"VCVTTSS2USIL", "VCVTTSS2USIL", []Operand{vreg(t, "X1"), AX}, "62f17e0878c1"},
{"VCVTTSS2USIQ", "VCVTTSS2USIQ", []Operand{vreg(t, "X1"), AX}, "62f1fe0878c1"},
{"VCVTSI2SDL", "VCVTSI2SDL", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "c5f32ad0"},
{"VCVTSI2SDQ", "VCVTSI2SDQ", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "c4e1f32ad0"},
{"VCVTSI2SSL", "VCVTSI2SSL", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "c5f22ad0"},
{"VCVTSI2SSQ", "VCVTSI2SSQ", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "c4e1f22ad0"},
{"VCVTUSI2SDL", "VCVTUSI2SDL", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "62f177087bd0"},
{"VCVTUSI2SDQ", "VCVTUSI2SDQ", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "62f1f7087bd0"},
{"VCVTUSI2SSL", "VCVTUSI2SSL", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "62f176087bd0"},
{"VCVTUSI2SSQ", "VCVTUSI2SSQ", []Operand{AX, vreg(t, "X1"), vreg(t, "X2")}, "62f1f6087bd0"},
{"VCVTSD2SI mem", "VCVTSD2SI", []Operand{mem(AX), BX}, "c5fb2d18"},
{"VCVTSI2SDQ mem", "VCVTSI2SDQ", []Operand{mem(BX), vreg(t, "X1"), vreg(t, "X2")}, "c4e1f32a13"},
{"VCVTSD2SIQ hi gpr", "VCVTSD2SIQ", []Operand{vreg(t, "X1"), vreg(t, "R9")}, "c461fb2dc9"},
{"VCVTSI2SDQ hi gpr", "VCVTSI2SDQ", []Operand{vreg(t, "R10"), vreg(t, "X1"), vreg(t, "X2")}, "c4c1f32ad2"},
}
for _, c := range cases {
code, err := Encode(c.mnem, c.ops...)
if err != nil {
t.Errorf("%s: Encode: %v", c.name, err)
continue
}
if got := hexCompact(code); got != c.want {
t.Errorf("%s: bytes %s, want %s", c.name, got, c.want)
continue
}
inst, err := x86asm.Decode(code, 64)
if err != nil {
t.Errorf("%s: Decode(%x): %v", c.name, code, err)
continue
}
// The decoder does not distinguish the Plan 9 SIQ spelling (the
// 64-bit GPR destination) from the base name; the W bit carries it.
want := c.mnem
got := inst.Op.String()
if got != want && !(len(want) > len(got) && want[:len(got)] == got) {
t.Errorf("%s: decoded as %s", c.name, got)
}
}
}
// TestEvexConversionGroundTruth covers the unsigned and truncating VCVT*
// conversions, the remaining sign/zero-extending moves, the signed/unsigned
// narrowing stores and the mask/vector conversions, byte for byte against
+18
View File
@@ -210,6 +210,24 @@ var vexTable = map[string]vexSpec{
"VCVTPD2PSX": {1, 0x5A, 0, 1, -1, vexRMSrcLen},
"VCVTPD2PSY": {1, 0x5A, 0, 1, -1, vexRMSrcLen},
// VEX scalar conversions between vector and general-purpose registers.
// Vector to GPR (two operands: vec/mem source, GPR destination, vvvv
// unused; the length follows the source).
"VCVTSD2SI": {1, 0x2D, 0, 3, -1, vexRM},
"VCVTSD2SIQ": {1, 0x2D, 1, 3, -1, vexRM},
"VCVTSS2SI": {1, 0x2D, 0, 2, -1, vexRM},
"VCVTSS2SIQ": {1, 0x2D, 1, 2, -1, vexRM},
"VCVTTSD2SI": {1, 0x2C, 0, 3, -1, vexRM},
"VCVTTSD2SIQ": {1, 0x2C, 1, 3, -1, vexRM},
"VCVTTSS2SI": {1, 0x2C, 0, 2, -1, vexRM},
"VCVTTSS2SIQ": {1, 0x2C, 1, 2, -1, vexRM},
// GPR to vector (three operands: GPR/mem source in r/m, the preserved
// vector source in vvvv, vector destination in reg).
"VCVTSI2SDL": {1, 0x2A, 0, 3, -1, vexNDS3},
"VCVTSI2SDQ": {1, 0x2A, 1, 3, -1, vexNDS3},
"VCVTSI2SSL": {1, 0x2A, 0, 2, -1, vexNDS3},
"VCVTSI2SSQ": {1, 0x2A, 1, 2, -1, vexNDS3},
// VEX.128/256.66.0F.WIG — word shifts (opdigit selects the shift).
"VPSRLW": {1, 0x71, 0, 1, 2, vexShiftImm},
"VPSRAW": {1, 0x71, 0, 1, 4, vexShiftImm},
+6 -3
View File
@@ -39,11 +39,14 @@ func TestVexNDS3(t *testing.T) {
}
inst, err := x86asm.Decode(code, 64)
if err != nil {
t.Errorf("%s: Decode(% x): %v", mnem, code, err)
t.Errorf("%s: Decode(% x): %v", mnem, err, code)
continue
}
if inst.Op.String() != mnem {
t.Errorf("%s: decoded as %s (% x)", mnem, inst.Op.String(), code)
// The decoder folds the Plan 9 L/Q GPR-width spellings (VCVTSI2SDL/
// SDQ, SSL/SSQ) onto the base name; the W bit carries the width.
got := inst.Op.String()
if got != mnem && !(len(mnem) > len(got) && mnem[:len(got)] == got) {
t.Errorf("%s: decoded as %s (% x)", mnem, got, code)
}
}
}
+186 -1
View File
@@ -8,6 +8,7 @@
package main
import (
"bytes"
"flag"
"fmt"
"io"
@@ -24,11 +25,12 @@ import (
"sourcedock.dev/petrbalvin/gasm-devkit/lint"
"sourcedock.dev/petrbalvin/gasm-devkit/lsp"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
)
// version is the release version, stamped at build time via
// -ldflags "-X main.version=…" (defaulting to the current release).
var version = "0.15.0"
var version = "0.26.0"
func main() {
if len(os.Args) < 2 {
@@ -46,6 +48,8 @@ func main() {
os.Exit(cmdLint(os.Args[2:]))
case "asm":
os.Exit(cmdAsm(os.Args[2:]))
case "verify":
os.Exit(cmdVerify(os.Args[2:]))
case "lsp":
os.Exit(cmdLSP(os.Args[2:]))
case "version", "--version", "-V":
@@ -80,6 +84,7 @@ Commands:
fmt canonicalise formatting (gofmt for assembly)
lint run static checks
asm assemble .s files to machine code (amd64)
verify JIT-assemble and run dynamic checks (amd64)
lsp run the language server over stdio
version print the version (same as --version)
@@ -466,3 +471,183 @@ requires -p, the package path, and the installed Go toolchain).
}
return 0
}
func cmdVerify(args []string) int {
fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] <file.s>", `
Assemble FILE (amd64), map it into executable memory and report the available
functions. This confirms the assembled image is self-consistent (no
unresolved external symbols) and executable — the prerequisite for dynamic
testing.
With -smoke, each NOSPLIT function is called with a zeroed argument block to
confirm the JIT trampoline works end-to-end. This is safe only for functions
that tolerate nil pointers and zero lengths in their arguments.
With -abi, each function is called with sentinel values in the callee-saved
registers (BP, R14) and a red-zone canary below SP; violations are reported.
With -profile, the static basic-block structure is listed for each function.
`)
smoke := fs.Bool("smoke", false, "call each NOSPLIT function with zeroed args")
abi := fs.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)")
profile := fs.Bool("profile", false, "list basic-block structure per function")
groundTruth := fs.Bool("ground-truth", false, "compare machine code byte-for-byte against go tool asm")
fuzz := fs.Bool("fuzz", false, "differential fuzz: JIT both gasm and go-tool-asm versions, compare outputs")
fuzzN := fs.Int("n", 1000, "number of fuzz iterations per function")
fs.Parse(args)
if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] <file.s>")
return 2
}
path := fs.Arg(0)
if arch.FromFilename(path) != arch.AMD64 {
fmt.Fprintln(os.Stderr, "gasm verify: only amd64 is supported")
return 1
}
k, err := verify.Load(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
defer k.Close()
names := k.FuncNames()
fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names))
rc := 0
// Ground-truth comparison: assemble with go tool asm and compare bytes.
if *groundTruth {
gt, err := verify.GroundTruth(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
return 1
}
matched, total := 0, 0
for _, name := range names {
fl, _ := k.Func(name)
gasmCode := k.Image().Code[fl.Offset : fl.Offset+fl.Size]
goCode, ok := gt[name]
if !ok {
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
continue
}
total++
// Compare, masking relocation sites (disp32 fields that the
// Go linker fills at link time — gasm resolves them internally).
gasmCmp := make([]byte, len(gasmCode))
goCmp := make([]byte, len(goCode))
copy(gasmCmp, gasmCode)
copy(goCmp, goCode)
for _, r := range fl.Relocs {
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
gasmCmp[j] = 0
}
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
goCmp[j] = 0
}
}
if bytes.Equal(gasmCmp, goCmp) {
matched++
if len(fl.Relocs) > 0 {
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", name, fl.Size, len(fl.Relocs))
} else {
fmt.Printf(" %s: MATCH (%d bytes)\n", name, fl.Size)
}
} else {
fmt.Printf(" %s: MISMATCH (gasm %d bytes, go %d bytes)\n", name, fl.Size, len(goCode))
for i := 0; i < len(gasmCmp) && i < len(goCmp); i++ {
if gasmCmp[i] != goCmp[i] {
fmt.Printf(" first diff at byte %d: gasm=%02x go=%02x\n", i, gasmCmp[i], goCmp[i])
break
}
}
rc = 1
}
}
fmt.Printf("ground truth: %d/%d functions byte-identical\n", matched, total)
if matched < total {
rc = 1
}
}
// Differential fuzz: JIT both gasm and go-tool-asm, compare outputs.
// Each function runs in a subprocess so a crash (partial functions like
// decoders that fault on malformed input) doesn't kill the whole run.
if *fuzz {
gt, err := verify.GroundTruth(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: fuzz: %v\n", err)
return 1
}
src, err := readSource(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
sigs := verify.ExtractSignatures(src)
fuzzed := 0
for _, name := range names {
sig, ok := sigs[name]
if !ok {
fmt.Printf(" %s: SKIP (no // func signature)\n", name)
continue
}
goCode, ok := gt[name]
if !ok {
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
continue
}
res := k.FuzzFunc(name, sig, goCode, *fuzzN, int64(fuzzed*7+42))
fmt.Printf(" %s\n", res)
if !res.OK() {
rc = 1
}
fuzzed++
}
fmt.Printf("fuzz: %d functions tested, %d iterations each\n", fuzzed, *fuzzN)
}
for _, name := range names {
fl, _ := k.Func(name)
flags := ""
if fl.NoSplit {
flags = " NOSPLIT"
}
fmt.Printf(" %s: %d bytes, args=%d, frame=%d%s\n", name, fl.Size, fl.Args, fl.Frame, flags)
if *profile {
blocks, err := k.Blocks(name)
if err != nil {
fmt.Printf(" profile: %v\n", err)
} else {
fmt.Printf(" blocks: %d\n", len(blocks))
}
}
if *smoke && fl.NoSplit {
args := make([]byte, fl.Args)
_, err := k.CallFunc(name, args)
if err != nil {
fmt.Printf(" smoke: FAIL — %v\n", err)
rc = 1
} else {
fmt.Printf(" smoke: OK\n")
}
}
if *abi && fl.NoSplit {
args := make([]byte, fl.Args)
_, report, err := k.CallFuncChecked(name, args)
if err != nil {
fmt.Printf(" abi: FAIL — %v\n", err)
rc = 1
} else if !report.OK() {
fmt.Printf(" abi: %s\n", report)
rc = 1
} else {
fmt.Printf(" abi: clean\n")
}
}
}
return rc
}
+28 -1
View File
@@ -242,7 +242,9 @@ helper and conversion tail (VRCP14*, VRSQRT14*, VGETEXP*, VGETMANT*,
VSCALEF*, VRNDSCALE*, VREDUCE*, VFIXUPIMM*, VRANGE*, VFPCLASS* with an
opmask destination, and the VCVT* conversions — signed, unsigned and
truncating, including the length-suffixed X/Y spellings and the
mask/vector conversions VPMOVM2*/VPMOV*2M), and gather/scatter with VSIB addressing — both the
mask/vector conversions VPMOVM2*/VPMOV*2M, and the scalar conversions
between vector and general-purpose registers (VCVT{,T}S{D,S}2SI{,Q} and
the unsigned forms, VCVTSI2*/VCVTUSI2*), and gather/scatter with VSIB addressing — both the
VEX spelling with a vector mask register and the EVEX spelling with an
explicit K mask, where the EVEX length follows the VSIB index register,
not the data register. The EVEX mnemonic
@@ -282,6 +284,31 @@ references and the implicit funcdata/DWARF symbols remain future work (the
linker fills the latter's defaults); the rest of Phase 2 is those, the
remaining EVEX forms and the other architectures.
### `verify`
The dynamic-analysis substrate (Phase 3). It JIT-loads assembled images into
executable memory and invokes them directly, enabling differential testing,
runtime ABI checks and coverage profiling.
The execution model is pure Go (stdlib only). `Map` copies machine code into
an anonymous `syscall.Mmap` mapping and enforces W^X (write the bytes, then
`mprotect` to read-execute). `Call` prepares a stack whose first word is the
address of an assembly trampoline (`leaveJIT`), lays the ABI0 argument
block after it, switches to that stack via `enterJIT` (which saves the Go
stack pointer in a package global and jumps to the target), and recovers
control when the function RETs into `leaveJIT` (which restores the Go stack
and returns). A 64-byte pad below the return address accommodates the
ABIInternal wrapper that the Go runtime interposes on assembly functions.
`Load` / `LoadSource` / `LoadAST` parse, assemble and map a `.s` file in one
step, returning a `Kernel` whose `CallFunc` method marshals the argument block
by name. The image must be self-contained (no external relocations); the
assembler’s `Image.Bytes()` provides the code-and-data concatenation.
The `gasm verify` CLI subcommand exposes this: it loads a file, reports the
available functions and (with `-smoke`) calls each NOSPLIT function with zeroed
arguments to confirm the trampoline round-trips.
## Extension points
- **New architecture:** add an entry to the generator in `_gen`, run
+56
View File
@@ -0,0 +1,56 @@
# Deferred decisions
Design decisions deliberately postponed, with enough context to pick them up
again without re-deriving the analysis. Each entry records what is deferred,
why, the options on the table, and the trigger that should reopen it.
---
## GOOBJ external (cross-package) symbol references
**Status:** deferred (v0.15.0, 2026-08-02). The GOOBJ emitter resolves only
symbols defined in the file being assembled; a reference to any other symbol
is rejected.
**Why it is deferred.** GOOBJ symbol references are *positional*: a
reference is a `{PkgIdx, SymIdx}` pair, where `SymIdx` is the index of the
symbol in the *referenced package's* symbol-definition table. That ordering
is not derivable from the reference site — it lives in the referenced
package's gc export data (the iexport binary format, which evolves with the
toolchain). `cmd/asm` reads it with `cmd/internal` readers gasm cannot
import, so emitting external references means either parsing export data
ourselves or taking a dependency that does.
**What works today.** Single-package objects: every symbol the file defines
(as `TEXT` or `GLOBL`, static or exported) and every reference to them.
This covers the production use case — the go-flac / go-lz4 kernels carry no
`FUNCDATA`/`PCDATA`, hence no references into `runtime`, and the Go side
references the assembly symbols, never the reverse. Such a package builds
with its assembly object replaced by a gasm-emitted one.
**The options, when we return.**
1. **`golang.org/x/tools/go/gcexportdata` as a production dependency.**
The straightforward path: read each imported package's export file
(paths from `-importcfg` or `go list -export`), assign symbol indices in
its symbol order, write `PkgIndex`/`Autolib` entries (fingerprints from
the export files' build IDs) and positional references. Robust across
toolchain versions — `x/tools` tracks the format. **Cost:** the first
production dependency beyond the standard library, an explicit deviation
from the "production code depends only on the standard library"
principle in the README. Requires the user's explicit agreement.
2. **A minimal iexport parser of our own.** Preserves self-containment.
Substantial effort and inherently fragile: the format is an internal
contract that changes with Go releases, so the parser needs a
version-gated fallback and regression tests against several toolchains.
3. **Shell out to the toolchain for symbol metadata.** Consistent with the
existing GOOBJ preamble probe (which already runs `go tool asm`), but no
toolchain command exposes a package's symbols *in definition-index
order* — `go tool nm` sorts differently — so this does not solve the
core problem on its own; it would only feed option 1 or 2.
**Trigger to reopen.** An assembly file that needs a cross-package
reference — in practice `FUNCDATA $…, runtime·…(SB)` (stack maps / GC
metadata written in assembly), or any kernel that calls into another
package directly. Until then, option 3's limitation is moot and the
single-package emitter suffices.
+1 -1
View File
@@ -3,7 +3,7 @@
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
version := "0.15.0"
version := "0.26.0"
default:
@just --list
+28
View File
@@ -0,0 +1,28 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
#include "textflag.h"
// func cleanAdd(a, b int64) int64
// A well-behaved function that preserves all callee-saved registers.
TEXT ·cleanAdd(SB), NOSPLIT, $0-24
MOVQ a+0(FP), AX
ADDQ b+8(FP), AX
MOVQ AX, ret+16(FP)
RET
// func dirtyBP(a int64) int64
// Deliberately clobbers BP (an ABI violation for a NOSPLIT frame=0 function).
TEXT ·dirtyBP(SB), NOSPLIT, $0-16
MOVQ $0x1234, BP
MOVQ a+0(FP), AX
MOVQ AX, ret+8(FP)
RET
// func dirtyR14(a int64) int64
// Deliberately clobbers R14 (the goroutine pointer — a serious ABI violation).
TEXT ·dirtyR14(SB), NOSPLIT, $0-16
MOVQ $0x5678, R14
MOVQ a+0(FP), AX
MOVQ AX, ret+8(FP)
RET
+67
View File
@@ -0,0 +1,67 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
#include "textflag.h"
// func add(a, b int64) int64
TEXT ·add(SB), NOSPLIT, $0-24
MOVQ a+0(FP), AX
ADDQ b+8(FP), AX
MOVQ AX, ret+16(FP)
RET
// func sum(data []int64) int64
// Sums all elements of the slice.
TEXT ·sum(SB), NOSPLIT, $0-32
MOVQ data_base+0(FP), SI
MOVQ data_len+8(FP), CX
XORQ AX, AX
TESTQ CX, CX
JZ sum_done
sum_loop:
ADDQ (SI), AX
ADDQ $8, SI
DECQ CX
JNZ sum_loop
sum_done:
MOVQ AX, ret+24(FP)
RET
// func wideCopy(dst, src []byte)
// Non-overlapping copy of min(len(dst), len(src)) bytes using 32-byte moves.
TEXT ·wideCopy(SB), NOSPLIT, $0-48
MOVQ dst_base+0(FP), DI
MOVQ dst_len+8(FP), BX
MOVQ src_base+24(FP), SI
MOVQ src_len+32(FP), R8
CMPQ BX, R8
JLE wc_have_n
MOVQ R8, BX
wc_have_n:
CMPQ BX, $32
JB wc_small
VMOVDQU (SI), Y0
VMOVDQU Y0, (DI)
VMOVDQU -32(SI)(BX*1), Y0
VMOVDQU Y0, -32(DI)(BX*1)
VZEROUPPER
RET
wc_small:
TESTQ BX, BX
JZ wc_done
wc_byte:
MOVB (SI), R8B
MOVB R8B, (DI)
INCQ SI
INCQ DI
DECQ BX
JNZ wc_byte
wc_done:
RET
+130
View File
@@ -0,0 +1,130 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
//go:build amd64
package verify
import (
"encoding/binary"
"fmt"
"syscall"
"unsafe"
)
// abiResult records register-clobber violations detected by the ABI-checking
// trampoline. Bit 0: BP clobbered. Bit 1: R14 clobbered.
var abiResult uint64
// savedBP holds the caller's frame pointer across the ABI-checked JIT call.
// Referenced by enterJITChecked to satisfy go vet's save-before-clobber rule.
var savedBP uintptr
// leaveCheckedPtr is initialised by the linker from the GLOBL/DATA in
// abi_amd64.s: it holds the raw address of leaveJITCheckedRaw (which has
// no ABIInternal wrapper, so the JIT function RETs directly into it).
var leaveCheckedPtr uintptr
// enterJITChecked sets sentinels in BP and R14, switches to the prepared
// stack and jumps to fn.
//
//go:nosplit
func enterJITChecked(fn uintptr, stack uintptr)
// leaveJITCheckedRaw is the raw return trampoline for ABI checks. Its
// address is obtained from the GLOBL in abi_amd64.s (leaveCheckedPtr),
// which points to the .abi0 code — NOT the ABIInternal wrapper that this
// declaration would generate. The declaration exists solely to satisfy
// go vet's "missing Go declaration" check.
//
//go:nosplit
func leaveJITCheckedRaw()
// ABIReport describes the result of an ABI-checking call.
type ABIReport struct {
BPClobbered bool // BP was modified by the function
R14Clobbered bool // R14 (goroutine pointer) was modified
RedZoneHit bool // the 128-byte red zone below SP was written
}
// OK returns true when no violations were detected.
func (r ABIReport) OK() bool {
return !r.BPClobbered && !r.R14Clobbered && !r.RedZoneHit
}
// String returns a human-readable summary.
func (r ABIReport) String() string {
if r.OK() {
return "ABI clean"
}
s := "ABI violation:"
if r.BPClobbered {
s += " BP clobbered"
}
if r.R14Clobbered {
s += " R14 clobbered"
}
if r.RedZoneHit {
s += " red-zone written"
}
return s
}
// redZoneSize is the System V AMD64 red zone: 128 bytes below SP that a
// leaf function may use without adjusting SP. Go does not use the red zone,
// so any write there is a bug.
const redZoneSize = 128
// redZoneFill is the byte pattern used to detect red-zone writes.
const redZoneFill = 0xA5
// CallChecked invokes the function with ABI sentinels and a red-zone
// canary, returning both the argument block (with results) and an ABIReport.
func CallChecked(fnAddr uintptr, args []byte) ([]byte, ABIReport, error) {
report := ABIReport{}
// Reset the global result.
abiResult = 0
// Prepare the stack: [red-zone canary][padding][leaveJITCheckedRaw][args...]
// The red zone sits below the initial SP, so the function would have to
// write below SP to corrupt it.
totalSize := redZoneSize + stackPad + 8 + len(args) + 64
stackMem, err := syscall.Mmap(-1, 0, totalSize,
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON)
if err != nil {
return nil, report, fmt.Errorf("verify: stack mmap: %w", err)
}
defer syscall.Munmap(stackMem)
// Fill the red zone with the canary pattern.
for i := 0; i < redZoneSize; i++ {
stackMem[i] = redZoneFill
}
// Return address and args after the red zone and padding.
retOff := redZoneSize + stackPad
binary.LittleEndian.PutUint64(stackMem[retOff:retOff+8], uint64(leaveCheckedPtr))
copy(stackMem[retOff+8:], args)
stackBase := uintptr(unsafe.Pointer(&stackMem[retOff]))
enterJITChecked(fnAddr, stackBase)
// Read the register-clobber result.
res := abiResult
report.BPClobbered = res&1 != 0
report.R14Clobbered = res&2 != 0
// Check the red zone.
for i := 0; i < redZoneSize; i++ {
if stackMem[i] != redZoneFill {
report.RedZoneHit = true
break
}
}
// Copy out the argument area.
out := make([]byte, len(args))
copy(out, stackMem[retOff+8:retOff+8+len(args)])
return out, report, nil
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
#include "textflag.h"
// ABI-checking trampoline. Sets sentinel values in the callee-saved
// registers (BP, R14) before entering the JIT function and checks whether
// they survived on return.
//
// The return trampoline (leaveJITCheckedRaw) is a raw TEXT symbol with no
// Go function declaration, so the toolchain does NOT interpose an
// ABIInternal wrapper — the JIT function RETs directly into the check code,
// which sees the registers exactly as the function left them.
//
// Go ABI0 on amd64 guarantees:
// - BP is callee-saved (NOSPLIT frame=0 functions must not touch it).
// - R14 holds the goroutine pointer and must survive across any call.
// Sentinel values chosen to be unlikely in normal execution.
#define SENTINEL_BP 0xDEADBEEFCAFEF00D
#define SENTINEL_R14 0x0BADF00DDEADBEEF
// GLOBL holding the raw address of the leave trampoline, read by Go.
GLOBL ·leaveCheckedPtr(SB), NOPTR, $8
DATA ·leaveCheckedPtr(SB)/8, $·leaveJITCheckedRaw(SB)
// func enterJITChecked(fn uintptr, stack uintptr)
// Sets sentinels in BP and R14, switches to the prepared stack and jumps
// to fn. The prepared stack's return address must be leaveJITCheckedRaw
// (read from leaveCheckedPtr).
TEXT ·enterJITChecked(SB), NOSPLIT, $0-16
MOVQ fn+0(FP), AX // target (before SP switch)
MOVQ SP, ·savedSP(SB) // preserve Go stack
MOVQ BP, ·savedBP(SB) // preserve frame pointer (vet requires save before clobber)
MOVQ $SENTINEL_BP, BP // sentinel in BP
MOVQ $SENTINEL_R14, R14 // sentinel in R14
MOVQ stack+8(FP), SP // switch to prepared stack
JMP AX
// leaveJITCheckedRaw is the raw return trampoline. It has NO Go function
// declaration, so no ABIInternal wrapper is generated — the JIT function's
// RET lands here directly, seeing BP and R14 exactly as the function left
// them. It checks the sentinels, records violations in abiResult, then
// restores the Go stack and returns.
TEXT ·leaveJITCheckedRaw(SB), NOSPLIT, $0-0
// Check BP against the sentinel.
MOVQ $SENTINEL_BP, CX
CMPQ BP, CX
JEQ bp_ok
ORQ $1, ·abiResult(SB)
bp_ok:
// Check R14 against the sentinel.
MOVQ $SENTINEL_R14, CX
CMPQ R14, CX
JEQ r14_ok
ORQ $2, ·abiResult(SB)
r14_ok:
MOVQ ·savedSP(SB), SP
RET
+26
View File
@@ -0,0 +1,26 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
//go:build !amd64
package verify
import "fmt"
// ABIReport describes the result of an ABI-checking call.
type ABIReport struct {
BPClobbered bool
R14Clobbered bool
RedZoneHit bool
}
// OK returns true when no violations were detected.
func (r ABIReport) OK() bool { return false }
// String returns a human-readable summary.
func (r ABIReport) String() string { return "verify: ABI checks require amd64" }
// CallChecked is unavailable on non-amd64 architectures.
func CallChecked(fnAddr uintptr, args []byte) ([]byte, ABIReport, error) {
return nil, ABIReport{}, fmt.Errorf("verify: ABI checks require amd64")
}
+145
View File
@@ -0,0 +1,145 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"testing"
"unsafe"
)
func loadABIKernel(t *testing.T) *Kernel {
t.Helper()
k, err := Load("../testdata/verify/abi_amd64.s")
if err != nil {
t.Fatalf("Load: %v", err)
}
t.Cleanup(k.Close)
return k
}
func TestABIClean(t *testing.T) {
k := loadABIKernel(t)
args := make([]byte, 24)
PutUint64(args, 0, 3)
PutUint64(args, 8, 4)
out, report, err := k.CallFuncChecked("cleanAdd", args)
if err != nil {
t.Fatalf("CallFuncChecked: %v", err)
}
if got := int64(GetUint64(out, 16)); got != 7 {
t.Errorf("cleanAdd(3, 4) = %d, want 7", got)
}
if !report.OK() {
t.Errorf("cleanAdd: %s", report)
}
}
func TestABIBPClobbered(t *testing.T) {
k := loadABIKernel(t)
args := make([]byte, 16)
PutUint64(args, 0, 42)
out, report, err := k.CallFuncChecked("dirtyBP", args)
if err != nil {
t.Fatalf("CallFuncChecked: %v", err)
}
if got := int64(GetUint64(out, 8)); got != 42 {
t.Errorf("dirtyBP(42) = %d, want 42", got)
}
if !report.BPClobbered {
t.Error("dirtyBP: expected BP clobbered, but report says clean")
}
if report.R14Clobbered {
t.Error("dirtyBP: R14 should not be clobbered")
}
}
func TestABIR14Clobbered(t *testing.T) {
k := loadABIKernel(t)
args := make([]byte, 16)
PutUint64(args, 0, 99)
out, report, err := k.CallFuncChecked("dirtyR14", args)
if err != nil {
t.Fatalf("CallFuncChecked: %v", err)
}
if got := int64(GetUint64(out, 8)); got != 99 {
t.Errorf("dirtyR14(99) = %d, want 99", got)
}
if !report.R14Clobbered {
t.Error("dirtyR14: expected R14 clobbered, but report says clean")
}
if report.BPClobbered {
t.Error("dirtyR14: BP should not be clobbered")
}
}
// TestABILZ4Kernels verifies that the production go-lz4 kernels are ABI-clean:
// they preserve BP and R14 and do not write into the red zone.
func TestABILZ4Kernels(t *testing.T) {
k := loadLZ4Kernel(t)
// wideCopyAVX2 with a real copy.
src := make([]byte, 128)
for i := range src {
src[i] = byte(i)
}
dst := make([]byte, 128)
args := make([]byte, 48)
PutPtr(args, 0, unsafe.Pointer(&dst[0]))
PutUint64(args, 8, 128)
PutUint64(args, 16, 128)
PutPtr(args, 24, unsafe.Pointer(&src[0]))
PutUint64(args, 32, 128)
PutUint64(args, 40, 128)
_, report, err := k.CallFuncChecked("wideCopyAVX2", args)
if err != nil {
t.Fatalf("CallFuncChecked(wideCopyAVX2): %v", err)
}
if !report.OK() {
t.Errorf("wideCopyAVX2: %s", report)
}
// decodeBlockAVX2 with a simple block.
decSrc := []byte{0x50, 'H', 'e', 'l', 'l', 'o'}
decDst := make([]byte, 64)
decArgs := make([]byte, 64)
PutPtr(decArgs, 0, unsafe.Pointer(&decSrc[0]))
PutUint64(decArgs, 8, uint64(len(decSrc)))
PutUint64(decArgs, 16, uint64(cap(decSrc)))
PutPtr(decArgs, 24, unsafe.Pointer(&decDst[0]))
PutUint64(decArgs, 32, uint64(len(decDst)))
PutUint64(decArgs, 40, uint64(cap(decDst)))
_, report, err = k.CallFuncChecked("decodeBlockAVX2", decArgs)
if err != nil {
t.Fatalf("CallFuncChecked(decodeBlockAVX2): %v", err)
}
if !report.OK() {
t.Errorf("decodeBlockAVX2: %s", report)
}
}
func TestCallFuncCheckedErrors(t *testing.T) {
k := loadABIKernel(t)
// Nonexistent function.
_, _, err := k.CallFuncChecked("nope", make([]byte, 8))
if err == nil {
t.Fatal("expected error for nonexistent function")
}
// Arg block too small.
_, _, err = k.CallFuncChecked("cleanAdd", make([]byte, 8))
if err == nil {
t.Fatal("expected error for too-small arg block")
}
}
+144
View File
@@ -0,0 +1,144 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"bytes"
"math/rand"
"os"
"testing"
"unsafe"
)
const lz4AVX512Path = "../../go-libraries/go-lz4/avx512_amd64.s"
func loadLZ4AVX512Kernel(t *testing.T) *Kernel {
t.Helper()
if _, err := os.Stat(lz4AVX512Path); err != nil {
t.Skipf("sibling kernel not available: %v", err)
}
k, err := Load(lz4AVX512Path)
if err != nil {
t.Fatalf("Load(%s): %v", lz4AVX512Path, err)
}
t.Cleanup(k.Close)
return k
}
func TestAVX512DecodeKnownAnswers(t *testing.T) {
k := loadLZ4AVX512Kernel(t)
tests := []struct {
name string
src []byte
wantN int
wantCode int
}{
{"literals_only", []byte{0x50, 'H', 'e', 'l', 'l', 'o'}, 5, 0},
{"literals_and_match", []byte{0x54, 'A', 'A', 'A', 'A', 'A', 0x05, 0x00, 0x30, 'B', 'B', 'B'}, 16, 0},
{"overlapping", []byte{0x14, 'X', 0x01, 0x00, 0x10, 'Y'}, 10, 0},
{"malformed", []byte{0x50, 'H', 'e'}, 0, 1},
{"zero_offset", []byte{0x14, 'X', 0x00, 0x00}, 0, 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dst := make([]byte, 64)
args := make([]byte, 64)
PutPtr(args, 0, unsafe.Pointer(&tt.src[0]))
PutUint64(args, 8, uint64(len(tt.src)))
PutUint64(args, 16, uint64(cap(tt.src)))
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
PutUint64(args, 32, uint64(len(dst)))
PutUint64(args, 40, uint64(cap(dst)))
out, err := k.CallFunc("decodeBlockAVX512", args)
if err != nil {
t.Fatalf("CallFunc: %v", err)
}
n := int(GetUint64(out, 48))
code := int(GetUint64(out, 56))
if n != tt.wantN || code != tt.wantCode {
t.Errorf("got (n=%d, code=%d), want (n=%d, code=%d)", n, code, tt.wantN, tt.wantCode)
}
})
}
}
func TestAVX512DifferentialFuzz(t *testing.T) {
k := loadLZ4AVX512Kernel(t)
rng := rand.New(rand.NewSource(77))
for i := 0; i < 3000; i++ {
wantSize := 1 + rng.Intn(4096)
src := genLZ4Block(rng, wantSize)
dstSize := wantSize + 64
goDst := make([]byte, dstSize)
goN, goCode := decodeBlockGo(src, goDst)
jitDst := make([]byte, dstSize)
args := make([]byte, 64)
if len(src) > 0 {
PutPtr(args, 0, unsafe.Pointer(&src[0]))
}
PutUint64(args, 8, uint64(len(src)))
PutUint64(args, 16, uint64(cap(src)))
if dstSize > 0 {
PutPtr(args, 24, unsafe.Pointer(&jitDst[0]))
}
PutUint64(args, 32, uint64(dstSize))
PutUint64(args, 40, uint64(cap(jitDst)))
out, err := k.CallFunc("decodeBlockAVX512", args)
if err != nil {
t.Fatalf("iter %d: %v", i, err)
}
jitN := int(GetUint64(out, 48))
jitCode := int(GetUint64(out, 56))
if jitCode != goCode {
t.Fatalf("iter %d: code mismatch: JIT=%d Go=%d", i, jitCode, goCode)
}
if jitCode != 0 {
continue
}
if jitN != goN {
t.Fatalf("iter %d: n mismatch: JIT=%d Go=%d", i, jitN, goN)
}
if !bytes.Equal(jitDst[:jitN], goDst[:goN]) {
t.Fatalf("iter %d: output mismatch (n=%d)", i, jitN)
}
}
}
func TestAVX512WideCopy(t *testing.T) {
k := loadLZ4AVX512Kernel(t)
sizes := []int{0, 1, 31, 32, 63, 64, 65, 127, 128, 256, 1024}
for _, n := range sizes {
src := make([]byte, n)
for i := range src {
src[i] = byte(i*11 + 3)
}
dst := make([]byte, n)
args := make([]byte, 48)
if n > 0 {
PutPtr(args, 0, unsafe.Pointer(&dst[0]))
PutPtr(args, 24, unsafe.Pointer(&src[0]))
}
PutUint64(args, 8, uint64(n))
PutUint64(args, 16, uint64(n))
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(n))
_, err := k.CallFunc("wideCopyAVX512", args)
if err != nil {
t.Fatalf("wideCopyAVX512(n=%d): %v", n, err)
}
if !bytes.Equal(dst, src) {
t.Errorf("wideCopyAVX512(n=%d): mismatch", n)
}
}
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
//go:build amd64
package verify
import (
"encoding/binary"
"fmt"
"reflect"
"syscall"
"unsafe"
)
// savedSP holds the Go stack pointer while a JIT call is in flight.
// Referenced by the assembly trampoline (trampoline_amd64.s).
var savedSP uintptr
// enterJIT switches to the prepared stack and jumps to fn.
// It does not return normally; the JIT function's RET transfers control
// to leaveJIT, which restores the Go stack.
//
//go:nosplit
func enterJIT(fn uintptr, stack uintptr)
// leaveJIT restores the Go stack after a JIT function returns.
// Its address is placed as the return address on the prepared stack.
//
//go:nosplit
func leaveJIT()
// leaveJITAddr is the machine address of leaveJIT, resolved once at init.
var leaveJITAddr uintptr
func init() {
leaveJITAddr = reflect.ValueOf(leaveJIT).Pointer()
}
// stackPad is padding below the return address on the prepared stack.
// The ABIInternal wrapper that leaveJIT's address resolves to executes
// PUSHQ BP and CALL before reaching the raw assembly, writing up to 16
// bytes below the return-address slot. 64 bytes of headroom is ample.
const stackPad = 64
// Call invokes the assembled function at fnAddr with the given ABI0 argument
// block (the raw bytes that would appear at FP+0). It returns the argument
// block after the call, which contains any results the function wrote back
// (the ABI0 convention shares the argument area for inputs and outputs).
//
// The function must be NOSPLIT (no stack growth) and must not reference
// external symbols — the image is self-contained.
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
// Prepare the stack: [padding][leaveJIT addr][args...]
stackSize := stackPad + 8 + len(args) + 64 // padding + ret + args + safety
stackMem, err := syscall.Mmap(-1, 0, stackSize,
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON)
if err != nil {
return nil, fmt.Errorf("verify: stack mmap: %w", err)
}
defer syscall.Munmap(stackMem)
// The return address sits after the padding; the function's SP will
// point here, leaving stackPad bytes below for the wrapper's pushes.
retOff := stackPad
binary.LittleEndian.PutUint64(stackMem[retOff:retOff+8], uint64(leaveJITAddr))
// The ABI0 argument area follows the return address.
copy(stackMem[retOff+8:], args)
stackBase := uintptr(unsafe.Pointer(&stackMem[retOff]))
enterJIT(fnAddr, stackBase)
// Copy out the (possibly modified) argument area.
out := make([]byte, len(args))
copy(out, stackMem[retOff+8:retOff+8+len(args)])
return out, nil
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
//go:build !amd64
package verify
import "fmt"
// Call is unavailable on non-amd64 architectures.
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
return nil, fmt.Errorf("verify: JIT execution requires amd64")
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"fmt"
"sort"
)
// Block describes one basic block within a function: a maximal sequence of
// instructions with a single entry point (a label or the function start) and
// a single exit (a jump, conditional jump or RET).
type Block struct {
Offset int // byte offset within the function
Label string // label name ("" for the entry block)
}
// Blocks identifies the basic blocks of a function from its local labels.
// Each label is a potential jump target and therefore a block boundary; the
// function entry (offset 0) is always a block. The blocks are returned in
// ascending offset order.
func (k *Kernel) Blocks(name string) ([]Block, error) {
idx, ok := k.funcs[name]
if !ok {
return nil, fmt.Errorf("verify: function %q not found", name)
}
fl := k.img.Funcs[idx]
blocks := []Block{{Offset: 0, Label: "(entry)"}}
// Build a reverse map: offset → label name.
offToLabel := make(map[int]string, len(fl.Labels))
for label, off := range fl.Labels {
if off > 0 && off < fl.Size {
offToLabel[off] = label
}
}
// Collect and sort offsets.
offsets := make([]int, 0, len(offToLabel))
for off := range offToLabel {
offsets = append(offsets, off)
}
sort.Ints(offsets)
for _, off := range offsets {
blocks = append(blocks, Block{Offset: off, Label: offToLabel[off]})
}
return blocks, nil
}
// BlockCount returns the number of identified basic blocks for the function.
func (k *Kernel) BlockCount(name string) (int, error) {
blocks, err := k.Blocks(name)
if err != nil {
return 0, err
}
return len(blocks), nil
}
// PathFingerprint is the observable output of one function execution: the
// values written back into the result slots of the argument block. Two
// executions that produce the same fingerprint took observationally
// equivalent paths (though they may differ internally).
type PathFingerprint struct {
Results []uint64 // the result words from the arg block
}
// ProfilePaths runs the function with each of the given argument blocks and
// collects the distinct output fingerprints. This measures path diversity:
// how many observationally different execution paths the input corpus
// exercises. Combined with Blocks (the static block count), it gives a
// lower bound on code coverage.
func (k *Kernel) ProfilePaths(name string, argSets [][]byte, resultOffsets []int) ([]PathFingerprint, error) {
idx, ok := k.funcs[name]
if !ok {
return nil, fmt.Errorf("verify: function %q not found", name)
}
fl := k.img.Funcs[idx]
seen := map[string]bool{}
var paths []PathFingerprint
for _, args := range argSets {
if len(args) < fl.Args {
return nil, fmt.Errorf("verify: %s: arg block too small", name)
}
out, err := k.CallFunc(name, args)
if err != nil {
return nil, err
}
fp := PathFingerprint{}
key := ""
for _, off := range resultOffsets {
v := GetUint64(out, off)
fp.Results = append(fp.Results, v)
key += fmt.Sprintf("%016x", v)
}
if !seen[key] {
seen[key] = true
paths = append(paths, fp)
}
}
return paths, nil
}
+85
View File
@@ -0,0 +1,85 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"testing"
"unsafe"
)
func TestBlocks(t *testing.T) {
k := loadBasic(t)
// The "sum" function has labels: sum_done, sum_loop.
blocks, err := k.Blocks("sum")
if err != nil {
t.Fatalf("Blocks(sum): %v", err)
}
if len(blocks) < 3 {
t.Errorf("sum: expected at least 3 blocks (entry + 2 labels), got %d", len(blocks))
}
if blocks[0].Offset != 0 {
t.Errorf("first block offset = %d, want 0", blocks[0].Offset)
}
t.Logf("sum blocks: %v", blocks)
}
func TestBlockCount(t *testing.T) {
k := loadLZ4Kernel(t)
n, err := k.BlockCount("decodeBlockAVX2")
if err != nil {
t.Fatalf("BlockCount: %v", err)
}
// The decoder has many labels (dec_loop, dec_malformed, etc.).
if n < 10 {
t.Errorf("decodeBlockAVX2: expected at least 10 blocks, got %d", n)
}
t.Logf("decodeBlockAVX2: %d basic blocks", n)
}
func TestProfilePaths(t *testing.T) {
k := loadLZ4Kernel(t)
// Build a corpus of varied LZ4 blocks.
var argSets [][]byte
blocks := []struct {
src []byte
dstSize int
}{
{[]byte{0x00}, 16}, // empty
{[]byte{0x50, 'H', 'e', 'l', 'l', 'o'}, 16}, // literals only
{[]byte{0x54, 'A', 'A', 'A', 'A', 'A', 5, 0, 0x30, 'B', 'B', 'B'}, 32}, // match
{[]byte{0x14, 'X', 1, 0, 0x10, 'Y'}, 16}, // overlapping
{[]byte{0x50, 'H'}, 16}, // malformed
{[]byte{0x14, 'X', 0, 0}, 16}, // zero offset
}
for _, b := range blocks {
args := make([]byte, 64)
if len(b.src) > 0 {
PutPtr(args, 0, unsafe.Pointer(&b.src[0]))
}
PutUint64(args, 8, uint64(len(b.src)))
PutUint64(args, 16, uint64(cap(b.src)))
dst := make([]byte, b.dstSize)
if len(dst) > 0 {
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
}
PutUint64(args, 32, uint64(len(dst)))
PutUint64(args, 40, uint64(cap(dst)))
argSets = append(argSets, args)
}
// Result offsets: n+48 and code+56.
paths, err := k.ProfilePaths("decodeBlockAVX2", argSets, []int{48, 56})
if err != nil {
t.Fatalf("ProfilePaths: %v", err)
}
// We expect at least 3 distinct paths: success (various n), malformed, zero offset.
if len(paths) < 3 {
t.Errorf("expected at least 3 distinct paths, got %d", len(paths))
}
t.Logf("decodeBlockAVX2: %d distinct output paths from %d inputs", len(paths), len(argSets))
}
+295
View File
@@ -0,0 +1,295 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"bytes"
"math/rand"
"testing"
"unsafe"
)
// decodeBlockGo is a minimal portable LZ4 block decoder used as the
// differential-testing oracle. It mirrors the contract of
// go-lz4's decodeBlockGo: (bytesWritten, code) where code is
// 0 = ok, 1 = malformed, 2 = zero offset.
func decodeBlockGo(src, dst []byte) (int, int) {
if len(src) == 0 {
return 0, 1
}
si, di := 0, 0
for {
if si >= len(src) {
return 0, 1 // truncated: no token
}
token := int(src[si])
si++
// Literals.
lLen := token >> 4
if lLen == 15 {
for {
if si >= len(src) {
return 0, 1
}
b := int(src[si])
si++
lLen += b
if b != 255 {
break
}
}
}
if si+lLen > len(src) {
return 0, 1 // truncated literals
}
if di+lLen > len(dst) {
return 0, 1 // destination overflow
}
copy(dst[di:di+lLen], src[si:si+lLen])
di += lLen
si += lLen
// End of block.
if si >= len(src) {
return di, 0
}
// Match offset.
if si+2 > len(src) {
return 0, 1
}
offset := int(src[si]) | int(src[si+1])<<8
si += 2
if offset == 0 {
return 0, 2
}
// Match length.
mLen := token & 15
if mLen == 15 {
for {
if si >= len(src) {
return 0, 1
}
b := int(src[si])
si++
mLen += b
if b != 255 {
break
}
}
}
mLen += 4
// Copy match (overlapping-safe).
if di-offset < 0 {
return 0, 1 // offset reaches before dst start
}
if di+mLen > len(dst) {
return 0, 1 // destination overflow
}
for i := 0; i < mLen; i++ {
dst[di+i] = dst[di-offset+i]
}
di += mLen
}
}
// genLZ4Block generates a random valid LZ4 block that decompresses into
// approximately wantSize bytes. The block is always well-formed (ends with
// a literals-only sequence).
func genLZ4Block(rng *rand.Rand, wantSize int) []byte {
var block []byte
produced := 0
for produced < wantSize {
remaining := wantSize - produced
// Decide: emit a literals+match sequence or the final literals.
if remaining <= 8 || rng.Intn(4) == 0 {
// Final literals-only sequence.
lLen := remaining
if lLen > 60 {
lLen = 1 + rng.Intn(60)
}
block = appendToken(block, lLen, 0)
for i := 0; i < lLen; i++ {
block = append(block, byte(rng.Intn(256)))
}
produced += lLen
break
}
// Literals + match.
lLen := rng.Intn(min(16, remaining))
if produced+lLen == 0 {
lLen = 1 // must have at least 1 literal before the first match
}
mLenRaw := rng.Intn(12) // match length = mLenRaw + 4
mLen := mLenRaw + 4
if produced+mLen > remaining {
mLen = remaining - produced
if mLen < 4 {
// Not enough room for a match; emit final literals.
lLen = remaining
block = appendToken(block, lLen, 0)
for i := 0; i < lLen; i++ {
block = append(block, byte(rng.Intn(256)))
}
break
}
mLenRaw = mLen - 4
}
block = appendToken(block, lLen, mLenRaw)
for i := 0; i < lLen; i++ {
block = append(block, byte(rng.Intn(256)))
}
produced += lLen
// Offset: must be <= produced (can't reference before start).
maxOff := produced
if maxOff > 65535 {
maxOff = 65535
}
offset := 1 + rng.Intn(maxOff)
block = append(block, byte(offset), byte(offset>>8))
produced += mLen
}
return block
}
// appendToken appends a token (and extension bytes if needed) for the given
// literal and match lengths.
func appendToken(block []byte, lLen, mLenRaw int) []byte {
lit4 := lLen
if lit4 > 15 {
lit4 = 15
}
ml4 := mLenRaw
if ml4 > 15 {
ml4 = 15
}
block = append(block, byte(lit4<<4|ml4))
// Literal extension bytes.
rem := lLen - 15
for rem >= 255 {
block = append(block, 255)
rem -= 255
}
if lLen >= 15 {
block = append(block, byte(rem))
}
// Match extension bytes.
rem = mLenRaw - 15
for rem >= 255 {
block = append(block, 255)
rem -= 255
}
if mLenRaw >= 15 {
block = append(block, byte(rem))
}
return block
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
// TestDifferentialLZ4Fuzz drives the JIT-assembled decodeBlockAVX2 with
// random valid LZ4 blocks and compares the output bit-for-bit against the
// portable Go reference.
func TestDifferentialLZ4Fuzz(t *testing.T) {
k := loadLZ4Kernel(t)
const iterations = 5000
rng := rand.New(rand.NewSource(42))
for i := 0; i < iterations; i++ {
wantSize := 1 + rng.Intn(4096)
src := genLZ4Block(rng, wantSize)
dstSize := wantSize + 64 // generous destination
// Go reference.
goDst := make([]byte, dstSize)
goN, goCode := decodeBlockGo(src, goDst)
// JIT kernel.
jitDst := make([]byte, dstSize)
jitN, jitCode := callDecodeBlockAVX2(t, k, src, jitDst)
if jitCode != goCode {
t.Fatalf("iter %d: code mismatch: JIT=%d, Go=%d (src len=%d)",
i, jitCode, goCode, len(src))
}
if jitCode != 0 {
continue // both agree it's malformed/zero-offset
}
if jitN != goN {
t.Fatalf("iter %d: n mismatch: JIT=%d, Go=%d (src len=%d)",
i, jitN, goN, len(src))
}
if !bytes.Equal(jitDst[:jitN], goDst[:goN]) {
t.Fatalf("iter %d: output mismatch (n=%d, src len=%d)", i, jitN, len(src))
}
}
}
// TestDifferentialLZ4Hostile drives the kernel with random garbage to check
// that error codes agree with the Go reference (no crashes, same classification).
func TestDifferentialLZ4Hostile(t *testing.T) {
k := loadLZ4Kernel(t)
const iterations = 2000
rng := rand.New(rand.NewSource(99))
for i := 0; i < iterations; i++ {
srcLen := rng.Intn(128)
src := make([]byte, srcLen)
rng.Read(src)
dstSize := rng.Intn(512)
dst := make([]byte, dstSize)
// Go reference.
goDst := make([]byte, dstSize)
copy(goDst, dst)
_, goCode := decodeBlockGo(src, goDst)
// JIT kernel.
jitDst := make([]byte, dstSize)
copy(jitDst, dst)
_, jitCode := callDecodeBlockAVX2(t, k, src, jitDst)
if jitCode != goCode {
t.Fatalf("iter %d: hostile code mismatch: JIT=%d, Go=%d (srcLen=%d, dstSize=%d)",
i, jitCode, goCode, srcLen, dstSize)
}
}
}
// callDecodeBlockAVX2Raw is like callDecodeBlockAVX2 but accepts explicit
// dst size (for hostile tests where dst may be smaller than the output).
func callDecodeBlockAVX2Raw(t *testing.T, k *Kernel, src, dst []byte) (int, int) {
t.Helper()
args := make([]byte, 64)
if len(src) > 0 {
PutPtr(args, 0, unsafe.Pointer(&src[0]))
}
PutUint64(args, 8, uint64(len(src)))
PutUint64(args, 16, uint64(cap(src)))
if len(dst) > 0 {
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
}
PutUint64(args, 32, uint64(len(dst)))
PutUint64(args, 40, uint64(cap(dst)))
out, err := k.CallFunc("decodeBlockAVX2", args)
if err != nil {
t.Fatalf("CallFunc(decodeBlockAVX2): %v", err)
}
return int(GetUint64(out, 48)), int(GetUint64(out, 56))
}
+585
View File
@@ -0,0 +1,585 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"bytes"
"math/rand"
"os"
"testing"
"unsafe"
)
const flacKernelPath = "../../go-libraries/go-flac/avx2_amd64.s"
func loadFLACKernel(t *testing.T) *Kernel {
t.Helper()
if _, err := os.Stat(flacKernelPath); err != nil {
t.Skipf("sibling kernel not available: %v", err)
}
k, err := Load(flacKernelPath)
if err != nil {
t.Fatalf("Load(%s): %v", flacKernelPath, err)
}
t.Cleanup(k.Close)
return k
}
// --- Portable Go references (from go-flac/simd.go) ---
func decodeMono16Go(src []byte, dst []int32) {
for i := 0; i < len(dst); i++ {
dst[i] = int32(int16(uint16(src[2*i]) | uint16(src[2*i+1])<<8))
}
}
func pack16Go(dst []byte, src []int32) {
for i, v := range src {
dst[2*i] = byte(v)
dst[2*i+1] = byte(v >> 8)
}
}
func decorrelateLeftSideGo(left, right, out []int32) {
for i := range left {
l := left[i]
out[2*i] = l
out[2*i+1] = l - right[i]
}
}
func decorrelateSideRightGo(left, right, out []int32) {
for i := range left {
side := left[i]
rch := right[i]
out[2*i] = rch + side
out[2*i+1] = rch
}
}
func decorrelateMidSideGo(left, right, out []int32) {
for i := range left {
mid := left[i]
side := right[i]
mid2 := mid<<1 | (side & 1)
out[2*i] = (mid2 + side) >> 1
out[2*i+1] = (mid2 - side) >> 1
}
}
func decorrelateInterleaveGo(left, right, out []int32) {
for i := range left {
out[2*i] = left[i]
out[2*i+1] = right[i]
}
}
func analyzeO1RangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool) {
swin = swin[:len(dstP)+1]
for j := 0; j+1 < len(swin); j++ {
r := swin[j+1] - swin[j]
if r == -2147483648 { // math.MinInt32
overflow = true
}
f := uint32(r<<1) ^ uint32(r>>31)
dstP[j] = f
partSum += uint64(f)
bl := 0
for v := f; v > 0; v >>= 1 {
bl++
}
if bl > 31 {
bl = 31
}
hist[bl]++
}
return
}
func analyzeO2RangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool) {
swin = swin[:len(dstP)+2]
for j := 0; j+2 < len(swin); j++ {
r := swin[j+2] - 2*swin[j+1] + swin[j]
if r == -2147483648 {
overflow = true
}
f := uint32(r<<1) ^ uint32(r>>31)
dstP[j] = f
partSum += uint64(f)
bl := 0
for v := f; v > 0; v >>= 1 {
bl++
}
if bl > 31 {
bl = 31
}
hist[bl]++
}
return
}
func analyzeResRangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool) {
for j := 0; j < len(swin); j++ {
r := swin[j]
if r == -2147483648 {
overflow = true
}
f := uint32(r<<1) ^ uint32(r>>31)
dstP[j] = f
partSum += uint64(f)
bl := 0
for v := f; v > 0; v >>= 1 {
bl++
}
if bl > 31 {
bl = 31
}
hist[bl]++
}
return
}
func decodeMono24Go(src []byte, dst []int32) {
for i := 0; i < len(dst); i++ {
off := 3 * i
u := uint32(src[off]) | uint32(src[off+1])<<8 | uint32(src[off+2])<<16
dst[i] = int32(u<<8) >> 8
}
}
func decodeStereo16Go(src []byte, left, right []int32) {
for i := 0; i < len(left); i++ {
left[i] = int32(int16(uint16(src[4*i]) | uint16(src[4*i+1])<<8))
right[i] = int32(int16(uint16(src[4*i+2]) | uint16(src[4*i+3])<<8))
}
}
// --- Differential tests ---
func TestFLACDecodeMono16(t *testing.T) {
k := loadFLACKernel(t)
rng := rand.New(rand.NewSource(7))
for iter := 0; iter < 500; iter++ {
n := rng.Intn(256)
src := make([]byte, 2*n)
rng.Read(src)
goDst := make([]int32, n)
decodeMono16Go(src, goDst)
jitDst := make([]int32, n)
args := make([]byte, 48)
if len(src) > 0 {
PutPtr(args, 0, unsafe.Pointer(&src[0]))
}
PutUint64(args, 8, uint64(len(src)))
PutUint64(args, 16, uint64(cap(src)))
if n > 0 {
PutPtr(args, 24, unsafe.Pointer(&jitDst[0]))
}
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(cap(jitDst)))
_, err := k.CallFunc("decodeMono16AVX2", args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
for i := range goDst {
if jitDst[i] != goDst[i] {
t.Fatalf("iter %d: mismatch at [%d]: JIT=%d Go=%d", iter, i, jitDst[i], goDst[i])
}
}
}
}
func TestFLACPack16(t *testing.T) {
k := loadFLACKernel(t)
rng := rand.New(rand.NewSource(13))
for iter := 0; iter < 500; iter++ {
n := rng.Intn(256)
src := make([]int32, n)
for i := range src {
src[i] = int32(rng.Intn(65536) - 32768)
}
goDst := make([]byte, 2*n)
pack16Go(goDst, src)
jitDst := make([]byte, 2*n)
args := make([]byte, 48)
if len(jitDst) > 0 {
PutPtr(args, 0, unsafe.Pointer(&jitDst[0]))
}
PutUint64(args, 8, uint64(len(jitDst)))
PutUint64(args, 16, uint64(cap(jitDst)))
if n > 0 {
PutPtr(args, 24, unsafe.Pointer(&src[0]))
}
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(cap(src)))
_, err := k.CallFunc("pack16AVX2", args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
if !bytes.Equal(jitDst, goDst) {
t.Fatalf("iter %d: output mismatch (n=%d)", iter, n)
}
}
}
func TestFLACDecorrelate(t *testing.T) {
k := loadFLACKernel(t)
rng := rand.New(rand.NewSource(21))
kernels := []struct {
name string
ref func(left, right, out []int32)
}{
{"decorrelateLeftSideAVX2", decorrelateLeftSideGo},
{"decorrelateSideRightAVX2", decorrelateSideRightGo},
{"decorrelateMidSideAVX2", decorrelateMidSideGo},
{"decorrelateInterleaveAVX2", decorrelateInterleaveGo},
}
for _, kk := range kernels {
t.Run(kk.name, func(t *testing.T) {
for iter := 0; iter < 200; iter++ {
n := rng.Intn(128)
left := make([]int32, n)
right := make([]int32, n)
for i := range left {
left[i] = int32(rng.Intn(1<<24) - 1<<23)
right[i] = int32(rng.Intn(1<<24) - 1<<23)
}
goOut := make([]int32, 2*n)
kk.ref(left, right, goOut)
jitOut := make([]int32, 2*n)
args := make([]byte, 72)
if n > 0 {
PutPtr(args, 0, unsafe.Pointer(&left[0]))
PutPtr(args, 24, unsafe.Pointer(&right[0]))
PutPtr(args, 48, unsafe.Pointer(&jitOut[0]))
}
PutUint64(args, 8, uint64(n))
PutUint64(args, 16, uint64(cap(left)))
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(cap(right)))
PutUint64(args, 56, uint64(2*n))
PutUint64(args, 64, uint64(cap(jitOut)))
_, err := k.CallFunc(kk.name, args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
for i := range goOut {
if jitOut[i] != goOut[i] {
t.Fatalf("iter %d: mismatch at [%d]: JIT=%d Go=%d", iter, i, jitOut[i], goOut[i])
}
}
}
})
}
}
func TestFLACAnalyzeO1Range(t *testing.T) {
k := loadFLACKernel(t)
rng := rand.New(rand.NewSource(33))
for iter := 0; iter < 300; iter++ {
n := 1 + rng.Intn(128) // partition size
swin := make([]int32, n+1)
for i := range swin {
swin[i] = int32(rng.Intn(1<<20) - 1<<19)
}
goDstP := make([]uint32, n)
var goHist [32]uint16
goSum, goOvf := analyzeO1RangeGo(swin, goDstP, &goHist)
jitDstP := make([]uint32, n)
var jitHist [32]uint16
args := make([]byte, 72) // 65 rounded up
PutPtr(args, 0, unsafe.Pointer(&swin[0]))
PutUint64(args, 8, uint64(len(swin)))
PutUint64(args, 16, uint64(cap(swin)))
PutPtr(args, 24, unsafe.Pointer(&jitDstP[0]))
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(cap(jitDstP)))
PutPtr(args, 48, unsafe.Pointer(&jitHist[0]))
out, err := k.CallFunc("analyzeO1RangeAVX2", args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
jitSum := GetUint64(out, 56)
jitOvf := out[64] != 0
if jitSum != goSum {
t.Fatalf("iter %d: partSum mismatch: JIT=%d Go=%d", iter, jitSum, goSum)
}
if jitOvf != goOvf {
t.Fatalf("iter %d: overflow mismatch: JIT=%v Go=%v", iter, jitOvf, goOvf)
}
for i := range goDstP {
if jitDstP[i] != goDstP[i] {
t.Fatalf("iter %d: dstP[%d] mismatch: JIT=%d Go=%d", iter, i, jitDstP[i], goDstP[i])
}
}
if jitHist != goHist {
t.Fatalf("iter %d: hist mismatch: JIT=%v Go=%v", iter, jitHist, goHist)
}
}
}
func TestFLACFastStereoSums(t *testing.T) {
k := loadFLACKernel(t)
rng := rand.New(rand.NewSource(44))
for iter := 0; iter < 300; iter++ {
n := 1 + rng.Intn(256)
left := make([]int32, n)
right := make([]int32, n)
for i := range left {
left[i] = int32(rng.Intn(1<<24) - 1<<23)
right[i] = int32(rng.Intn(1<<24) - 1<<23)
}
// Go reference: compute the four sums.
var goSums [4]uint64
for i := 0; i < n; i++ {
l := left[i]
r := right[i]
side := l - r
mid := (l + r) >> 1
goSums[0] += foldAbs(l) + foldAbs(r)
goSums[1] += foldAbs(l) + foldAbs(side)
goSums[2] += foldAbs(side) + foldAbs(r)
goSums[3] += foldAbs(mid) + foldAbs(side)
}
var jitSums [4]uint64
args := make([]byte, 56)
PutPtr(args, 0, unsafe.Pointer(&left[0]))
PutUint64(args, 8, uint64(n))
PutUint64(args, 16, uint64(cap(left)))
PutPtr(args, 24, unsafe.Pointer(&right[0]))
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(cap(right)))
PutPtr(args, 48, unsafe.Pointer(&jitSums[0]))
_, err := k.CallFunc("fastStereoSumsAVX2", args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
if jitSums != goSums {
t.Fatalf("iter %d: sums mismatch:\n JIT=%v\n Go =%v", iter, jitSums, goSums)
}
}
}
func foldAbs(v int32) uint64 {
return uint64(uint32(v<<1) ^ uint32(v>>31))
}
// runAnalyzeTest is the shared harness for the analyzeO*Range family.
func runAnalyzeTest(t *testing.T, k *Kernel, name string, order int, ref func([]int32, []uint32, *[32]uint16) (uint64, bool)) {
t.Helper()
rng := rand.New(rand.NewSource(int64(order)*100 + 7))
for iter := 0; iter < 200; iter++ {
n := 1 + rng.Intn(128)
swin := make([]int32, n+order)
for i := range swin {
swin[i] = int32(rng.Intn(1<<20) - 1<<19)
}
goDstP := make([]uint32, n)
var goHist [32]uint16
goSum, goOvf := ref(swin, goDstP, &goHist)
jitDstP := make([]uint32, n)
var jitHist [32]uint16
args := make([]byte, 72)
PutPtr(args, 0, unsafe.Pointer(&swin[0]))
PutUint64(args, 8, uint64(len(swin)))
PutUint64(args, 16, uint64(cap(swin)))
PutPtr(args, 24, unsafe.Pointer(&jitDstP[0]))
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(cap(jitDstP)))
PutPtr(args, 48, unsafe.Pointer(&jitHist[0]))
out, err := k.CallFunc(name, args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
jitSum := GetUint64(out, 56)
jitOvf := out[64] != 0
if jitSum != goSum {
t.Fatalf("iter %d: partSum: JIT=%d Go=%d", iter, jitSum, goSum)
}
if jitOvf != goOvf {
t.Fatalf("iter %d: overflow: JIT=%v Go=%v", iter, jitOvf, goOvf)
}
for i := range goDstP {
if jitDstP[i] != goDstP[i] {
t.Fatalf("iter %d: dstP[%d]: JIT=%d Go=%d", iter, i, jitDstP[i], goDstP[i])
}
}
if jitHist != goHist {
t.Fatalf("iter %d: hist mismatch", iter)
}
}
}
func TestFLACAnalyzeO2Range(t *testing.T) {
k := loadFLACKernel(t)
runAnalyzeTest(t, k, "analyzeO2RangeAVX2", 2, analyzeO2RangeGo)
}
func TestFLACAnalyzeResRange(t *testing.T) {
k := loadFLACKernel(t)
// analyzeResRange has order 0: swin IS the residual (no prediction).
runAnalyzeTest(t, k, "analyzeResRangeAVX2", 0, analyzeResRangeGo)
}
func TestFLACAnalyzeO3Range(t *testing.T) {
k := loadFLACKernel(t)
ref := func(swin []int32, dstP []uint32, hist *[32]uint16) (uint64, bool) {
swin = swin[:len(dstP)+3]
var partSum uint64
var overflow bool
for j := 0; j+3 < len(swin); j++ {
r := swin[j+3] - 3*swin[j+2] + 3*swin[j+1] - swin[j]
if r == -2147483648 {
overflow = true
}
f := uint32(r<<1) ^ uint32(r>>31)
dstP[j] = f
partSum += uint64(f)
bl := 0
for v := f; v > 0; v >>= 1 {
bl++
}
if bl > 31 {
bl = 31
}
hist[bl]++
}
return partSum, overflow
}
runAnalyzeTest(t, k, "analyzeO3RangeAVX2", 3, ref)
}
func TestFLACAnalyzeO4Range(t *testing.T) {
k := loadFLACKernel(t)
ref := func(swin []int32, dstP []uint32, hist *[32]uint16) (uint64, bool) {
swin = swin[:len(dstP)+4]
var partSum uint64
var overflow bool
for j := 0; j+4 < len(swin); j++ {
r := swin[j+4] - 4*swin[j+3] + 6*swin[j+2] - 4*swin[j+1] + swin[j]
if r == -2147483648 {
overflow = true
}
f := uint32(r<<1) ^ uint32(r>>31)
dstP[j] = f
partSum += uint64(f)
bl := 0
for v := f; v > 0; v >>= 1 {
bl++
}
if bl > 31 {
bl = 31
}
hist[bl]++
}
return partSum, overflow
}
runAnalyzeTest(t, k, "analyzeO4RangeAVX2", 4, ref)
}
func TestFLACDecodeMono24(t *testing.T) {
k := loadFLACKernel(t)
rng := rand.New(rand.NewSource(55))
for iter := 0; iter < 500; iter++ {
n := rng.Intn(256)
src := make([]byte, 3*n)
rng.Read(src)
goDst := make([]int32, n)
decodeMono24Go(src, goDst)
jitDst := make([]int32, n)
args := make([]byte, 48)
if len(src) > 0 {
PutPtr(args, 0, unsafe.Pointer(&src[0]))
}
PutUint64(args, 8, uint64(len(src)))
PutUint64(args, 16, uint64(cap(src)))
if n > 0 {
PutPtr(args, 24, unsafe.Pointer(&jitDst[0]))
}
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(cap(jitDst)))
_, err := k.CallFunc("decodeMono24AVX2", args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
for i := range goDst {
if jitDst[i] != goDst[i] {
t.Fatalf("iter %d: dst[%d]: JIT=%d Go=%d", iter, i, jitDst[i], goDst[i])
}
}
}
}
func TestFLACDecodeStereo16(t *testing.T) {
k := loadFLACKernel(t)
rng := rand.New(rand.NewSource(66))
for iter := 0; iter < 500; iter++ {
n := rng.Intn(256)
src := make([]byte, 4*n) // [L0,R0,L1,R1,...]
rng.Read(src)
goLeft := make([]int32, n)
goRight := make([]int32, n)
decodeStereo16Go(src, goLeft, goRight)
jitLeft := make([]int32, n)
jitRight := make([]int32, n)
args := make([]byte, 72)
if len(src) > 0 {
PutPtr(args, 0, unsafe.Pointer(&src[0]))
}
PutUint64(args, 8, uint64(len(src)))
PutUint64(args, 16, uint64(cap(src)))
if n > 0 {
PutPtr(args, 24, unsafe.Pointer(&jitLeft[0]))
PutPtr(args, 48, unsafe.Pointer(&jitRight[0]))
}
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(cap(jitLeft)))
PutUint64(args, 56, uint64(n))
PutUint64(args, 64, uint64(cap(jitRight)))
_, err := k.CallFunc("decodeStereo16AVX2", args)
if err != nil {
t.Fatalf("iter %d: %v", iter, err)
}
for i := 0; i < n; i++ {
if jitLeft[i] != goLeft[i] || jitRight[i] != goRight[i] {
t.Fatalf("iter %d: [%d] L: JIT=%d Go=%d; R: JIT=%d Go=%d",
iter, i, jitLeft[i], goLeft[i], jitRight[i], goRight[i])
}
}
}
}
+362
View File
@@ -0,0 +1,362 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"fmt"
"math/rand"
"regexp"
"strconv"
"strings"
"unsafe"
)
// FuzzResult reports the outcome of a differential fuzz campaign for one
// function.
type FuzzResult struct {
Func string
Iterations int
Matches int
Mismatches int
FirstFail string // description of the first mismatch ("" if none)
}
// OK returns true when all iterations matched.
func (r FuzzResult) OK() bool { return r.Mismatches == 0 }
// String returns a human-readable summary.
func (r FuzzResult) String() string {
if r.OK() {
return fmt.Sprintf("%s: %d/%d iterations match", r.Func, r.Matches, r.Iterations)
}
return fmt.Sprintf("%s: %d/%d match, %d MISMATCH — %s",
r.Func, r.Matches, r.Iterations, r.Mismatches, r.FirstFail)
}
// funcSig is a parsed // func signature from the assembly source.
type funcSig struct {
name string
params []param
results []param
}
type param struct {
name string
typ string // "[]byte", "[]int32", "int", "*[32]uint16", etc.
}
// funcSigRe matches the conventional "// func name(...)" comment.
var funcSigRe = regexp.MustCompile(`^//\s*func\s+(\w+)\(([^)]*)\)\s*(.*)$`)
// parseFuncSig extracts the function signature from a "// func ..." comment.
func parseFuncSig(comment string) (funcSig, bool) {
m := funcSigRe.FindStringSubmatch(strings.TrimSpace(comment))
if m == nil {
return funcSig{}, false
}
sig := funcSig{name: m[1]}
sig.params = parseParams(m[2])
// Results may be "(a int, b int)" or "int" or "(int, error)".
res := strings.TrimSpace(m[3])
res = strings.TrimPrefix(res, "(")
res = strings.TrimSuffix(res, ")")
if res != "" {
sig.results = parseParams(res)
}
return sig, true
}
// parseParams splits "a []byte, b []int32" into typed parameters, handling
// shared types ("a, b []int32").
func parseParams(s string) []param {
s = strings.TrimSpace(s)
if s == "" {
return nil
}
var out []param
for _, field := range strings.Split(s, ",") {
field = strings.TrimSpace(field)
if field == "" {
continue
}
parts := strings.Fields(field)
if len(parts) == 1 {
// Unnamed: "int" or "[]byte".
out = append(out, param{typ: parts[0]})
} else {
// Named: "a []byte" or shared "a, b []int32" (handled by the
// comma split above — "a" alone means the type follows in the
// next field; this is a simplification that covers the common
// case where each param has its own type).
out = append(out, param{name: parts[0], typ: parts[1]})
}
}
return out
}
// ExtractSignatures scans assembly source for "// func name(...)" comments
// that immediately precede a TEXT directive, and returns the parsed
// signatures keyed by the function's short name.
func ExtractSignatures(src string) map[string]funcSig {
lines := strings.Split(src, "\n")
sigs := make(map[string]funcSig)
var comments []string
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "//") {
comments = append(comments, trimmed)
continue
}
if strings.HasPrefix(trimmed, "TEXT") {
// Search the comment block for the // func line.
for _, c := range comments {
if sig, ok := parseFuncSig(c); ok {
sigs[sig.name] = sig
break
}
}
comments = nil
continue
}
if trimmed != "" {
comments = nil
}
}
return sigs
}
// FuzzFunc runs a differential fuzz campaign: it JIT-executes both the
// gasm-assembled and the go-tool-asm-assembled versions of the named
// function with random inputs derived from the // func signature, and
// compares the output argument area bit-for-bit.
//
// The signature comment must appear immediately above the TEXT directive
// in the source (the conventional Go assembly layout).
func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations int, seed int64) FuzzResult {
result := FuzzResult{Func: name, Iterations: iterations}
rng := rand.New(rand.NewSource(seed))
// Map the Go-assembled code into a second executable region.
goExec, err := Map(goCode)
if err != nil {
result.Mismatches = iterations
result.FirstFail = fmt.Sprintf("map go code: %v", err)
return result
}
defer goExec.Unmap()
fl, err := k.Func(name)
if err != nil {
result.Mismatches = iterations
result.FirstFail = err.Error()
return result
}
for i := 0; i < iterations; i++ {
// Generate inputs and build TWO independent arg blocks (one per
// version) so that functions which write to their arguments
// (e.g. histogram increments) don't corrupt the other's input.
gasmArgs, goArgs, bufs := genDualArgs(rng, sig, fl.Args)
// Call the gasm version.
gasmOut, err := k.CallFunc(name, gasmArgs)
if err != nil {
result.Mismatches++
if result.FirstFail == "" {
result.FirstFail = fmt.Sprintf("iter %d: gasm call: %v", i, err)
}
releaseBufs(bufs)
continue
}
// Call the Go version (same function, independent buffers).
goOut, err := Call(goExec.FuncAddr(0), goArgs)
if err != nil {
result.Mismatches++
if result.FirstFail == "" {
result.FirstFail = fmt.Sprintf("iter %d: go call: %v", i, err)
}
releaseBufs(bufs)
continue
}
// Compare only the result area (after all input parameters).
// Pointers in the arg block differ (separate buffers), so we
// compare from resultOff to the end.
resultOff := paramsSize(sig)
gasmRes := gasmOut[resultOff:]
goRes := goOut[resultOff:]
if !equalBytes(gasmRes, goRes) {
result.Mismatches++
if result.FirstFail == "" {
result.FirstFail = fmt.Sprintf("iter %d: output mismatch at result offset %d", i, resultOff)
}
} else {
result.Matches++
}
releaseBufs(bufs)
}
return result
}
// genDualArgs generates two independent ABI0 argument blocks (for gasm and
// go) with identical logical content but separate backing buffers, so that
// functions which write to their arguments don't corrupt the other's input.
func genDualArgs(rng *rand.Rand, sig funcSig, argSize int) (gasmArgs, goArgs []byte, bufs [][]byte) {
gasmArgs = make([]byte, argSize)
goArgs = make([]byte, argSize)
off := 0
sliceIdx := 0
for _, p := range sig.params {
switch {
case strings.HasPrefix(p.typ, "[]"):
elemSize := elemSizeFor(p.typ)
n := 1 + rng.Intn(127)
var declaredLen int
if sliceIdx == 0 {
declaredLen = n
} else {
declaredLen = n + 512
}
bufBytes := (declaredLen+16)*elemSize + 128
// Two independent buffers with identical random content.
buf1 := make([]byte, bufBytes)
buf2 := make([]byte, bufBytes)
rng.Read(buf1[:n*elemSize])
copy(buf2, buf1)
bufs = append(bufs, buf1, buf2)
putPtr(gasmArgs, off, unsafe.Pointer(&buf1[0]))
putPtr(goArgs, off, unsafe.Pointer(&buf2[0]))
putU64(gasmArgs, off+8, uint64(declaredLen))
putU64(gasmArgs, off+16, uint64(declaredLen))
putU64(goArgs, off+8, uint64(declaredLen))
putU64(goArgs, off+16, uint64(declaredLen))
off += 24
sliceIdx++
case strings.HasPrefix(p.typ, "*["):
nElem := arrayLen(p.typ)
elem := elemSizeFor("[]" + p.typ[strings.Index(p.typ, "]")+1:])
size := nElem * elem
if size < 8 {
size = 8
}
buf1 := make([]byte, size)
buf2 := make([]byte, size)
rng.Read(buf1)
copy(buf2, buf1)
bufs = append(bufs, buf1, buf2)
putPtr(gasmArgs, off, unsafe.Pointer(&buf1[0]))
putPtr(goArgs, off, unsafe.Pointer(&buf2[0]))
off += 8
case p.typ == "int" || p.typ == "uint" || p.typ == "int64" || p.typ == "uint64":
v := uint64(rng.Intn(256))
putU64(gasmArgs, off, v)
putU64(goArgs, off, v)
off += 8
default:
v := rng.Uint64()
putU64(gasmArgs, off, v)
putU64(goArgs, off, v)
off += 8
}
}
return gasmArgs, goArgs, bufs
}
func elemSizeFor(sliceType string) int {
switch strings.TrimPrefix(sliceType, "[]") {
case "byte", "uint8", "int8":
return 1
case "uint16", "int16":
return 2
case "uint32", "int32", "float32":
return 4
case "uint64", "int64", "float64":
return 8
default:
return 8
}
}
// paramsSize returns the ABI0 stack size occupied by the input parameters.
func paramsSize(sig funcSig) int {
size := 0
for _, p := range sig.params {
switch {
case strings.HasPrefix(p.typ, "[]"):
size += 24 // slice header
case strings.HasPrefix(p.typ, "*["):
size += 8 // pointer
case p.typ == "bool":
size += 1
default:
size += 8 // int, uint, etc.
}
}
return size
}
func arrayLen(typ string) int {
// "*[32]uint16" → 32
start := strings.Index(typ, "[")
end := strings.Index(typ, "]")
if start < 0 || end < 0 || end <= start {
return 1
}
n, _ := strconv.Atoi(typ[start+1 : end])
if n <= 0 {
n = 1
}
return n
}
func putPtr(buf []byte, off int, p unsafe.Pointer) {
if off+8 <= len(buf) {
u64 := uint64(uintptr(p))
buf[off] = byte(u64)
buf[off+1] = byte(u64 >> 8)
buf[off+2] = byte(u64 >> 16)
buf[off+3] = byte(u64 >> 24)
buf[off+4] = byte(u64 >> 32)
buf[off+5] = byte(u64 >> 40)
buf[off+6] = byte(u64 >> 48)
buf[off+7] = byte(u64 >> 56)
}
}
func putU64(buf []byte, off int, v uint64) {
if off+8 <= len(buf) {
buf[off] = byte(v)
buf[off+1] = byte(v >> 8)
buf[off+2] = byte(v >> 16)
buf[off+3] = byte(v >> 24)
buf[off+4] = byte(v >> 32)
buf[off+5] = byte(v >> 40)
buf[off+6] = byte(v >> 48)
buf[off+7] = byte(v >> 56)
}
}
func equalBytes(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func releaseBufs(bufs [][]byte) {
// Keep buffers alive until after the call; nothing to free in Go,
// but this prevents the compiler from collecting them too early.
_ = bufs
}
+98
View File
@@ -0,0 +1,98 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"testing"
)
func TestExtractSignatures(t *testing.T) {
src := `// func add(a int64, b int64) int64
TEXT ·add(SB), NOSPLIT, $0-24
RET
// func wideCopy(dst []byte, src []byte)
TEXT ·wideCopy(SB), NOSPLIT, $0-48
RET
`
sigs := ExtractSignatures(src)
if len(sigs) != 2 {
t.Fatalf("expected 2 signatures, got %d: %v", len(sigs), sigs)
}
add, ok := sigs["add"]
if !ok {
t.Fatal("add not found")
}
if len(add.params) != 2 {
t.Errorf("add params: got %d, want 2", len(add.params))
}
wc, ok := sigs["wideCopy"]
if !ok {
t.Fatal("wideCopy not found")
}
if len(wc.params) != 2 {
t.Errorf("wideCopy params: got %d, want 2", len(wc.params))
}
if wc.params[0].typ != "[]byte" {
t.Errorf("wideCopy param[0].typ = %q, want []byte", wc.params[0].typ)
}
}
func TestFuzzWideCopy(t *testing.T) {
k := loadBasic(t)
gt, err := GroundTruth("../testdata/verify/basic_amd64.s")
if err != nil {
t.Fatalf("GroundTruth: %v", err)
}
goCode, ok := gt["wideCopy"]
if !ok {
t.Skip("wideCopy not in ground truth")
}
sig := funcSig{
name: "wideCopy",
params: []param{
{name: "dst", typ: "[]byte"},
{name: "src", typ: "[]byte"},
},
}
res := k.FuzzFunc("wideCopy", sig, goCode, 200, 42)
if !res.OK() {
t.Errorf("wideCopy fuzz: %s", res)
}
}
func TestParseFuncSig(t *testing.T) {
tests := []struct {
comment string
name string
nParams int
}{
{"// func add(a int64, b int64) int64", "add", 2},
{"// func wideCopy(dst []byte, src []byte)", "wideCopy", 2},
{"// func analyzeO1RangeAVX2(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool)", "analyzeO1RangeAVX2", 3},
{"// not a func", "", 0},
}
for _, tt := range tests {
sig, ok := parseFuncSig(tt.comment)
if tt.name == "" {
if ok {
t.Errorf("parseFuncSig(%q): expected not ok", tt.comment)
}
continue
}
if !ok {
t.Errorf("parseFuncSig(%q): expected ok", tt.comment)
continue
}
if sig.name != tt.name {
t.Errorf("parseFuncSig(%q).name = %q, want %q", tt.comment, sig.name, tt.name)
}
if len(sig.params) != tt.nParams {
t.Errorf("parseFuncSig(%q): %d params, want %d", tt.comment, len(sig.params), tt.nParams)
}
}
}
+173
View File
@@ -0,0 +1,173 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"bytes"
"encoding/binary"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)
// GroundTruth assembles the given .s file with the Go toolchain's own
// assembler and returns the machine code bytes for each TEXT function,
// keyed by the function's short name (the part after the middle dot).
// This is the universal oracle: any file that `go tool asm` accepts can
// be verified, with no hand-written reference.
func GroundTruth(path string) (map[string][]byte, error) {
goroot := runtime.GOROOT()
asmBin := filepath.Join(goroot, "pkg", "tool", runtime.GOOS+"_"+runtime.GOARCH, "asm")
if _, err := os.Stat(asmBin); err != nil {
return nil, fmt.Errorf("verify: go tool asm not found at %s: %w", asmBin, err)
}
includeDir := filepath.Join(goroot, "pkg", "include")
// Create a temp file for the object output.
tmpDir, err := os.MkdirTemp("", "gasm-verify-*")
if err != nil {
return nil, fmt.Errorf("verify: tempdir: %w", err)
}
defer os.RemoveAll(tmpDir)
objPath := filepath.Join(tmpDir, "out.o")
// Derive a package name from the file name (the assembler needs -p).
base := filepath.Base(path)
pkg := strings.TrimSuffix(base, ".s")
pkg = strings.TrimSuffix(pkg, "_amd64")
cmd := exec.Command(asmBin, "-I", includeDir, "-p", pkg, "-o", objPath, path)
if out, err := cmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("verify: go tool asm: %w\n%s", err, out)
}
objData, err := os.ReadFile(objPath)
if err != nil {
return nil, fmt.Errorf("verify: read object: %w", err)
}
return extractGOOBJCode(objData)
}
// GOOBJ block indices (cmd/internal/goobj).
const (
blkAutolib = iota
blkPkgIdx
blkFile
blkSymdef
blkHashed64def
blkHasheddef
blkNonpkgdef
blkNonpkgref
blkRefFlags
blkHash64
blkHash
blkRelocIdx
blkAuxIdx
blkDataIdx
blkReloc
blkAux
blkData
blkRefName
blkEnd
)
const goobjMagic = "\x00go120ld"
// extractGOOBJCode parses a GOOBJ payload and returns the code bytes for
// each non-package STEXT symbol (the functions).
func extractGOOBJCode(data []byte) (map[string][]byte, error) {
// Find the GOOBJ header (after the "go object ..." preamble).
i := bytes.Index(data, []byte(goobjMagic))
if i < 0 {
return nil, fmt.Errorf("verify: no GOOBJ magic in object file")
}
b := data[i:]
le := binary.LittleEndian
// Read block offsets (20 bytes into the header: 4 magic + 8 go version
// + 8 experiment = 20, then blkEnd+1 uint32 offsets).
var offs [blkEnd + 1]uint32
for j := 0; j <= blkEnd; j++ {
offs[j] = le.Uint32(b[20+4*j:])
}
blk := func(idx int) []byte { return b[offs[idx]:offs[idx+1]] }
// Parse non-package symbol definitions (blkNonpkgdef): each entry is
// 21 bytes: [nameLen:4][nameOff:4][abi:2][type:1][flag:1][flag2:1][size:4][align:4].
const symSize = 21
nonpkg := blk(blkNonpkgdef)
nSyms := len(nonpkg) / symSize
// Data index (blkDataIdx): one uint32 per defined symbol across ALL
// definition blocks (blkSymdef + blkHashed64def + blkHasheddef +
// blkNonpkgdef), in that order. We need the offset for the nonpkg
// symbols, which come last.
dataIdx := blk(blkDataIdx)
dataBlk := blk(blkData)
// Count symbols in the preceding definition blocks.
preceding := 0
for _, bi := range []int{blkSymdef, blkHashed64def, blkHasheddef} {
preceding += len(blk(bi)) / symSize
}
// Symbol name offsets in the GOOBJ symbol table are absolute byte
// offsets from the start of the GOOBJ payload (the magic).
readStr := func(off, ln uint32) string {
if int(off+ln) > len(b) {
return ""
}
return string(b[off : off+ln])
}
result := make(map[string][]byte)
const kindSTEXT = 1
for s := 0; s < nSyms; s++ {
x := nonpkg[s*symSize:]
nameLen := le.Uint32(x[0:])
nameOff := le.Uint32(x[4:])
typ := x[10]
size := le.Uint32(x[13:])
if typ != kindSTEXT || size == 0 {
continue
}
name := readStr(nameOff, nameLen)
// Strip the package prefix (everything up to and including the
// last middle dot or period-dot).
name = stripPkg(name)
// Data offset from the index (nonpkg symbols follow the preceding blocks).
diIdx := preceding + s
if (diIdx+1)*4 > len(dataIdx) {
continue
}
dOff := le.Uint32(dataIdx[diIdx*4:])
if int(dOff+size) > len(dataBlk) {
continue
}
code := make([]byte, size)
copy(code, dataBlk[dOff:dOff+size])
result[name] = code
}
return result, nil
}
// stripPkg removes the package path prefix from a symbol name, leaving
// just the function name. "pkg/path·FuncName" → "FuncName".
func stripPkg(name string) string {
if i := strings.LastIndex(name, "\u00B7"); i >= 0 {
return name[i+len("\u00B7"):]
}
if i := strings.LastIndex(name, "\"."); i >= 0 {
return name[i+2:]
}
if i := strings.LastIndex(name, "."); i >= 0 {
return name[i+1:]
}
return name
}
+77
View File
@@ -0,0 +1,77 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"testing"
)
func TestGroundTruthBasic(t *testing.T) {
// Use the simple test kernel — it assembles with go tool asm.
gt, err := GroundTruth("../testdata/verify/basic_amd64.s")
if err != nil {
t.Fatalf("GroundTruth: %v", err)
}
if len(gt) == 0 {
t.Fatal("no functions extracted from ground truth")
}
// The "add" function should be present and non-empty.
code, ok := gt["add"]
if !ok {
t.Fatalf("function 'add' not found in ground truth; got: %v", keys(gt))
}
if len(code) == 0 {
t.Fatal("add: zero-length code")
}
t.Logf("ground truth functions: %v", keys(gt))
}
func TestGroundTruthComparison(t *testing.T) {
// Assemble with gasm and compare against go tool asm.
k, err := Load("../testdata/verify/basic_amd64.s")
if err != nil {
t.Fatalf("Load: %v", err)
}
defer k.Close()
gt, err := GroundTruth("../testdata/verify/basic_amd64.s")
if err != nil {
t.Fatalf("GroundTruth: %v", err)
}
for _, name := range k.FuncNames() {
fl, _ := k.Func(name)
gasmCode := k.Image().Code[fl.Offset : fl.Offset+fl.Size]
goCode, ok := gt[name]
if !ok {
t.Errorf("%s: not in ground truth", name)
continue
}
if len(gasmCode) != len(goCode) {
t.Errorf("%s: size mismatch: gasm=%d go=%d", name, len(gasmCode), len(goCode))
continue
}
for i := range gasmCode {
if gasmCode[i] != goCode[i] {
t.Errorf("%s: byte %d differs: gasm=%02x go=%02x", name, i, gasmCode[i], goCode[i])
break
}
}
}
}
func TestGroundTruthBadFile(t *testing.T) {
_, err := GroundTruth("/nonexistent/file_amd64.s")
if err == nil {
t.Fatal("expected error for nonexistent file")
}
}
func keys(m map[string][]byte) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
+88
View File
@@ -0,0 +1,88 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
// Package verify provides the dynamic-analysis substrate for gasm: it
// JIT-assembles Plan 9 amd64 kernels into executable memory and calls them
// directly, enabling differential testing against portable Go references,
// runtime ABI checks and basic-block coverage profiling.
//
// The execution model is pure Go (stdlib only): machine code is mapped with
// syscall.Mmap and invoked through an assembly trampoline that switches to a
// prepared ABI0 stack. No cgo, no external toolchain.
package verify
import (
"encoding/binary"
"fmt"
"syscall"
"unsafe"
)
// Executable maps a copy of code into a read-execute memory region suitable
// for direct invocation. The mapping is anonymous and private; the original
// slice is not retained. Call Unmap to release the region.
type Executable struct {
addr uintptr // base address of the mapping
size int
mem []byte // the mmap'd slice (for Unmap)
}
// Map copies code into a freshly allocated RX region and returns it.
// The mapping is PROT_READ|PROT_EXEC; writes are not permitted after the
// copy, matching W^X policy.
func Map(code []byte) (*Executable, error) {
size := len(code)
if size == 0 {
return nil, fmt.Errorf("verify: cannot map zero-length code")
}
// Round up to the page size.
const pageSize = 4096
mapSize := (size + pageSize - 1) &^ (pageSize - 1)
mem, err := syscall.Mmap(-1, 0, mapSize,
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON)
if err != nil {
return nil, fmt.Errorf("verify: mmap: %w", err)
}
copy(mem, code)
// Remove write permission (W^X).
if err := syscall.Mprotect(mem, syscall.PROT_READ|syscall.PROT_EXEC); err != nil {
syscall.Munmap(mem)
return nil, fmt.Errorf("verify: mprotect: %w", err)
}
return &Executable{
addr: uintptr(unsafe.Pointer(&mem[0])),
size: size,
mem: mem,
}, nil
}
// Unmap releases the executable region.
func (e *Executable) Unmap() {
if e.mem != nil {
syscall.Munmap(e.mem)
e.mem = nil
}
}
// FuncAddr returns the absolute address of a function at the given offset
// within the mapped image.
func (e *Executable) FuncAddr(offset int) uintptr {
return e.addr + uintptr(offset)
}
// PutUint64 writes v into buf at byte offset off (little-endian).
func PutUint64(buf []byte, off int, v uint64) {
binary.LittleEndian.PutUint64(buf[off:off+8], v)
}
// GetUint64 reads a little-endian uint64 from buf at byte offset off.
func GetUint64(buf []byte, off int) uint64 {
return binary.LittleEndian.Uint64(buf[off : off+8])
}
// PutPtr writes a pointer value into buf at byte offset off.
func PutPtr(buf []byte, off int, p unsafe.Pointer) {
binary.LittleEndian.PutUint64(buf[off:off+8], uint64(uintptr(p)))
}
+215
View File
@@ -0,0 +1,215 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"bytes"
"testing"
"unsafe"
)
func loadBasic(t *testing.T) *Kernel {
t.Helper()
k, err := Load("../testdata/verify/basic_amd64.s")
if err != nil {
t.Fatalf("Load: %v", err)
}
t.Cleanup(k.Close)
return k
}
func TestJITAdd(t *testing.T) {
k := loadBasic(t)
tests := []struct {
a, b, want int64
}{
{0, 0, 0},
{1, 2, 3},
{-1, 1, 0},
{1 << 62, 1 << 62, -9223372036854775808}, // overflow wraps (MinInt64)
{-100, -200, -300},
}
for _, tt := range tests {
args := make([]byte, 24)
PutUint64(args, 0, uint64(tt.a))
PutUint64(args, 8, uint64(tt.b))
out, err := k.CallFunc("add", args)
if err != nil {
t.Fatalf("CallFunc(add, %d, %d): %v", tt.a, tt.b, err)
}
got := int64(GetUint64(out, 16))
if got != tt.want {
t.Errorf("add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
}
}
}
func TestJITSum(t *testing.T) {
k := loadBasic(t)
tests := []struct {
data []int64
want int64
}{
{nil, 0},
{[]int64{1}, 1},
{[]int64{1, 2, 3, 4, 5}, 15},
{[]int64{-10, 20, -30, 40}, 20},
}
for _, tt := range tests {
args := make([]byte, 32)
if len(tt.data) > 0 {
PutPtr(args, 0, unsafe.Pointer(&tt.data[0]))
}
PutUint64(args, 8, uint64(len(tt.data)))
PutUint64(args, 16, uint64(cap(tt.data)))
out, err := k.CallFunc("sum", args)
if err != nil {
t.Fatalf("CallFunc(sum, %v): %v", tt.data, err)
}
got := int64(GetUint64(out, 24))
if got != tt.want {
t.Errorf("sum(%v) = %d, want %d", tt.data, got, tt.want)
}
}
}
func TestJITWideCopy(t *testing.T) {
k := loadBasic(t)
tests := []struct {
name string
n int
}{
{"empty", 0},
{"tiny", 7},
{"exact32", 32},
{"overlap_range", 48},
{"exact64", 64},
{"unaligned", 45},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
src := make([]byte, tt.n)
for i := range src {
src[i] = byte(i * 7)
}
dst := make([]byte, tt.n)
args := make([]byte, 48)
if tt.n > 0 {
PutPtr(args, 0, unsafe.Pointer(&dst[0]))
PutPtr(args, 24, unsafe.Pointer(&src[0]))
}
PutUint64(args, 8, uint64(tt.n)) // dst_len
PutUint64(args, 16, uint64(tt.n)) // dst_cap
PutUint64(args, 32, uint64(tt.n)) // src_len
PutUint64(args, 40, uint64(tt.n)) // src_cap
_, err := k.CallFunc("wideCopy", args)
if err != nil {
t.Fatalf("CallFunc(wideCopy): %v", err)
}
if !bytes.Equal(dst, src) {
t.Errorf("wideCopy: dst ≠ src\n got %x\n want %x", dst, src)
}
})
}
}
func TestKernelFuncNames(t *testing.T) {
k := loadBasic(t)
names := k.FuncNames()
want := []string{"add", "sum", "wideCopy"}
if len(names) != len(want) {
t.Fatalf("FuncNames() = %v, want %v", names, want)
}
for i, n := range names {
if n != want[i] {
t.Errorf("FuncNames()[%d] = %q, want %q", i, n, want[i])
}
}
}
func TestKernelFuncNotFound(t *testing.T) {
k := loadBasic(t)
_, err := k.CallFunc("nonexistent", make([]byte, 8))
if err == nil {
t.Fatal("expected error for nonexistent function")
}
}
func TestKernelArgTooSmall(t *testing.T) {
k := loadBasic(t)
_, err := k.CallFunc("add", make([]byte, 8)) // needs 24
if err == nil {
t.Fatal("expected error for too-small arg block")
}
}
func TestMapZeroLength(t *testing.T) {
_, err := Map(nil)
if err == nil {
t.Fatal("expected error for zero-length code")
}
}
func TestLoadSourceError(t *testing.T) {
_, err := LoadSource("bad.s", "TEXT ·f(SB), NOSPLIT\n\tBADINSTRUCTION\n")
// The parser may or may not error on unknown instructions (it's
// error-tolerant), but the assembler will reject it.
if err == nil {
t.Log("LoadSource succeeded unexpectedly (parser is error-tolerant)")
}
}
func TestLoadSourceParseError(t *testing.T) {
// A completely invalid file that the parser rejects.
_, err := LoadSource("empty.s", "")
if err != nil {
t.Logf("expected: %v", err)
}
}
func TestFuncLookup(t *testing.T) {
k := loadBasic(t)
fl, err := k.Func("add")
if err != nil {
t.Fatalf("Func(add): %v", err)
}
if fl.Name != "add" {
t.Errorf("Func(add).Name = %q, want %q", fl.Name, "add")
}
if fl.Args != 24 {
t.Errorf("Func(add).Args = %d, want 24", fl.Args)
}
_, err = k.Func("nonexistent")
if err == nil {
t.Fatal("expected error for nonexistent function")
}
}
func TestABIReportString(t *testing.T) {
r := ABIReport{}
if r.String() != "ABI clean" {
t.Errorf("clean report = %q", r.String())
}
r.BPClobbered = true
if r.OK() {
t.Error("expected not OK with BP clobbered")
}
s := r.String()
if s == "ABI clean" {
t.Error("expected violation string, got clean")
}
r.R14Clobbered = true
r.RedZoneHit = true
s = r.String()
if s == "ABI clean" {
t.Error("expected violation string for all flags")
}
}
+160
View File
@@ -0,0 +1,160 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"bytes"
"os"
"testing"
"unsafe"
)
// lz4KernelPath is the sibling repository's AVX2 kernel, used for
// integration testing. The test is skipped when the file is absent
// (e.g. in CI without the sibling checkout).
const lz4KernelPath = "../../go-libraries/go-lz4/avx2_amd64.s"
func loadLZ4Kernel(t *testing.T) *Kernel {
t.Helper()
if _, err := os.Stat(lz4KernelPath); err != nil {
t.Skipf("sibling kernel not available: %v", err)
}
k, err := Load(lz4KernelPath)
if err != nil {
t.Fatalf("Load(%s): %v", lz4KernelPath, err)
}
t.Cleanup(k.Close)
return k
}
// callDecodeBlockAVX2 invokes the JIT-assembled decodeBlockAVX2 with the
// given src and dst buffers, returning (n, code).
func callDecodeBlockAVX2(t *testing.T, k *Kernel, src, dst []byte) (int, int) {
t.Helper()
args := make([]byte, 64)
if len(src) > 0 {
PutPtr(args, 0, unsafe.Pointer(&src[0]))
}
PutUint64(args, 8, uint64(len(src)))
PutUint64(args, 16, uint64(cap(src)))
if len(dst) > 0 {
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
}
PutUint64(args, 32, uint64(len(dst)))
PutUint64(args, 40, uint64(cap(dst)))
out, err := k.CallFunc("decodeBlockAVX2", args)
if err != nil {
t.Fatalf("CallFunc(decodeBlockAVX2): %v", err)
}
return int(GetUint64(out, 48)), int(GetUint64(out, 56))
}
func TestLZ4DecodeKnownAnswers(t *testing.T) {
k := loadLZ4Kernel(t)
tests := []struct {
name string
src []byte
dstSize int
wantDst []byte
wantN int
wantCode int
}{
{
name: "literals_only",
src: []byte{0x50, 'H', 'e', 'l', 'l', 'o'},
dstSize: 16,
wantDst: []byte("Hello"),
wantN: 5,
wantCode: 0,
},
{
name: "literals_and_match",
src: []byte{0x54, 'A', 'A', 'A', 'A', 'A', 0x05, 0x00, 0x30, 'B', 'B', 'B'},
dstSize: 32,
wantDst: []byte("AAAAAAAAAAAAABBB"),
wantN: 16,
wantCode: 0,
},
{
name: "overlapping_match",
// 1 literal 'X', then match offset=1 length=4+4=8 → "XXXXXXXXX",
// then final 1 literal 'Y'.
src: []byte{0x14, 'X', 0x01, 0x00, 0x10, 'Y'},
dstSize: 16,
wantDst: []byte("XXXXXXXXXY"),
wantN: 10,
wantCode: 0,
},
{
name: "malformed_truncated",
src: []byte{0x50, 'H', 'e'}, // claims 5 literals, has 2
dstSize: 16,
wantN: 0,
wantCode: 1,
},
{
name: "zero_offset",
src: []byte{0x14, 'X', 0x00, 0x00},
dstSize: 16,
wantN: 0,
wantCode: 2,
},
{
name: "empty_token",
src: []byte{0x00}, // 0 literals, end of block
dstSize: 16,
wantDst: nil,
wantN: 0,
wantCode: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dst := make([]byte, tt.dstSize)
n, code := callDecodeBlockAVX2(t, k, tt.src, dst)
if n != tt.wantN || code != tt.wantCode {
t.Fatalf("decodeBlockAVX2: got (n=%d, code=%d), want (n=%d, code=%d)",
n, code, tt.wantN, tt.wantCode)
}
if tt.wantCode == 0 && tt.wantDst != nil {
if !bytes.Equal(dst[:n], tt.wantDst) {
t.Errorf("output mismatch:\n got %q\n want %q", dst[:n], tt.wantDst)
}
}
})
}
}
func TestLZ4WideCopyAVX2(t *testing.T) {
k := loadLZ4Kernel(t)
sizes := []int{0, 1, 15, 16, 31, 32, 33, 63, 64, 100, 256, 1024}
for _, n := range sizes {
src := make([]byte, n)
for i := range src {
src[i] = byte(i*13 + 7)
}
dst := make([]byte, n)
args := make([]byte, 48)
if n > 0 {
PutPtr(args, 0, unsafe.Pointer(&dst[0]))
PutPtr(args, 24, unsafe.Pointer(&src[0]))
}
PutUint64(args, 8, uint64(n))
PutUint64(args, 16, uint64(n))
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(n))
_, err := k.CallFunc("wideCopyAVX2", args)
if err != nil {
t.Fatalf("wideCopyAVX2(n=%d): %v", n, err)
}
if !bytes.Equal(dst, src) {
t.Errorf("wideCopyAVX2(n=%d): output mismatch", n)
}
}
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
#include "textflag.h"
// ABI0 JIT trampoline. enterJIT switches from the Go stack to a prepared
// stack and jumps to the assembled function; when the function RETs, control
// lands in leaveJIT, which restores the Go stack and returns to the Go caller.
//
// The prepared stack must begin with the address of leaveJIT (the return
// address the JIT function will pop), followed by the function's ABI0
// argument area.
//
// Single-threaded: savedSP is a package global, so only one JIT call may be
// in flight at a time. gasm verify runs sequentially.
// func enterJIT(fn uintptr, stack uintptr)
// Switches to the prepared stack and jumps to fn. Does not return normally;
// the JIT function's RET transfers control to leaveJIT.
TEXT ·enterJIT(SB), NOSPLIT, $0-16
MOVQ fn+0(FP), AX // target function address (before SP switch)
MOVQ SP, ·savedSP(SB) // preserve the Go stack pointer
MOVQ stack+8(FP), SP // switch to the prepared stack
JMP AX
// func leaveJIT()
// Restores the Go stack pointer and returns to enterJIT's caller.
TEXT ·leaveJIT(SB), NOSPLIT, $0-0
MOVQ ·savedSP(SB), SP
RET
+127
View File
@@ -0,0 +1,127 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"fmt"
"os"
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
)
// Kernel is a JIT-loaded assembly image ready for direct invocation.
// It wraps an executable memory mapping and the function layout metadata
// needed to marshal ABI0 calls.
type Kernel struct {
exec *Executable
img *asm.Image
funcs map[string]int // function name → index into img.Funcs
}
// Load parses, assembles and maps a .s file into executable memory.
// The returned Kernel is ready for Call. The caller must call Close to
// release the mapping.
func Load(path string) (*Kernel, error) {
src, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("verify: %w", err)
}
return LoadSource(path, string(src))
}
// LoadSource parses, assembles and maps assembly source into executable memory.
func LoadSource(filename, src string) (*Kernel, error) {
file, errs := parser.Parse(filename, src)
if len(errs) > 0 {
return nil, fmt.Errorf("verify: parse %s: %v", filename, errs[0])
}
return LoadAST(file)
}
// LoadAST assembles a parsed AST file and maps the result into executable
// memory.
func LoadAST(file *ast.File) (*Kernel, error) {
img, err := asm.AssembleFile(file)
if err != nil {
return nil, fmt.Errorf("verify: assemble: %w", err)
}
if len(img.Externals) > 0 {
return nil, fmt.Errorf("verify: unresolved external symbols: %v", img.Externals)
}
code := img.Bytes()
exec, err := Map(code)
if err != nil {
return nil, err
}
funcs := make(map[string]int, len(img.Funcs))
for i, f := range img.Funcs {
funcs[f.Name] = i
}
return &Kernel{exec: exec, img: img, funcs: funcs}, nil
}
// Func returns the layout metadata for the named function.
func (k *Kernel) Func(name string) (asm.FuncLayout, error) {
idx, ok := k.funcs[name]
if !ok {
return asm.FuncLayout{}, fmt.Errorf("verify: function %q not found", name)
}
return k.img.Funcs[idx], nil
}
// FuncNames returns the names of all functions in the kernel, in source order.
func (k *Kernel) FuncNames() []string {
names := make([]string, len(k.img.Funcs))
for i, f := range k.img.Funcs {
names[i] = f.Name
}
return names
}
// CallFunc invokes the named function with the given ABI0 argument block.
// The arg block is the raw bytes of the function's argument/result area
// (as declared by the TEXT $frame-args suffix). Returns the arg block
// after the call (with any results written back by the function).
func (k *Kernel) CallFunc(name string, args []byte) ([]byte, error) {
idx, ok := k.funcs[name]
if !ok {
return nil, fmt.Errorf("verify: function %q not found", name)
}
fl := k.img.Funcs[idx]
if len(args) < fl.Args {
return nil, fmt.Errorf("verify: %s: arg block too small: got %d, need %d", name, len(args), fl.Args)
}
fnAddr := k.exec.FuncAddr(fl.Offset)
return Call(fnAddr, args)
}
// CallFuncChecked invokes the named function with ABI sentinels and a
// red-zone canary, returning the argument block and an ABIReport that
// records any callee-saved register or red-zone violations.
func (k *Kernel) CallFuncChecked(name string, args []byte) ([]byte, ABIReport, error) {
idx, ok := k.funcs[name]
if !ok {
return nil, ABIReport{}, fmt.Errorf("verify: function %q not found", name)
}
fl := k.img.Funcs[idx]
if len(args) < fl.Args {
return nil, ABIReport{}, fmt.Errorf("verify: %s: arg block too small: got %d, need %d", name, len(args), fl.Args)
}
fnAddr := k.exec.FuncAddr(fl.Offset)
return CallChecked(fnAddr, args)
}
// Close releases the executable mapping.
func (k *Kernel) Close() {
if k.exec != nil {
k.exec.Unmap()
}
}
// Image returns the assembled image (code + data + metadata).
func (k *Kernel) Image() *asm.Image {
return k.img
}