Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a45b66139 | ||
|
|
382efe538a | ||
|
|
f8d28a42ba | ||
|
|
51a2854d7f | ||
|
|
c234c3dd5b | ||
|
|
9262990ce5 | ||
|
|
d9f6167a4d | ||
|
|
f5088c52fc |
+130
-3
@@ -8,6 +8,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -29,7 +30,7 @@ import (
|
||||
|
||||
// version is the release version, stamped at build time via
|
||||
// -ldflags "-X main.version=…" (defaulting to the current release).
|
||||
var version = "0.18.0"
|
||||
var version = "0.26.0"
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
@@ -472,7 +473,7 @@ requires -p, the package path, and the installed Go toolchain).
|
||||
}
|
||||
|
||||
func cmdVerify(args []string) int {
|
||||
fs := newCommand("verify", "gasm verify <file.s>", `
|
||||
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
|
||||
@@ -481,11 +482,21 @@ 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] <file.s>")
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] <file.s>")
|
||||
return 2
|
||||
}
|
||||
path := fs.Arg(0)
|
||||
@@ -504,6 +515,98 @@ that tolerate nil pointers and zero lengths in their arguments.
|
||||
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 := ""
|
||||
@@ -511,6 +614,16 @@ that tolerate nil pointers and zero lengths in their arguments.
|
||||
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)
|
||||
@@ -521,6 +634,20 @@ that tolerate nil pointers and zero lengths in their arguments.
|
||||
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
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
|
||||
|
||||
version := "0.18.0"
|
||||
version := "0.26.0"
|
||||
|
||||
default:
|
||||
@just --list
|
||||
|
||||
Vendored
+28
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -157,3 +157,59 @@ func TestMapZeroLength(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +98,30 @@ func (k *Kernel) CallFunc(name string, args []byte) ([]byte, error) {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user