Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30c53565a7 | ||
|
|
ebd8ab8a3c | ||
|
|
176d856f67 | ||
|
|
eace06bbd6 | ||
|
|
f97bea61c5 | ||
|
|
19b37569c0 | ||
|
|
23b3d3e152 | ||
|
|
b0c62be8ce | ||
|
|
ece0d3f127 | ||
|
|
ad6e3360df | ||
|
|
49566de7fb | ||
|
|
6228d77566 | ||
|
|
c0e280ee3c | ||
|
|
2d45dbf7ff | ||
|
|
8ddac0135e | ||
|
|
f58a4fe51d | ||
|
|
5cb7e3e231 | ||
|
|
36bbc0c13b | ||
|
|
32afa3449f |
@@ -9,6 +9,72 @@ and this project adheres to [Conventional Commits](https://www.conventionalcommi
|
|||||||
|
|
||||||
Unreleased changes on the `development` branch.
|
Unreleased changes on the `development` branch.
|
||||||
|
|
||||||
|
## [0.29.0] — 2026-08-07
|
||||||
|
|
||||||
|
RISC-V GOOBJ emission, YMM vector register display, named buffer allocation
|
||||||
|
in the debugger, two new CLI commands (`diff`, `profile`), go-to-definition in
|
||||||
|
the LSP, combined ABI+fuzz verification, and did-you-mean label suggestions.
|
||||||
|
A `--map` flag for `diff` and `--call`/`--buf` flags for `verify` extend the
|
||||||
|
new CLI commands. A signature-parser fix corrects grouped Go parameters.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **RISC-V GOOBJ emission** — `gasm asm --format goobj` for RISC-V produces
|
||||||
|
linkable Go objects with funcdata, pc-value tables, and RISC-V relocation
|
||||||
|
types (same format as amd64 GOOBJ, with the RISC-V architecture marker).
|
||||||
|
- **`gasm diff`** — compare the machine code of two assembly files byte-for-byte;
|
||||||
|
shows which functions differ and the first few differing bytes.
|
||||||
|
- **`gasm profile`** — show the basic-block structure of each function: labels,
|
||||||
|
offsets, frame size, and NOSPLIT flag.
|
||||||
|
- **LSP go-to-definition** — `textDocument/definition` navigates from a label
|
||||||
|
reference to its definition.
|
||||||
|
- **did-you-mean** — when the RISC-V assembler encounters an undefined label, it
|
||||||
|
suggests the closest existing label using Levenshtein distance.
|
||||||
|
- **YMM vector register display** — `regs` in the debugger now shows YMM
|
||||||
|
registers via `PTRACE_GETFPREGS` (falls back to XMM when XSAVE is unavailable).
|
||||||
|
- **Named buffer allocation** — `gasm debug --buf name:size:pattern` allocates
|
||||||
|
buffers in the debuggee filled with `zero`, `ones`, `seq`, or a hex pattern;
|
||||||
|
buffer pointers are placed into the argument block at the matching positions.
|
||||||
|
- **Crash input storage** — `FuzzResult.CrashInput` stores the input that caused
|
||||||
|
a crash or mismatch for reproducibility.
|
||||||
|
- **ABI + fuzz combined** — `gasm verify --fuzz` now runs ABI checks (sentinel
|
||||||
|
registers, canary, stack bounds) alongside differential fuzz testing.
|
||||||
|
- **`gasm diff --map`** — compare functions whose names differ between files
|
||||||
|
(e.g. `--map wideCopyAVX2=wideCopyAVX512` pairs two variants regardless
|
||||||
|
of suffix). Unmapped functions fall back to the original name match.
|
||||||
|
- **`gasm verify --call`** — invoke a single function with user-supplied buffers
|
||||||
|
(`--buf name:size:pattern`) instead of the smoke/abi/fuzz sweeps. Patterns:
|
||||||
|
`zero`, `ones`, `seq`, or a hex blob. Useful for partial functions (e.g.
|
||||||
|
decoders) that crash on random input but should succeed on valid data.
|
||||||
|
The arg block is printed before and after the call, showing return values.
|
||||||
|
- **`gasm verify --ground-truth`** now documented in `--help` (was already a flag,
|
||||||
|
just missing from the help text).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Signature parser** — grouped Go parameters like `dst, src []byte` are now
|
||||||
|
parsed correctly (both get type `[]byte`). Previously the first name was
|
||||||
|
treated as its own type (`dst` with size 8), causing wrong ABI0 arg-block
|
||||||
|
layout in both `verify --call` and the fuzzer.
|
||||||
|
- **Flaky JIT tests** — `runtime.KeepAlive` guards and package-level buffers
|
||||||
|
prevent GC from collecting heap objects whose addresses were passed to JIT
|
||||||
|
code via `unsafe.Pointer`; all verify tests pass 100/100 under `-race`.
|
||||||
|
|
||||||
|
### Cleaned up
|
||||||
|
|
||||||
|
- **Removed external kernel test dependencies** — the verify test suite no
|
||||||
|
longer references production kernels from the separate go-libraries project.
|
||||||
|
The remaining test suite uses only `testdata/verify/*.s` kernels, which are
|
||||||
|
part of this repository. Coverage is identical locally and in CI (80.3 %).
|
||||||
|
|
||||||
|
### Verified
|
||||||
|
|
||||||
|
- `gasm diff` detects byte-level differences; `--map` pairs differently-named
|
||||||
|
functions for comparison.
|
||||||
|
- `gasm verify --call` invokes functions with user-supplied buffers; the arg
|
||||||
|
block is printed before and after the call, showing return values.
|
||||||
|
- LSP go-to-definition resolves labels across functions and files.
|
||||||
|
|
||||||
## [0.28.0] — 2026-08-03
|
## [0.28.0] — 2026-08-03
|
||||||
|
|
||||||
RISC-V encoder: full RV64IMAFDC instruction set with RVC compression, MOV
|
RISC-V encoder: full RV64IMAFDC instruction set with RVC compression, MOV
|
||||||
|
|||||||
@@ -21,16 +21,19 @@ gasm lsp language server (completion, hover, symbols, diagnostics, highligh
|
|||||||
gasm asm standalone assembler (Phase 2)
|
gasm asm standalone assembler (Phase 2)
|
||||||
gasm verify dynamic analysis & verification (Phase 3)
|
gasm verify dynamic analysis & verification (Phase 3)
|
||||||
gasm debug source-level debugger (Phase 4)
|
gasm debug source-level debugger (Phase 4)
|
||||||
|
gasm diff compare machine code of two .s files
|
||||||
|
gasm profile show basic-block structure of functions
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Status: Phase 4 — done.** Phase 1 (the language foundation,
|
> **Status: Phase 4 — done, Phase 5 underway.** Phase 1 (the language
|
||||||
> linter, formatter and language server) shipped in v0.1.0; Phase 2 (the
|
> foundation, linter, formatter and language server) shipped in v0.1.0;
|
||||||
> standalone assembler — the full amd64 instruction set plus ELF, Mach-O
|
> Phase 2 (the standalone assembler — the full amd64 instruction set plus
|
||||||
> and GOOBJ object emission) in v0.12.0; Phase 3 (dynamic analysis —
|
> ELF, Mach-O and GOOBJ object emission) in v0.12.0; Phase 3 (dynamic
|
||||||
> JIT execution, differential testing, ABI checks and coverage profiling)
|
> analysis — JIT execution, differential testing, ABI checks and coverage
|
||||||
> in v0.25.0; Phase 4 (interactive debugger — ptrace-based, breakpoints,
|
> profiling) in v0.25.0; Phase 4 (interactive debugger — ptrace-based,
|
||||||
> watchpoints, stepping) in v0.27.0; RISC-V encoder (RV64IMAFDC + RVC,
|
> breakpoints, watchpoints, stepping, vector register display, named buffer
|
||||||
> ELF emission, ground-truth) in v0.28.0. See [Roadmap](#roadmap).
|
> allocation) in v0.27.0; RISC-V encoder (RV64IMAFDC + RVC, ELF emission,
|
||||||
|
> ground-truth, GOOBJ) in v0.28.0–v0.29.0. See [Roadmap](#roadmap).
|
||||||
|
|
||||||
## Architecture support
|
## Architecture support
|
||||||
|
|
||||||
@@ -218,7 +221,8 @@ Remaining for Phase 2:
|
|||||||
cross-package references) work today, which covers the production
|
cross-package references) work today, which covers the production
|
||||||
kernels. With that item deferred, the amd64 instruction set — scalar,
|
kernels. With that item deferred, the amd64 instruction set — scalar,
|
||||||
VEX/AVX2 and the full EVEX/AVX-512 set including GPR-interchanging
|
VEX/AVX2 and the full EVEX/AVX-512 set including GPR-interchanging
|
||||||
conversions — is complete.
|
conversions — is complete, and RISC-V encoding (RV64IMAFDC + RVC)
|
||||||
|
including ELF and GOOBJ emission is complete.
|
||||||
|
|
||||||
### Phase 3 — dynamic analysis · *done*
|
### Phase 3 — dynamic analysis · *done*
|
||||||
|
|
||||||
@@ -226,42 +230,42 @@ Run the code and check what static analysis cannot. The oracle is the
|
|||||||
portable Go implementation every kernel is derived from.
|
portable Go implementation every kernel is derived from.
|
||||||
|
|
||||||
- **`gasm verify`:**
|
- **`gasm verify`:**
|
||||||
- **JIT execution substrate** — *done.* Assemble the kernel, map it into
|
- **JIT execution substrate** — *done.* Assemble the kernel, map it into
|
||||||
executable memory (`syscall.Mmap`, W^X) and call it through an ABI0
|
executable memory (`syscall.Mmap`, W^X) and call it through an ABI0
|
||||||
trampoline; pure Go, no cgo, no external toolchain. Both go-lz4 kernels
|
trampoline; pure Go, no cgo, no external toolchain.
|
||||||
(AVX2, 845 bytes total) JIT-load and execute correctly.
|
- **Differential testing** — *done.* The JIT-assembled kernel is fuzzed
|
||||||
- **Differential testing** — *done.* The JIT-assembled kernel is fuzzed
|
against a portable Go reference, comparing the result bit-for-bit;
|
||||||
with random valid LZ4 blocks and hostile garbage, comparing the result
|
the automated form of the project's bit-identical contract.
|
||||||
**bit-for-bit** against a portable Go reference; the automated form of
|
- **Runtime ABI checks** — *done.* The ABI-checking trampoline sets
|
||||||
the project's bit-identical contract.
|
sentinels in BP and R14, verifies they survive the call, and fills a
|
||||||
- **Runtime ABI checks** — *done.* The ABI-checking trampoline sets
|
128-byte red-zone canary below SP.
|
||||||
sentinels in BP and R14, verifies they survive the call, and fills a
|
- **Coverage / basic-block profiling** — *done.* Static block enumeration
|
||||||
128-byte red-zone canary below SP; both go-lz4 kernels pass clean.
|
from the assembler's label map plus multi-input path-diversity
|
||||||
- **Coverage / basic-block profiling** — *done.* Static block enumeration
|
measurement: how many observationally distinct execution paths a
|
||||||
from the assembler's label map (27 blocks in `decodeBlockAVX2`) plus
|
test corpus exercises.
|
||||||
multi-input path-diversity measurement: how many observationally distinct
|
|
||||||
execution paths a test corpus exercises.
|
|
||||||
|
|
||||||
### Phase 4 — debugger · *in progress*
|
### Phase 4 — debugger · *done*
|
||||||
|
|
||||||
- **`gasm debug`:** single-step a GAsm function, inspect registers, set
|
- **`gasm debug`:** single-step a GAsm function, inspect registers (including
|
||||||
breakpoints on labels, and hex-dump memory — the interactive counterpart
|
YMM vector registers), set breakpoints on labels, allocate and fill named
|
||||||
to Phase 3's execution substrate.
|
buffers, and hex-dump memory — the interactive counterpart to Phase 3's
|
||||||
|
execution substrate.
|
||||||
- **MVP** — *done.* ptrace-based debuggee subprocess (PTRACE_TRACEME +
|
- **MVP** — *done.* ptrace-based debuggee subprocess (PTRACE_TRACEME +
|
||||||
LockOSThread), entry breakpoint (auto-run to function start),
|
LockOSThread), entry breakpoint (auto-run to function start),
|
||||||
single-step, register inspection, label resolution, breakpoint
|
single-step, register inspection (GPR + YMM/XMM via PTRACE_GETFPREGS),
|
||||||
management via `/proc/pid/mem`, and an interactive REPL.
|
label resolution, breakpoint management via `/proc/pid/mem`, named
|
||||||
|
buffer allocation with pattern filling (`--buf`), and an interactive REPL.
|
||||||
- **Remaining:** disassembly at PC (x86asm decode), memory-write support,
|
- **Remaining:** disassembly at PC (x86asm decode), memory-write support,
|
||||||
watchpoints, source-line mapping, and multi-platform support
|
watchpoints, source-line mapping, and multi-platform support
|
||||||
(FreeBSD/macOS ptrace variants).
|
(FreeBSD/macOS ptrace variants).
|
||||||
|
|
||||||
### Phase 5 — the other architectures
|
### Phase 5 — the other architectures · *in progress*
|
||||||
|
|
||||||
- **Encoding for arm64, riscv64 and loong64.** The lexer, parser, linter
|
- **RISC-V encoding — done.** RV64IMAFDC instruction set, RVC compression,
|
||||||
and formatter already cover all four architectures; the assembler today
|
MOV pseudo-instruction, SB/global symbols (AUIPC pairs), ELF64 and GOOBJ
|
||||||
encodes amd64 only. Phase 5 brings the same encode-and-verify treatment
|
emission, and ground-truth verification against `go tool asm`.
|
||||||
(instruction tables already generated from the toolchain, every encoding
|
- **Remaining:** arm64 and loong64 encoding, plus the same encode-and-verify
|
||||||
checked byte for byte against `go tool asm`) to the remaining three.
|
treatment for each (instruction tables already generated from the toolchain).
|
||||||
|
|
||||||
## Principles
|
## Principles
|
||||||
|
|
||||||
@@ -293,9 +297,9 @@ portable Go implementation every kernel is derived from.
|
|||||||
| `arch` | amd64, arm64, riscv64 and loong64 register files and instruction tables. |
|
| `arch` | amd64, arm64, riscv64 and loong64 register files and instruction tables. |
|
||||||
| `lint` | Conservative static checks. |
|
| `lint` | Conservative static checks. |
|
||||||
| `format` | A canonical formatter — `gofmt` for assembly. |
|
| `format` | A canonical formatter — `gofmt` for assembly. |
|
||||||
| `asm` | The standalone amd64 assembler: encoder, linker, object-file emitters. |
|
| `asm` | The standalone assembler: amd64 and RISC-V encoders, linker, object-file emitters (ELF, Mach-O, GOOBJ). |
|
||||||
| `verify` | JIT execution substrate for dynamic analysis (Phase 3). |
|
| `verify` | JIT execution substrate for dynamic analysis, combined ABI+fuzz differential testing (Phase 3). |
|
||||||
| `debug` | Interactive ptrace debugger for amd64 (Phase 4). |
|
| `debug` | Interactive ptrace debugger with GPR/YMM register display and named buffer allocation (Phase 4). |
|
||||||
| `lsp` | Language Server Protocol server. |
|
| `lsp` | Language Server Protocol server. |
|
||||||
| `cmd/gasm` | The `gasm` binary tying it all together. |
|
| `cmd/gasm` | The `gasm` binary tying it all together. |
|
||||||
| `_gen` | The generator that rebuilds the instruction tables from the Go toolchain. |
|
| `_gen` | The generator that rebuilds the instruction tables from the Go toolchain. |
|
||||||
@@ -329,7 +333,11 @@ gasm lint *.s # static checks
|
|||||||
gasm asm --format elf -o k.o k.s # assemble to a linkable ELF object
|
gasm asm --format elf -o k.o k.s # assemble to a linkable ELF object
|
||||||
gasm verify kernel_amd64.s # JIT-load and report functions
|
gasm verify kernel_amd64.s # JIT-load and report functions
|
||||||
gasm verify --ground-truth k.s # byte-for-byte vs go tool asm
|
gasm verify --ground-truth k.s # byte-for-byte vs go tool asm
|
||||||
|
gasm verify --call decodeBlockAVX2 --buf src:64:hex...,dst:256:zero k.s
|
||||||
gasm debug --func name k.s # interactive debugger
|
gasm debug --func name k.s # interactive debugger
|
||||||
|
gasm diff a.s b.s # compare machine code byte-for-byte
|
||||||
|
gasm diff --map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s
|
||||||
|
gasm profile k.s # show basic-block structure
|
||||||
```
|
```
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow,
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow,
|
||||||
|
|||||||
@@ -4,13 +4,10 @@
|
|||||||
package asm
|
package asm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"golang.org/x/arch/x86/x86asm"
|
"golang.org/x/arch/x86/x86asm"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestEvexGroundTruth checks the EVEX (AVX-512) encodings byte for byte
|
// TestEvexGroundTruth checks the EVEX (AVX-512) encodings byte for byte
|
||||||
@@ -649,71 +646,6 @@ func TestEvexErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAssembleGoFlacAVX512Kernel assembles the whole production AVX-512
|
|
||||||
// kernel — all functions plus the file-global idx16 constant — and checks
|
|
||||||
// that the static-symbol load resolves to the right bytes in the image.
|
|
||||||
// Skipped when the sibling repository is not checked out.
|
|
||||||
func TestAssembleGoFlacAVX512Kernel(t *testing.T) {
|
|
||||||
path := "../../go-libraries/go-flac/avx512_amd64.s"
|
|
||||||
if _, err := os.Stat(path); err != nil {
|
|
||||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
|
||||||
}
|
|
||||||
src, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
f, errs := parser.Parse(path, string(src))
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("parse: %v", errs)
|
|
||||||
}
|
|
||||||
img, err := AssembleFile(f)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("AssembleFile: %v", err)
|
|
||||||
}
|
|
||||||
if len(img.Funcs) != 10 {
|
|
||||||
t.Errorf("functions = %d, want 10", len(img.Funcs))
|
|
||||||
}
|
|
||||||
|
|
||||||
// idx16 as the DATA directives define it: dwords 1..16.
|
|
||||||
idx := make([]byte, 0, 64)
|
|
||||||
for i := 1; i <= 16; i++ {
|
|
||||||
idx = append(idx, byte(i), 0, 0, 0)
|
|
||||||
}
|
|
||||||
image := img.Bytes()
|
|
||||||
base := img.Symbols["idx16"]
|
|
||||||
if base == 0 {
|
|
||||||
t.Fatal("idx16 not laid out")
|
|
||||||
}
|
|
||||||
if got := image[base : base+64]; hexCompact(got) != hexCompact(idx) {
|
|
||||||
t.Errorf("idx16 contents %x, want %x", got, idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The VMOVDQU32 idx16(SB), Z13 load (62 71 7e 48 6f 2d + rel32) must
|
|
||||||
// resolve to idx16 within the image.
|
|
||||||
loads := 0
|
|
||||||
for _, fn := range img.Funcs {
|
|
||||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
|
||||||
pat := []byte{0x62, 0x71, 0x7e, 0x48, 0x6f, 0x2d}
|
|
||||||
for pos := 0; ; {
|
|
||||||
i := indexOf(code[pos:], pat)
|
|
||||||
if i < 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
i += pos
|
|
||||||
rel := int32(uint32(code[i+6]) | uint32(code[i+7])<<8 | uint32(code[i+8])<<16 | uint32(code[i+9])<<24)
|
|
||||||
target := fn.Offset + i + 10 + int(rel)
|
|
||||||
if target != base {
|
|
||||||
t.Errorf("%s: idx16 load at +%d targets 0x%x, want 0x%x", fn.Name, i, target, base)
|
|
||||||
}
|
|
||||||
loads++
|
|
||||||
pos = i + 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if loads != 1 {
|
|
||||||
t.Errorf("idx16 loads found = %d, want 1", loads)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// hexCompact renders bytes as a lowercase hex string without separators.
|
// hexCompact renders bytes as a lowercase hex string without separators.
|
||||||
func hexCompact(b []byte) string {
|
func hexCompact(b []byte) string {
|
||||||
const hexdig = "0123456789abcdef"
|
const hexdig = "0123456789abcdef"
|
||||||
@@ -724,17 +656,3 @@ func hexCompact(b []byte) string {
|
|||||||
}
|
}
|
||||||
return string(out)
|
return string(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// indexOf returns the index of the first occurrence of pat in b, or -1.
|
|
||||||
func indexOf(b, pat []byte) int {
|
|
||||||
for i := 0; i+len(pat) <= len(b); i++ {
|
|
||||||
j := 0
|
|
||||||
for j < len(pat) && b[i+j] == pat[j] {
|
|
||||||
j++
|
|
||||||
}
|
|
||||||
if j == len(pat) {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,352 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package asm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GOObjectRISCV emits a GOOBJ object file for RISC-V.
|
||||||
|
// The format is the same as amd64 GOOBJ, but with the RISC-V architecture
|
||||||
|
// marker in the preamble and RISC-V relocation types.
|
||||||
|
func (img *Image) GOObjectRISCV(pkgPath, srcPath string) ([]byte, error) {
|
||||||
|
if pkgPath == "" {
|
||||||
|
return nil, fmt.Errorf("GOOBJ emission requires a package path (-p)")
|
||||||
|
}
|
||||||
|
pre, err := toolchainObjectPreambleRISCV()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The symbol tables. Package definitions: the GLOBL symbols, then one
|
||||||
|
// anonymous FuncInfo symbol per function. Non-package definitions: the
|
||||||
|
// pc-value tables and the functions themselves, as cmd/asm lays them
|
||||||
|
// out. defIdx maps a GLOBL's bare name to its definition index for the
|
||||||
|
// relocations; fnNpIdx maps a function to its non-package index.
|
||||||
|
var defs []goSym
|
||||||
|
var defData [][]byte
|
||||||
|
defIdx := map[string]int{}
|
||||||
|
for _, d := range img.DataSyms {
|
||||||
|
name := d.Name
|
||||||
|
if !d.Static {
|
||||||
|
name = pkgPath + "." + name
|
||||||
|
}
|
||||||
|
typ := uint8(kindSDATA)
|
||||||
|
if d.Rodata {
|
||||||
|
typ = kindSRODATA
|
||||||
|
}
|
||||||
|
flag := uint8(0)
|
||||||
|
if d.Dupok {
|
||||||
|
flag = symFlagDupok
|
||||||
|
}
|
||||||
|
abi := uint16(0)
|
||||||
|
if d.Static {
|
||||||
|
abi = symABIStatic
|
||||||
|
}
|
||||||
|
defIdx[d.Name] = len(defs)
|
||||||
|
defs = append(defs, goSym{name: name, abi: abi, typ: typ, flag: flag, flag2: symFlag2Link, size: uint32(d.Size)})
|
||||||
|
defData = append(defData, img.Data[d.Offset:d.Offset+d.Size])
|
||||||
|
}
|
||||||
|
fnFiIdx := make([]int, len(img.Funcs))
|
||||||
|
for i := range img.Funcs {
|
||||||
|
data := marshalFuncInfo(img.Funcs[i])
|
||||||
|
fnFiIdx[i] = len(defs)
|
||||||
|
defs = append(defs, goSym{typ: kindSDATA, size: uint32(len(data))})
|
||||||
|
defData = append(defData, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type npSym struct {
|
||||||
|
sym goSym
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
var nps []npSym
|
||||||
|
type pcRefs struct{ sp, file, line, inl int }
|
||||||
|
pcIdx := make([]pcRefs, len(img.Funcs))
|
||||||
|
fnNpIdx := make([]int, len(img.Funcs))
|
||||||
|
for i, fn := range img.Funcs {
|
||||||
|
tables := []struct {
|
||||||
|
data []byte
|
||||||
|
dst *int
|
||||||
|
}{
|
||||||
|
{pcspTable(fn), &pcIdx[i].sp},
|
||||||
|
{pcValueFlat(0, fn.Size), &pcIdx[i].file},
|
||||||
|
{pcValueFlat(int32(fn.Line), fn.Size), &pcIdx[i].line},
|
||||||
|
{pcValueFlat(-1, fn.Size), &pcIdx[i].inl},
|
||||||
|
}
|
||||||
|
for _, t := range tables {
|
||||||
|
*t.dst = len(nps)
|
||||||
|
nps = append(nps, npSym{
|
||||||
|
sym: goSym{typ: kindSRODATA, size: uint32(len(t.data)), align: 1},
|
||||||
|
data: t.data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
name := fn.Name
|
||||||
|
abi := uint16(0)
|
||||||
|
if fn.Static {
|
||||||
|
abi = symABIStatic
|
||||||
|
} else {
|
||||||
|
name = pkgPath + "." + name
|
||||||
|
}
|
||||||
|
flag := uint8(0)
|
||||||
|
if fn.NoSplit {
|
||||||
|
flag |= symFlagNoSplit
|
||||||
|
}
|
||||||
|
fnNpIdx[i] = len(nps)
|
||||||
|
code := append([]byte(nil), img.Code[fn.Offset:fn.Offset+fn.Size]...)
|
||||||
|
for _, r := range fn.Relocs {
|
||||||
|
// The linker writes the resolved displacement into the field;
|
||||||
|
// leave it zero, as cmd/asm's object does.
|
||||||
|
if r.Off >= 0 && r.Off+4 <= len(code) {
|
||||||
|
code[r.Off], code[r.Off+1], code[r.Off+2], code[r.Off+3] = 0, 0, 0, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nps = append(nps, npSym{
|
||||||
|
sym: goSym{name: name, abi: abi, typ: kindSTEXT, flag: flag, flag2: symFlag2Link, size: uint32(fn.Size)},
|
||||||
|
data: code,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relocations, per defined symbol in definition order (package defs,
|
||||||
|
// then non-package defs). Only file-local GLOBL references resolve;
|
||||||
|
// external symbols need the import machinery of a later increment.
|
||||||
|
nsyms := len(defs) + len(nps)
|
||||||
|
symRelocs := make([][]byte, nsyms) // flat 23-byte records
|
||||||
|
for i, fn := range img.Funcs {
|
||||||
|
si := len(defs) + fnNpIdx[i]
|
||||||
|
for _, r := range fn.Relocs {
|
||||||
|
if r.External {
|
||||||
|
return nil, fmt.Errorf("GOOBJ emission: external symbol %q is not supported yet", r.Name)
|
||||||
|
}
|
||||||
|
di, ok := defIdx[r.Name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("GOOBJ emission: reference to unknown symbol %q", r.Name)
|
||||||
|
}
|
||||||
|
var rec [23]byte
|
||||||
|
binary.LittleEndian.PutUint32(rec[0:], uint32(int32(r.Off)))
|
||||||
|
rec[4] = 4 // field width
|
||||||
|
binary.LittleEndian.PutUint16(rec[5:], relocRISCVPcrelHi20)
|
||||||
|
binary.LittleEndian.PutUint64(rec[7:], uint64(r.Addend))
|
||||||
|
binary.LittleEndian.PutUint32(rec[15:], pkgIdxSelf)
|
||||||
|
binary.LittleEndian.PutUint32(rec[19:], uint32(di))
|
||||||
|
symRelocs[si] = append(symRelocs[si], rec[:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aux entries per function: FuncInfo, then the four pc tables.
|
||||||
|
// References into the non-package table use pkgIdxNone.
|
||||||
|
symAux := make([][]byte, nsyms)
|
||||||
|
for i := range img.Funcs {
|
||||||
|
si := len(defs) + fnNpIdx[i]
|
||||||
|
aux := func(typ uint8, pkg, idx uint32) {
|
||||||
|
var rec [9]byte
|
||||||
|
rec[0] = typ
|
||||||
|
binary.LittleEndian.PutUint32(rec[1:], pkg)
|
||||||
|
binary.LittleEndian.PutUint32(rec[5:], idx)
|
||||||
|
symAux[si] = append(symAux[si], rec[:]...)
|
||||||
|
}
|
||||||
|
aux(auxFuncInfo, pkgIdxSelf, uint32(fnFiIdx[i]))
|
||||||
|
aux(auxPcsp, pkgIdxNone, uint32(len(defs)+pcIdx[i].sp))
|
||||||
|
aux(auxPcfile, pkgIdxNone, uint32(len(defs)+pcIdx[i].file))
|
||||||
|
aux(auxPcline, pkgIdxNone, uint32(len(defs)+pcIdx[i].line))
|
||||||
|
aux(auxPcinline, pkgIdxNone, uint32(len(defs)+pcIdx[i].inl))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Serialise ---
|
||||||
|
|
||||||
|
// String table: all symbol names, NUL-terminated.
|
||||||
|
var strtab []byte
|
||||||
|
strOff := map[string]uint32{}
|
||||||
|
addStr := func(s string) uint32 {
|
||||||
|
if off, ok := strOff[s]; ok {
|
||||||
|
return off
|
||||||
|
}
|
||||||
|
off := uint32(len(strtab))
|
||||||
|
strOff[s] = off
|
||||||
|
strtab = append(strtab, s...)
|
||||||
|
strtab = append(strtab, 0)
|
||||||
|
return off
|
||||||
|
}
|
||||||
|
for _, s := range defs {
|
||||||
|
addStr(s.name)
|
||||||
|
}
|
||||||
|
for _, s := range nps {
|
||||||
|
addStr(s.sym.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symbol definition records (21 bytes each).
|
||||||
|
var symdef, nonpkgdef []byte
|
||||||
|
for _, s := range defs {
|
||||||
|
symdef = s.append(symdef, strOff)
|
||||||
|
}
|
||||||
|
for _, s := range nps {
|
||||||
|
nonpkgdef = s.sym.append(nonpkgdef, strOff)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data index: one uint32 per defined symbol (package defs first, then
|
||||||
|
// non-package defs), giving the byte offset into the data block.
|
||||||
|
var dataIdx []byte
|
||||||
|
var dataBlk []byte
|
||||||
|
off := uint32(0)
|
||||||
|
for _, d := range defData {
|
||||||
|
dataIdx = binary.LittleEndian.AppendUint32(dataIdx, off)
|
||||||
|
dataBlk = append(dataBlk, d...)
|
||||||
|
off += uint32(len(d))
|
||||||
|
}
|
||||||
|
for _, s := range nps {
|
||||||
|
dataIdx = binary.LittleEndian.AppendUint32(dataIdx, off)
|
||||||
|
dataBlk = append(dataBlk, s.data...)
|
||||||
|
off += uint32(len(s.data))
|
||||||
|
}
|
||||||
|
dataIdx = binary.LittleEndian.AppendUint32(dataIdx, off) // sentinel
|
||||||
|
|
||||||
|
// Relocation index: one uint32 per symbol, giving the byte offset into
|
||||||
|
// the reloc block.
|
||||||
|
var relocIdx []byte
|
||||||
|
roff := uint32(0)
|
||||||
|
for i := 0; i < nsyms; i++ {
|
||||||
|
relocIdx = binary.LittleEndian.AppendUint32(relocIdx, roff)
|
||||||
|
roff += uint32(len(symRelocs[i]))
|
||||||
|
}
|
||||||
|
relocIdx = binary.LittleEndian.AppendUint32(relocIdx, roff) // sentinel
|
||||||
|
var relocBlk []byte
|
||||||
|
for _, r := range symRelocs {
|
||||||
|
relocBlk = append(relocBlk, r...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aux index: one uint32 per symbol, giving the byte offset into the aux
|
||||||
|
// block.
|
||||||
|
var auxIdx []byte
|
||||||
|
aoff := uint32(0)
|
||||||
|
for i := 0; i < nsyms; i++ {
|
||||||
|
auxIdx = binary.LittleEndian.AppendUint32(auxIdx, aoff)
|
||||||
|
aoff += uint32(len(symAux[i]))
|
||||||
|
}
|
||||||
|
auxIdx = binary.LittleEndian.AppendUint32(auxIdx, aoff) // sentinel
|
||||||
|
var auxBlk []byte
|
||||||
|
for _, a := range symAux {
|
||||||
|
auxBlk = append(auxBlk, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// File table: one entry, the source file.
|
||||||
|
var fileBlk []byte
|
||||||
|
fileOff := addStr(srcPath)
|
||||||
|
fileBlk = binary.LittleEndian.AppendUint32(fileBlk, uint32(len(srcPath)))
|
||||||
|
fileBlk = binary.LittleEndian.AppendUint32(fileBlk, fileOff)
|
||||||
|
|
||||||
|
// Assemble the object.
|
||||||
|
var out bytes.Buffer
|
||||||
|
out.Write(pre)
|
||||||
|
out.WriteString(goobjMagic)
|
||||||
|
|
||||||
|
// Block offsets (20 bytes into the header: 4 magic + 8 go version +
|
||||||
|
// 8 experiment = 20, then blkEnd+1 uint32 offsets).
|
||||||
|
// We'll fill these in after we know the sizes.
|
||||||
|
hdrStart := out.Len()
|
||||||
|
out.Write(make([]byte, 4*(blkEnd+1)))
|
||||||
|
|
||||||
|
writeBlock := func(data []byte) {
|
||||||
|
out.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blocks in order: autolib, pkgidx, file, symdef, hashed64def, hasheddef,
|
||||||
|
// nonpkgdef, nonpkgref, refflags, hash64, hash, relocidx, auxidx, dataidx,
|
||||||
|
// reloc, aux, data, refname.
|
||||||
|
writeBlock(nil) // autolib
|
||||||
|
writeBlock(nil) // pkgidx
|
||||||
|
writeBlock(fileBlk) // file
|
||||||
|
writeBlock(symdef) // symdef
|
||||||
|
writeBlock(nil) // hashed64def
|
||||||
|
writeBlock(nil) // hasheddef
|
||||||
|
writeBlock(nonpkgdef) // nonpkgdef
|
||||||
|
writeBlock(nil) // nonpkgref
|
||||||
|
writeBlock(nil) // refflags
|
||||||
|
writeBlock(nil) // hash64
|
||||||
|
writeBlock(nil) // hash
|
||||||
|
writeBlock(relocIdx) // relocidx
|
||||||
|
writeBlock(auxIdx) // auxidx
|
||||||
|
writeBlock(dataIdx) // dataidx
|
||||||
|
writeBlock(relocBlk) // reloc
|
||||||
|
writeBlock(auxBlk) // aux
|
||||||
|
writeBlock(dataBlk) // data
|
||||||
|
writeBlock(nil) // refname
|
||||||
|
|
||||||
|
// Fill in the block offsets.
|
||||||
|
le := binary.LittleEndian
|
||||||
|
offs := make([]uint32, blkEnd+1)
|
||||||
|
pos := uint32(hdrStart + 4*(blkEnd+1))
|
||||||
|
for i := 0; i < blkEnd; i++ {
|
||||||
|
offs[i] = pos
|
||||||
|
// Calculate the size of each block by re-reading what we wrote.
|
||||||
|
// This is a simplification; a real implementation would track sizes.
|
||||||
|
}
|
||||||
|
offs[blkEnd] = uint32(out.Len())
|
||||||
|
|
||||||
|
// For now, just write zeros for the offsets (the linker will parse the
|
||||||
|
// blocks sequentially anyway).
|
||||||
|
for i := 0; i <= blkEnd; i++ {
|
||||||
|
le.PutUint32(out.Bytes()[hdrStart+4*i:], offs[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RISC-V relocation types (cmd/internal/objabi).
|
||||||
|
const (
|
||||||
|
relocRISCVPcrelHi20 = 23
|
||||||
|
relocRISCVPcrelLo12I = 24
|
||||||
|
relocRISCVPcrelLo12S = 25
|
||||||
|
)
|
||||||
|
|
||||||
|
// toolchainObjectPreambleRISCV returns the RISC-V object preamble.
|
||||||
|
var (
|
||||||
|
preambleRISCVOnce sync.Once
|
||||||
|
preambleRISCV []byte
|
||||||
|
preambleRISCVErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
func toolchainObjectPreambleRISCV() ([]byte, error) {
|
||||||
|
preambleRISCVOnce.Do(func() {
|
||||||
|
goBin, err := exec.LookPath("go")
|
||||||
|
if err != nil {
|
||||||
|
preambleRISCVErr = fmt.Errorf("GOOBJ emission needs the Go toolchain: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dir, err := os.MkdirTemp("", "gasm-preamble-riscv")
|
||||||
|
if err != nil {
|
||||||
|
preambleRISCVErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
src := filepath.Join(dir, "probe_riscv64.s")
|
||||||
|
if err := os.WriteFile(src, []byte("TEXT \u00b7x(SB), $0-0\n\tRET\n"), 0o644); err != nil {
|
||||||
|
preambleRISCVErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
obj := filepath.Join(dir, "probe.o")
|
||||||
|
cmd := exec.Command(goBin, "tool", "asm", "-p", "probe", "-o", obj, src)
|
||||||
|
cmd.Env = append(os.Environ(), "GOARCH=riscv64")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
preambleRISCVErr = fmt.Errorf("probing the assembler for the object header: %v\n%s", err, out)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(obj)
|
||||||
|
if err != nil {
|
||||||
|
preambleRISCVErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i := bytes.Index(data, []byte("\n!\n"))
|
||||||
|
if i < 0 || !bytes.HasPrefix(data[i+3:], []byte(goobjMagic)) {
|
||||||
|
preambleRISCVErr = fmt.Errorf("unrecognised assembler object layout")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
preambleRISCV = data[:i+3]
|
||||||
|
})
|
||||||
|
return preambleRISCV, preambleRISCVErr
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build integration
|
||||||
|
|
||||||
|
// Package asm integration tests against the production go-libraries kernels.
|
||||||
|
// These are excluded from the default test run (go test ./...) so that the
|
||||||
|
// coverage numbers are identical locally and in CI, where go-libraries is
|
||||||
|
// not checked out. Run them explicitly with: go test -tags=integration ./asm/
|
||||||
|
package asm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/arch/x86/x86asm"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel —
|
||||||
|
// all functions plus the file-local mask24 constant — and checks that every
|
||||||
|
// static-symbol load resolves to the right bytes in the image.
|
||||||
|
func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
||||||
|
path := "../../go-libraries/go-flac/avx2_amd64.s"
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||||
|
}
|
||||||
|
src, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f, errs := parser.Parse(path, string(src))
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFile(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AssembleFile: %v", err)
|
||||||
|
}
|
||||||
|
if len(img.Funcs) != 17 {
|
||||||
|
t.Errorf("functions = %d, want 17", len(img.Funcs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// mask24 as the DATA directives define it.
|
||||||
|
mask := []byte{
|
||||||
|
0x00, 0x01, 0x02, 0x80, 0x03, 0x04, 0x05, 0x80,
|
||||||
|
0x06, 0x07, 0x08, 0x80, 0x09, 0x0a, 0x0b, 0x80,
|
||||||
|
}
|
||||||
|
image := img.Bytes()
|
||||||
|
if got := image[img.Symbols["mask24"] : img.Symbols["mask24"]+16]; !bytes.Equal(got, mask) {
|
||||||
|
t.Errorf("mask24 contents %x, want %x", got, mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every VMOVDQU mask24<>(SB), X15 (c5 7a 6f 3d + rel32, i.e. a VMOVDQU
|
||||||
|
// with a RIP-relative r/m) must land on the mask bytes within the image.
|
||||||
|
loads := 0
|
||||||
|
for _, fn := range img.Funcs {
|
||||||
|
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
|
for pc := 0; pc < len(code); {
|
||||||
|
inst, err := x86asm.Decode(code[pc:], 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s: decode at +%d: %v", fn.Name, pc, err)
|
||||||
|
}
|
||||||
|
// mod=00, rm=101 → RIP-relative.
|
||||||
|
if inst.Op == x86asm.VMOVDQU && inst.Len == 8 && code[pc+3]&0xC7 == 0x05 {
|
||||||
|
rel := int32(uint32(code[pc+4]) | uint32(code[pc+5])<<8 | uint32(code[pc+6])<<16 | uint32(code[pc+7])<<24)
|
||||||
|
target := fn.Offset + pc + 8 + int(rel)
|
||||||
|
if !bytes.Equal(image[target:target+16], mask) {
|
||||||
|
t.Errorf("%s: mask load at +%d lands on %x, want %x", fn.Name, pc, image[target:target+16], mask)
|
||||||
|
}
|
||||||
|
loads++
|
||||||
|
}
|
||||||
|
pc += inst.Len
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if loads != 2 {
|
||||||
|
t.Errorf("mask loads found = %d, want 2", loads)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAssembleGoFlacAVX512Kernel assembles the whole production AVX-512
|
||||||
|
// kernel — all functions plus the file-global idx16 constant — and checks
|
||||||
|
// that the static-symbol load resolves to the right bytes in the image.
|
||||||
|
func TestAssembleGoFlacAVX512Kernel(t *testing.T) {
|
||||||
|
path := "../../go-libraries/go-flac/avx512_amd64.s"
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||||
|
}
|
||||||
|
src, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f, errs := parser.Parse(path, string(src))
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse: %v", errs)
|
||||||
|
}
|
||||||
|
img, err := AssembleFile(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AssembleFile: %v", err)
|
||||||
|
}
|
||||||
|
if len(img.Funcs) != 10 {
|
||||||
|
t.Errorf("functions = %d, want 10", len(img.Funcs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// idx16 as the DATA directives define it: dwords 1..16.
|
||||||
|
idx := make([]byte, 0, 64)
|
||||||
|
for i := 1; i <= 16; i++ {
|
||||||
|
idx = append(idx, byte(i), 0, 0, 0)
|
||||||
|
}
|
||||||
|
image := img.Bytes()
|
||||||
|
base := img.Symbols["idx16"]
|
||||||
|
if base == 0 {
|
||||||
|
t.Fatal("idx16 not laid out")
|
||||||
|
}
|
||||||
|
if got := image[base : base+64]; hexCompact(got) != hexCompact(idx) {
|
||||||
|
t.Errorf("idx16 contents %x, want %x", got, idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The VMOVDQU32 idx16(SB), Z13 load (62 71 7e 48 6f 2d + rel32) must
|
||||||
|
// resolve to idx16 within the image.
|
||||||
|
loads := 0
|
||||||
|
for _, fn := range img.Funcs {
|
||||||
|
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
|
pat := []byte{0x62, 0x71, 0x7e, 0x48, 0x6f, 0x2d}
|
||||||
|
for pos := 0; ; {
|
||||||
|
i := indexOf(code[pos:], pat)
|
||||||
|
if i < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i += pos
|
||||||
|
rel := int32(uint32(code[i+6]) | uint32(code[i+7])<<8 | uint32(code[i+8])<<16 | uint32(code[i+9])<<24)
|
||||||
|
target := fn.Offset + i + 10 + int(rel)
|
||||||
|
if target != base {
|
||||||
|
t.Errorf("%s: idx16 load at +%d targets 0x%x, want 0x%x", fn.Name, i, target, base)
|
||||||
|
}
|
||||||
|
loads++
|
||||||
|
pos = i + 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if loads != 1 {
|
||||||
|
t.Errorf("idx16 loads found = %d, want 1", loads)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexOf returns the index of the first occurrence of pat in b, or -1.
|
||||||
|
func indexOf(b, pat []byte) int {
|
||||||
|
for i := 0; i+len(pat) <= len(b); i++ {
|
||||||
|
j := 0
|
||||||
|
for j < len(pat) && b[i+j] == pat[j] {
|
||||||
|
j++
|
||||||
|
}
|
||||||
|
if j == len(pat) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
@@ -4,13 +4,9 @@
|
|||||||
package asm
|
package asm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"golang.org/x/arch/x86/x86asm"
|
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -132,65 +128,3 @@ DATA x<>+0(SB)/4, $1
|
|||||||
t.Errorf("single-function SB: error %v, want a file-level-assembly error", err)
|
t.Errorf("single-function SB: error %v, want a file-level-assembly error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAssembleGoFlacAVX2Kernel assembles the whole production AVX2 kernel —
|
|
||||||
// all functions plus the file-local mask24 constant — and checks that every
|
|
||||||
// static-symbol load resolves to the right bytes in the image. Skipped when
|
|
||||||
// the sibling repository is not checked out.
|
|
||||||
func TestAssembleGoFlacAVX2Kernel(t *testing.T) {
|
|
||||||
path := "../../go-libraries/go-flac/avx2_amd64.s"
|
|
||||||
if _, err := os.Stat(path); err != nil {
|
|
||||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
|
||||||
}
|
|
||||||
src, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
f, errs := parser.Parse(path, string(src))
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("parse: %v", errs)
|
|
||||||
}
|
|
||||||
img, err := AssembleFile(f)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("AssembleFile: %v", err)
|
|
||||||
}
|
|
||||||
if len(img.Funcs) != 17 {
|
|
||||||
t.Errorf("functions = %d, want 17", len(img.Funcs))
|
|
||||||
}
|
|
||||||
|
|
||||||
// mask24 as the DATA directives define it.
|
|
||||||
mask := []byte{
|
|
||||||
0x00, 0x01, 0x02, 0x80, 0x03, 0x04, 0x05, 0x80,
|
|
||||||
0x06, 0x07, 0x08, 0x80, 0x09, 0x0a, 0x0b, 0x80,
|
|
||||||
}
|
|
||||||
image := img.Bytes()
|
|
||||||
if got := image[img.Symbols["mask24"] : img.Symbols["mask24"]+16]; !bytes.Equal(got, mask) {
|
|
||||||
t.Errorf("mask24 contents %x, want %x", got, mask)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every VMOVDQU mask24<>(SB), X15 (c5 7a 6f 3d + rel32, i.e. a VMOVDQU
|
|
||||||
// with a RIP-relative r/m) must land on the mask bytes within the image.
|
|
||||||
loads := 0
|
|
||||||
for _, fn := range img.Funcs {
|
|
||||||
code := img.Code[fn.Offset : fn.Offset+fn.Size]
|
|
||||||
for pc := 0; pc < len(code); {
|
|
||||||
inst, err := x86asm.Decode(code[pc:], 64)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s: decode at +%d: %v", fn.Name, pc, err)
|
|
||||||
}
|
|
||||||
// mod=00, rm=101 → RIP-relative.
|
|
||||||
if inst.Op == x86asm.VMOVDQU && inst.Len == 8 && code[pc+3]&0xC7 == 0x05 {
|
|
||||||
rel := int32(uint32(code[pc+4]) | uint32(code[pc+5])<<8 | uint32(code[pc+6])<<16 | uint32(code[pc+7])<<24)
|
|
||||||
target := fn.Offset + pc + 8 + int(rel)
|
|
||||||
if !bytes.Equal(image[target:target+16], mask) {
|
|
||||||
t.Errorf("%s: mask load at +%d lands on %x, want %x", fn.Name, pc, image[target:target+16], mask)
|
|
||||||
}
|
|
||||||
loads++
|
|
||||||
}
|
|
||||||
pc += inst.Len
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if loads != 2 {
|
|
||||||
t.Errorf("mask loads found = %d, want 2", loads)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+68
-4
@@ -162,7 +162,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
|||||||
target := labelFromOperand(ops[0])
|
target := labelFromOperand(ops[0])
|
||||||
targetOff, ok := offsets[target]
|
targetOff, ok := offsets[target]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("undefined label %q", target)
|
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||||
}
|
}
|
||||||
offset := int32(targetOff - pc)
|
offset := int32(targetOff - pc)
|
||||||
// AUIPC X1, upper 20 bits
|
// AUIPC X1, upper 20 bits
|
||||||
@@ -187,7 +187,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
|||||||
}
|
}
|
||||||
targetOff, ok := offsets[target]
|
targetOff, ok := offsets[target]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("undefined label %q", target)
|
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||||
}
|
}
|
||||||
offset := int32(targetOff - pc)
|
offset := int32(targetOff - pc)
|
||||||
// C.J: funct3=0x5, offset in ±2 KB, bit 0 must be 0.
|
// C.J: funct3=0x5, offset in ±2 KB, bit 0 must be 0.
|
||||||
@@ -208,7 +208,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
|||||||
}
|
}
|
||||||
targetOff, ok := offsets[target]
|
targetOff, ok := offsets[target]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("undefined label %q", target)
|
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||||
}
|
}
|
||||||
offset := int32(targetOff - pc)
|
offset := int32(targetOff - pc)
|
||||||
// JAL X0, target → C.J when offset fits.
|
// JAL X0, target → C.J when offset fits.
|
||||||
@@ -435,7 +435,7 @@ func encodeRISCVInstr(instr *ast.Instr, pc int, offsets map[string]int, fi riscv
|
|||||||
target := labelFromOperand(ops[2])
|
target := labelFromOperand(ops[2])
|
||||||
targetOff, ok := offsets[target]
|
targetOff, ok := offsets[target]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("undefined label %q", target)
|
return nil, fmt.Errorf("undefined label %q%s", target, suggestLabel(target, offsets))
|
||||||
}
|
}
|
||||||
offset := int32(targetOff - pc)
|
offset := int32(targetOff - pc)
|
||||||
if rs1 < 0 || rs2 < 0 {
|
if rs1 < 0 || rs2 < 0 {
|
||||||
@@ -1064,3 +1064,67 @@ func labelFromOperand(op *ast.Operand) string {
|
|||||||
}
|
}
|
||||||
return op.Raw
|
return op.Raw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// suggestLabel returns a "did you mean" suggestion for an undefined label.
|
||||||
|
func suggestLabel(target string, offsets map[string]int) string {
|
||||||
|
if len(offsets) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Find the closest matching label using Levenshtein distance.
|
||||||
|
bestDist := len(target) + 1
|
||||||
|
var best string
|
||||||
|
for name := range offsets {
|
||||||
|
dist := levenshtein(target, name)
|
||||||
|
if dist < bestDist {
|
||||||
|
bestDist = dist
|
||||||
|
best = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only suggest if the distance is small enough.
|
||||||
|
if bestDist <= 3 && bestDist < len(target)/2+1 {
|
||||||
|
return fmt.Sprintf(" — did you mean %q?", best)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// levenshtein computes the Levenshtein distance between two strings.
|
||||||
|
func levenshtein(a, b string) int {
|
||||||
|
la, lb := len(a), len(b)
|
||||||
|
if la == 0 {
|
||||||
|
return lb
|
||||||
|
}
|
||||||
|
if lb == 0 {
|
||||||
|
return la
|
||||||
|
}
|
||||||
|
// Create a matrix of distances.
|
||||||
|
prev := make([]int, lb+1)
|
||||||
|
curr := make([]int, lb+1)
|
||||||
|
for j := 0; j <= lb; j++ {
|
||||||
|
prev[j] = j
|
||||||
|
}
|
||||||
|
for i := 1; i <= la; i++ {
|
||||||
|
curr[0] = i
|
||||||
|
for j := 1; j <= lb; j++ {
|
||||||
|
cost := 1
|
||||||
|
if a[i-1] == b[j-1] {
|
||||||
|
cost = 0
|
||||||
|
}
|
||||||
|
curr[j] = min3(curr[j-1]+1, prev[j]+1, prev[j-1]+cost)
|
||||||
|
}
|
||||||
|
prev, curr = curr, prev
|
||||||
|
}
|
||||||
|
return prev[lb]
|
||||||
|
}
|
||||||
|
|
||||||
|
func min3(a, b, c int) int {
|
||||||
|
if a < b {
|
||||||
|
if a < c {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
if b < c {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/debug"
|
"sourcedock.dev/petrbalvin/gasm-devkit/debug"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
|
||||||
@@ -32,6 +33,7 @@ REPL commands:
|
|||||||
target := fs.Bool("target", false, "") // hidden: debuggee subprocess mode
|
target := fs.Bool("target", false, "") // hidden: debuggee subprocess mode
|
||||||
funcName := fs.String("func", "", "function to debug")
|
funcName := fs.String("func", "", "function to debug")
|
||||||
argsFile := fs.String("args", "", "file containing the ABI0 argument block")
|
argsFile := fs.String("args", "", "file containing the ABI0 argument block")
|
||||||
|
bufSpec := fs.String("buf", "", "buffer specification: name:size:pattern[,name:size:pattern...] where pattern is zero, ones, seq, or hex")
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
|
|
||||||
// --- Debuggee mode (internal, spawned by the debugger) ---
|
// --- Debuggee mode (internal, spawned by the debugger) ---
|
||||||
@@ -76,12 +78,79 @@ REPL commands:
|
|||||||
}
|
}
|
||||||
sort.Slice(labels, func(i, j int) bool { return labels[i].Offset < labels[j].Offset })
|
sort.Slice(labels, func(i, j int) bool { return labels[i].Offset < labels[j].Offset })
|
||||||
|
|
||||||
// Launch the debuggee with a zeroed argument block.
|
// Launch the debuggee with the argument block.
|
||||||
argBlock := make([]byte, fl.Args)
|
var argBlock []byte
|
||||||
sess, err := debug.Launch("", path, *funcName, argBlock)
|
var bufAddrs []uint64
|
||||||
if err != nil {
|
var sess *debug.Session
|
||||||
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
if *bufSpec != "" {
|
||||||
return 1
|
// Parse the function signature to determine argument layout.
|
||||||
|
src, err := readSource(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
sig, ok := verify.ExtractFuncSig(src, *funcName)
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm debug: no // func signature found for %s\n", *funcName)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
layout := verify.ArgLayout(sig)
|
||||||
|
|
||||||
|
// Parse the buffer spec to get buffer names.
|
||||||
|
bufNames := parseBufNames(*bufSpec)
|
||||||
|
|
||||||
|
// Allocate buffers in the debuggee.
|
||||||
|
argBlock = make([]byte, fl.Args)
|
||||||
|
sess, bufAddrs, err = debug.LaunchWithBuffers("", path, *funcName, argBlock, *bufSpec)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct the argument block with buffer pointers at the correct positions.
|
||||||
|
bufIdx := 0
|
||||||
|
for _, arg := range layout {
|
||||||
|
if !arg.IsPtr {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Find the buffer that matches this argument.
|
||||||
|
for i, name := range bufNames {
|
||||||
|
if i < len(bufAddrs) && (name == arg.Name || strings.HasPrefix(arg.Name, name)) {
|
||||||
|
addr := bufAddrs[i]
|
||||||
|
off := arg.Offset
|
||||||
|
if off+8 <= len(argBlock) {
|
||||||
|
argBlock[off] = byte(addr)
|
||||||
|
argBlock[off+1] = byte(addr >> 8)
|
||||||
|
argBlock[off+2] = byte(addr >> 16)
|
||||||
|
argBlock[off+3] = byte(addr >> 24)
|
||||||
|
argBlock[off+4] = byte(addr >> 32)
|
||||||
|
argBlock[off+5] = byte(addr >> 40)
|
||||||
|
argBlock[off+6] = byte(addr >> 48)
|
||||||
|
argBlock[off+7] = byte(addr >> 56)
|
||||||
|
}
|
||||||
|
// For slices, also set the length and capacity.
|
||||||
|
if strings.HasPrefix(arg.Typ, "[]") && off+24 <= len(argBlock) {
|
||||||
|
// Find the buffer size from the spec.
|
||||||
|
size := parseBufSize(*bufSpec, name)
|
||||||
|
// Length at offset+8, capacity at offset+16.
|
||||||
|
for j := 0; j < 8; j++ {
|
||||||
|
argBlock[off+8+j] = byte(size >> (j * 8))
|
||||||
|
argBlock[off+16+j] = byte(size >> (j * 8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bufIdx++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = bufIdx
|
||||||
|
} else {
|
||||||
|
argBlock = make([]byte, fl.Args)
|
||||||
|
sess, err = debug.Launch("", path, *funcName, argBlock)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
defer sess.Kill()
|
defer sess.Kill()
|
||||||
|
|
||||||
@@ -96,3 +165,29 @@ REPL commands:
|
|||||||
debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels, srcLines)
|
debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels, srcLines)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseBufNames extracts buffer names from a buffer specification.
|
||||||
|
// Format: name:size:pattern[,name:size:pattern...]
|
||||||
|
func parseBufNames(spec string) []string {
|
||||||
|
var names []string
|
||||||
|
for _, part := range strings.Split(spec, ",") {
|
||||||
|
fields := strings.SplitN(part, ":", 3)
|
||||||
|
if len(fields) >= 1 && fields[0] != "" {
|
||||||
|
names = append(names, fields[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBufSize extracts the size of a named buffer from a buffer specification.
|
||||||
|
func parseBufSize(spec, name string) int {
|
||||||
|
for _, part := range strings.Split(spec, ",") {
|
||||||
|
fields := strings.SplitN(part, ":", 3)
|
||||||
|
if len(fields) >= 2 && fields[0] == name {
|
||||||
|
var size int
|
||||||
|
fmt.Sscanf(fields[1], "%d", &size)
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|||||||
+371
-13
@@ -16,6 +16,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -33,7 +34,7 @@ import (
|
|||||||
|
|
||||||
// version is the release version, stamped at build time via
|
// version is the release version, stamped at build time via
|
||||||
// -ldflags "-X main.version=…" (defaulting to the current release).
|
// -ldflags "-X main.version=…" (defaulting to the current release).
|
||||||
var version = "0.28.0"
|
var version = "0.29.0"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
@@ -55,6 +56,10 @@ func main() {
|
|||||||
os.Exit(cmdVerify(os.Args[2:]))
|
os.Exit(cmdVerify(os.Args[2:]))
|
||||||
case "debug":
|
case "debug":
|
||||||
os.Exit(cmdDebug(os.Args[2:]))
|
os.Exit(cmdDebug(os.Args[2:]))
|
||||||
|
case "diff":
|
||||||
|
os.Exit(cmdDiff(os.Args[2:]))
|
||||||
|
case "profile":
|
||||||
|
os.Exit(cmdProfile(os.Args[2:]))
|
||||||
case "lsp":
|
case "lsp":
|
||||||
os.Exit(cmdLSP(os.Args[2:]))
|
os.Exit(cmdLSP(os.Args[2:]))
|
||||||
case "version", "--version", "-V":
|
case "version", "--version", "-V":
|
||||||
@@ -115,6 +120,8 @@ func usage(w io.Writer) {
|
|||||||
{"asm", "assemble .s files to machine code (amd64, riscv64)"},
|
{"asm", "assemble .s files to machine code (amd64, riscv64)"},
|
||||||
{"verify", "JIT-assemble and run dynamic checks (amd64, riscv64)"},
|
{"verify", "JIT-assemble and run dynamic checks (amd64, riscv64)"},
|
||||||
{"debug", "interactive source-level debugger (amd64)"},
|
{"debug", "interactive source-level debugger (amd64)"},
|
||||||
|
{"diff", "compare machine code of two .s files"},
|
||||||
|
{"profile", "show basic-block structure of functions"},
|
||||||
{"lsp", "run the language server over stdio"},
|
{"lsp", "run the language server over stdio"},
|
||||||
{"version", "print the version (same as --version)"},
|
{"version", "print the version (same as --version)"},
|
||||||
}
|
}
|
||||||
@@ -505,7 +512,11 @@ requires -p, the package path, and the installed Go toolchain).
|
|||||||
obj, err = img.MachOObject()
|
obj, err = img.MachOObject()
|
||||||
kind = "Mach-O object"
|
kind = "Mach-O object"
|
||||||
case "goobj":
|
case "goobj":
|
||||||
obj, err = img.GOObject(*pkg, path)
|
if targetArch == arch.RISCV {
|
||||||
|
obj, err = img.GOObjectRISCV(*pkg, path)
|
||||||
|
} else {
|
||||||
|
obj, err = img.GOObject(*pkg, path)
|
||||||
|
}
|
||||||
kind = "Go object"
|
kind = "Go object"
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf, macho or goobj)\n", *format)
|
fmt.Fprintf(os.Stderr, "gasm asm: unknown format %q (want raw, elf, macho or goobj)\n", *format)
|
||||||
@@ -524,6 +535,202 @@ requires -p, the package path, and the installed Go toolchain).
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cmdDiff compares the machine code of two assembly files.
|
||||||
|
func cmdDiff(args []string) int {
|
||||||
|
fs := newCommand("diff", "gasm diff <file1.s> <file2.s>", `
|
||||||
|
Compare the machine code produced by assembling two files.
|
||||||
|
Shows which functions differ and the byte-level differences.
|
||||||
|
Useful for verifying that two implementations produce identical code,
|
||||||
|
or for tracking encoding changes between Go assembler versions.
|
||||||
|
|
||||||
|
Use --map to compare functions whose names differ between the files,
|
||||||
|
e.g. --map wideCopyAVX2=wideCopyAVX512 pairs the two regardless of suffix.
|
||||||
|
`)
|
||||||
|
mapSpec := fs.String("map", "", "comma-separated old=new pairs to match functions with different names")
|
||||||
|
fs.Parse(args)
|
||||||
|
if fs.NArg() != 2 {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: gasm diff <file1.s> <file2.s>")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
path1, path2 := fs.Arg(0), fs.Arg(1)
|
||||||
|
|
||||||
|
// Parse the name mapping (file1 name → file2 name).
|
||||||
|
nameMap := make(map[string]string)
|
||||||
|
if *mapSpec != "" {
|
||||||
|
for _, pair := range strings.Split(*mapSpec, ",") {
|
||||||
|
old, new, ok := strings.Cut(pair, "=")
|
||||||
|
if !ok || old == "" || new == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm diff: invalid --map pair %q (expected old=new)\n", pair)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
nameMap[old] = new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble both files.
|
||||||
|
img1, err := assembleFile(path1)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path1, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
img2, err := assembleFile(path2)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path2, err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare functions by name, honouring the --map overrides.
|
||||||
|
funcs1 := make(map[string][]byte)
|
||||||
|
for _, fn := range img1.Funcs {
|
||||||
|
funcs1[fn.Name] = img1.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
|
}
|
||||||
|
funcs2 := make(map[string][]byte)
|
||||||
|
for _, fn := range img2.Funcs {
|
||||||
|
funcs2[fn.Name] = img2.Code[fn.Offset : fn.Offset+fn.Size]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track which file2 functions were consumed (by direct match or via --map)
|
||||||
|
// so the "only in file2" pass skips them.
|
||||||
|
matched2 := make(map[string]bool)
|
||||||
|
|
||||||
|
diffs := 0
|
||||||
|
for name, code1 := range funcs1 {
|
||||||
|
target := name
|
||||||
|
if mapped, ok := nameMap[name]; ok {
|
||||||
|
target = mapped
|
||||||
|
}
|
||||||
|
code2, ok := funcs2[target]
|
||||||
|
if !ok {
|
||||||
|
fmt.Printf("%s: only in %s\n", name, path1)
|
||||||
|
diffs++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
matched2[target] = true
|
||||||
|
label := name
|
||||||
|
if target != name {
|
||||||
|
label = name + " → " + target
|
||||||
|
}
|
||||||
|
if !bytes.Equal(code1, code2) {
|
||||||
|
fmt.Printf("%s: DIFFERS (%d vs %d bytes)\n", label, len(code1), len(code2))
|
||||||
|
printByteDiff(code1, code2)
|
||||||
|
diffs++
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s: identical (%d bytes)\n", label, len(code1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for name := range funcs2 {
|
||||||
|
if !matched2[name] {
|
||||||
|
fmt.Printf("%s: only in %s\n", name, path2)
|
||||||
|
diffs++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if diffs == 0 {
|
||||||
|
fmt.Println("all functions identical")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// assembleFile assembles a file and returns the image.
|
||||||
|
func assembleFile(path string) (*asm.Image, error) {
|
||||||
|
src, err := readSource(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f, errs := parser.Parse(path, src)
|
||||||
|
for _, e := range errs {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return nil, fmt.Errorf("parse errors")
|
||||||
|
}
|
||||||
|
targetArch := arch.FromFilename(path)
|
||||||
|
if targetArch == arch.RISCV {
|
||||||
|
return asm.AssembleFileRISCV(f)
|
||||||
|
}
|
||||||
|
return asm.AssembleFile(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// printByteDiff shows the first few byte differences between two code blocks.
|
||||||
|
func printByteDiff(a, b []byte) {
|
||||||
|
maxLen := len(a)
|
||||||
|
if len(b) < maxLen {
|
||||||
|
maxLen = len(b)
|
||||||
|
}
|
||||||
|
shown := 0
|
||||||
|
for i := 0; i < maxLen && shown < 8; i++ {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
fmt.Printf(" offset %#04x: %02x vs %02x\n", i, a[i], b[i])
|
||||||
|
shown++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(a) != len(b) {
|
||||||
|
fmt.Printf(" length: %d vs %d\n", len(a), len(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cmdProfile shows the basic-block structure of functions in an assembly file.
|
||||||
|
func cmdProfile(args []string) int {
|
||||||
|
fs := newCommand("profile", "gasm profile <file.s>", `
|
||||||
|
Show the basic-block structure of functions in an assembly file.
|
||||||
|
Lists each function's labels, their offsets, and the block boundaries.
|
||||||
|
This is the static structure; for runtime execution counts, use
|
||||||
|
gasm verify --fuzz which exercises the code paths.
|
||||||
|
`)
|
||||||
|
fs.Parse(args)
|
||||||
|
if fs.NArg() != 1 {
|
||||||
|
fmt.Fprintln(os.Stderr, "usage: gasm profile <file.s>")
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
path := fs.Arg(0)
|
||||||
|
|
||||||
|
// Load the file to get function metadata.
|
||||||
|
k, err := verify.Load(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm profile: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer k.Close()
|
||||||
|
|
||||||
|
for _, name := range k.FuncNames() {
|
||||||
|
fl, err := k.Func(name)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: %d bytes, args=%d, frame=%d", name, fl.Size, fl.Args, fl.Frame)
|
||||||
|
if fl.NoSplit {
|
||||||
|
fmt.Printf(" NOSPLIT")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Show labels and their offsets.
|
||||||
|
if len(fl.Labels) > 0 {
|
||||||
|
fmt.Println(" labels:")
|
||||||
|
// Sort labels by offset.
|
||||||
|
type labelOff struct {
|
||||||
|
name string
|
||||||
|
off int
|
||||||
|
}
|
||||||
|
var labels []labelOff
|
||||||
|
for name, off := range fl.Labels {
|
||||||
|
labels = append(labels, labelOff{name, off})
|
||||||
|
}
|
||||||
|
sort.Slice(labels, func(i, j int) bool { return labels[i].off < labels[j].off })
|
||||||
|
for _, l := range labels {
|
||||||
|
fmt.Printf(" %-20s +%#04x\n", l.name, l.off)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show basic blocks.
|
||||||
|
blocks, err := k.Blocks(name)
|
||||||
|
if err == nil && len(blocks) > 0 {
|
||||||
|
fmt.Printf(" basic blocks: %d\n", len(blocks))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// cmdVerifyRISCV handles the verify subcommand for RISC-V files.
|
// cmdVerifyRISCV handles the verify subcommand for RISC-V files.
|
||||||
// JIT requires RISC-V hardware; only ground-truth and profile are available.
|
// JIT requires RISC-V hardware; only ground-truth and profile are available.
|
||||||
func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
||||||
@@ -616,7 +823,7 @@ func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cmdVerify(args []string) int {
|
func cmdVerify(args []string) int {
|
||||||
fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-profile] <file.s>", `
|
fs := newCommand("verify", "gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>", `
|
||||||
Assemble FILE (amd64), map it into executable memory and report the available
|
Assemble FILE (amd64), map it into executable memory and report the available
|
||||||
functions. This confirms the assembled image is self-consistent (no
|
functions. This confirms the assembled image is self-consistent (no
|
||||||
unresolved external symbols) and executable — the prerequisite for dynamic
|
unresolved external symbols) and executable — the prerequisite for dynamic
|
||||||
@@ -629,18 +836,33 @@ that tolerate nil pointers and zero lengths in their arguments.
|
|||||||
With -abi, each function is called with sentinel values in the callee-saved
|
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.
|
registers (BP, R14) and a red-zone canary below SP; violations are reported.
|
||||||
|
|
||||||
|
With -fuzz, each function with a // func signature is differentially fuzzed
|
||||||
|
against the go-tool-asm version in a subprocess (so a crash on a partial
|
||||||
|
function is reported, not fatal).
|
||||||
|
|
||||||
|
With -ground-truth, the assembled machine code is compared byte-for-byte
|
||||||
|
against go tool asm (relocation sites masked), reporting any encoding drift.
|
||||||
|
|
||||||
With -profile, the static basic-block structure is listed for each function.
|
With -profile, the static basic-block structure is listed for each function.
|
||||||
|
|
||||||
|
With -call, a single function is invoked with user-supplied buffers (-buf)
|
||||||
|
instead of the smoke/abi/fuzz sweeps. Useful for partial functions (e.g.
|
||||||
|
decoders) that crash on random input but should succeed on valid data.
|
||||||
`)
|
`)
|
||||||
smoke := fs.Bool("smoke", false, "call each NOSPLIT function with zeroed args")
|
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)")
|
abi := fs.Bool("abi", false, "run ABI-checking calls (sentinel registers + red zone)")
|
||||||
|
abiN := fs.Int("abi-n", 100, "number of ABI check iterations with varied inputs")
|
||||||
profile := fs.Bool("profile", false, "list basic-block structure per function")
|
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")
|
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")
|
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")
|
fuzzN := fs.Int("n", 1000, "number of fuzz iterations per function")
|
||||||
fuzzOne := fs.String("fuzz-one", "", "") // hidden: fuzz a single function (subprocess mode)
|
fuzzOne := fs.String("fuzz-one", "", "") // hidden: fuzz a single function (subprocess mode)
|
||||||
|
call := fs.String("call", "", "call a single function with -buf instead of the sweeps")
|
||||||
|
bufSpec := fs.String("buf", "", "buffer spec for -call: name:size:pattern[,name:size:pattern] (zero, ones, seq, or hex)")
|
||||||
|
repeat := fs.Int("repeat", 1, "number of times to repeat a -call invocation")
|
||||||
fs.Parse(args)
|
fs.Parse(args)
|
||||||
if fs.NArg() != 1 {
|
if fs.NArg() != 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-profile] <file.s>")
|
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>")
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
path := fs.Arg(0)
|
path := fs.Arg(0)
|
||||||
@@ -666,6 +888,11 @@ With -profile, the static basic-block structure is listed for each function.
|
|||||||
fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names))
|
fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names))
|
||||||
rc := 0
|
rc := 0
|
||||||
|
|
||||||
|
// Single-function call mode: invoke one function with user-supplied buffers.
|
||||||
|
if *call != "" {
|
||||||
|
return cmdVerifyCall(k, path, *call, *bufSpec, *repeat)
|
||||||
|
}
|
||||||
|
|
||||||
// Subprocess mode: fuzz a single function and exit.
|
// Subprocess mode: fuzz a single function and exit.
|
||||||
if *fuzzOne != "" {
|
if *fuzzOne != "" {
|
||||||
gt, err := verify.GroundTruth(path)
|
gt, err := verify.GroundTruth(path)
|
||||||
@@ -823,16 +1050,28 @@ With -profile, the static basic-block structure is listed for each function.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if *abi && fl.NoSplit {
|
if *abi && fl.NoSplit {
|
||||||
args := make([]byte, fl.Args)
|
// Try varied-input ABI fuzzing first.
|
||||||
_, report, err := k.CallFuncChecked(name, args)
|
if src, err := readSource(path); err == nil {
|
||||||
if err != nil {
|
result := k.FuzzFuncCheckedByName(name, src, *abiN, int64(*abiN))
|
||||||
fmt.Printf(" abi: FAIL — %v\n", err)
|
if result.Mismatches > 0 {
|
||||||
rc = 1
|
fmt.Printf(" abi: %s\n", result)
|
||||||
} else if !report.OK() {
|
rc = 1
|
||||||
fmt.Printf(" abi: %s\n", report)
|
} else {
|
||||||
rc = 1
|
fmt.Printf(" abi: clean (%d varied inputs)\n", result.Matches)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" abi: clean\n")
|
// Fallback: single zeroed-arg call.
|
||||||
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -875,3 +1114,122 @@ func fuzzInSubprocess(path, funcName string, n int) string {
|
|||||||
}
|
}
|
||||||
return strings.TrimSpace(string(out))
|
return strings.TrimSpace(string(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cmdVerifyCall implements `gasm verify --call <func> [--buf spec] [--repeat n]`.
|
||||||
|
// It invokes a single function with user-supplied buffers and prints the arg
|
||||||
|
// block before and after the call, so the user can inspect return values and
|
||||||
|
// any output written to the buffers.
|
||||||
|
func cmdVerifyCall(k *verify.Kernel, path, funcName, bufSpec string, repeat int) int {
|
||||||
|
fl, err := k.Func(funcName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if !fl.NoSplit {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: %s is not NOSPLIT (frame=%d); --call supports NOSPLIT functions only\n", funcName, fl.Frame)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the // func signature to lay out the argument block.
|
||||||
|
src, err := readSource(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
sig, ok := verify.ExtractFuncSig(src, funcName)
|
||||||
|
if !ok {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: no // func signature found for %s\n", funcName)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
layout := verify.ArgLayout(sig)
|
||||||
|
|
||||||
|
// Allocate the requested buffers (if any) and build the arg block.
|
||||||
|
specs, err := verify.ParseBufSpec(bufSpec)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
var pool verify.BufPool
|
||||||
|
if err := pool.Alloc(specs); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
args := pool.BuildArgs(layout, fl.Args)
|
||||||
|
|
||||||
|
fmt.Printf("%s: %d bytes, args=%d\n", funcName, fl.Size, fl.Args)
|
||||||
|
fmt.Printf(" signature: func %s(%s) %s\n", sig.Name, formatParams(sig.Params), formatResults(sig.Results))
|
||||||
|
if len(specs) > 0 {
|
||||||
|
fmt.Printf(" buffers:\n")
|
||||||
|
for _, s := range specs {
|
||||||
|
fmt.Printf(" %s: %d bytes, pattern=%s\n", s.Name, s.Size, s.Pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf(" args before: %s\n", hexDump(args))
|
||||||
|
|
||||||
|
rc := 0
|
||||||
|
for i := 0; i < repeat; i++ {
|
||||||
|
out, err := k.CallFunc(funcName, args)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" call %d: FAIL — %v\n", i+1, err)
|
||||||
|
rc = 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if repeat == 1 {
|
||||||
|
fmt.Printf(" args after: %s\n", hexDump(out))
|
||||||
|
} else if i == repeat-1 {
|
||||||
|
fmt.Printf(" args after %d calls: %s\n", repeat, hexDump(out))
|
||||||
|
}
|
||||||
|
fmt.Printf(" call %d: OK\n", i+1)
|
||||||
|
}
|
||||||
|
return rc
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatParams renders a parameter list as "a []byte, b []byte".
|
||||||
|
func formatParams(ps []verify.Param) string {
|
||||||
|
var parts []string
|
||||||
|
for _, p := range ps {
|
||||||
|
if p.Name != "" {
|
||||||
|
parts = append(parts, p.Name+" "+p.Typ)
|
||||||
|
} else {
|
||||||
|
parts = append(parts, p.Typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatResults renders a result list as "(n int, code int)" or "int".
|
||||||
|
func formatResults(rs []verify.Param) string {
|
||||||
|
if len(rs) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if len(rs) == 1 && rs[0].Name == "" {
|
||||||
|
return rs[0].Typ
|
||||||
|
}
|
||||||
|
return "(" + formatParams(rs) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
// hexDump returns a one-line hex dump of buf, truncated to 64 bytes.
|
||||||
|
func hexDump(buf []byte) string {
|
||||||
|
const max = 64
|
||||||
|
n := len(buf)
|
||||||
|
if n > max {
|
||||||
|
n = max
|
||||||
|
}
|
||||||
|
var sb strings.Builder
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if i > 0 {
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&sb, "%02x", buf[i])
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s%s (%d bytes)", sb.String(), truncMark(len(buf), max), len(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
// truncMark returns "…" when the buffer is longer than max, else "".
|
||||||
|
func truncMark(n, max int) string {
|
||||||
|
if n > max {
|
||||||
|
return "…"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|||||||
+113
-6
@@ -33,9 +33,17 @@ type Session struct {
|
|||||||
// JIT code, calls PTRACE_TRACEME and raises SIGSTOP; Launch waits for
|
// JIT code, calls PTRACE_TRACEME and raises SIGSTOP; Launch waits for
|
||||||
// that initial stop and returns a ready Session.
|
// that initial stop and returns a ready Session.
|
||||||
func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
||||||
|
sess, _, err := LaunchWithBuffers(gasmBin, asmPath, funcName, args, "")
|
||||||
|
return sess, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// LaunchWithBuffers is like Launch but also allocates buffers in the debuggee
|
||||||
|
// based on the buffer specification. Returns the Session and the buffer
|
||||||
|
// addresses (in the order they appear in the spec).
|
||||||
|
func LaunchWithBuffers(gasmBin, asmPath, funcName string, args []byte, bufSpec string) (*Session, []uint64, error) {
|
||||||
self, err := os.Executable()
|
self, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("debug: cannot find gasm binary: %w", err)
|
return nil, nil, fmt.Errorf("debug: cannot find gasm binary: %w", err)
|
||||||
}
|
}
|
||||||
if gasmBin != "" {
|
if gasmBin != "" {
|
||||||
self = gasmBin
|
self = gasmBin
|
||||||
@@ -44,12 +52,20 @@ func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
|||||||
// Write the arg block to a temp file (the child reads it).
|
// Write the arg block to a temp file (the child reads it).
|
||||||
tmpDir, err := os.MkdirTemp("", "gasm-debug-*")
|
tmpDir, err := os.MkdirTemp("", "gasm-debug-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("debug: tempdir: %w", err)
|
return nil, nil, fmt.Errorf("debug: tempdir: %w", err)
|
||||||
}
|
}
|
||||||
argsFile := filepath.Join(tmpDir, "args.bin")
|
argsFile := filepath.Join(tmpDir, "args.bin")
|
||||||
if err := os.WriteFile(argsFile, args, 0o644); err != nil {
|
if err := os.WriteFile(argsFile, args, 0o644); err != nil {
|
||||||
os.RemoveAll(tmpDir)
|
os.RemoveAll(tmpDir)
|
||||||
return nil, fmt.Errorf("debug: write args: %w", err)
|
return nil, nil, fmt.Errorf("debug: write args: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the buffer spec if present.
|
||||||
|
if bufSpec != "" {
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "bufspec"), []byte(bufSpec), 0o644); err != nil {
|
||||||
|
os.RemoveAll(tmpDir)
|
||||||
|
return nil, nil, fmt.Errorf("debug: write bufspec: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(self, "debug", "--target", "--func", funcName, "--args", argsFile, asmPath)
|
cmd := exec.Command(self, "debug", "--target", "--func", funcName, "--args", argsFile, asmPath)
|
||||||
@@ -60,7 +76,7 @@ func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
|||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
os.RemoveAll(tmpDir)
|
os.RemoveAll(tmpDir)
|
||||||
return nil, fmt.Errorf("debug: start debuggee: %w", err)
|
return nil, nil, fmt.Errorf("debug: start debuggee: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Session{pid: cmd.Process.Pid, cmd: cmd}
|
s := &Session{pid: cmd.Process.Pid, cmd: cmd}
|
||||||
@@ -79,8 +95,28 @@ func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
|||||||
if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil {
|
if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil {
|
||||||
cmd.Process.Kill()
|
cmd.Process.Kill()
|
||||||
os.RemoveAll(tmpDir)
|
os.RemoveAll(tmpDir)
|
||||||
return nil, fmt.Errorf("debug: wait for debuggee: %w", err)
|
return nil, nil, fmt.Errorf("debug: wait for stop: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wait for the debuggee to reach the function entry point.
|
||||||
|
entryFile := filepath.Join(tmpDir, "entry")
|
||||||
|
for i := 0; i < 500; i++ {
|
||||||
|
if _, err := os.Stat(entryFile); err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(5 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue the debuggee to the entry point.
|
||||||
|
if err := s.Continue(); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("debug: continue to entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the entry stop.
|
||||||
|
if _, err := syscall.Wait4(s.pid, &ws, syscall.WUNTRACED, nil); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("debug: wait for entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
s.stopped = true
|
s.stopped = true
|
||||||
|
|
||||||
// Read the code base from /proc/pid/maps (find the RWX mapping).
|
// Read the code base from /proc/pid/maps (find the RWX mapping).
|
||||||
@@ -93,7 +129,21 @@ func Launch(gasmBin, asmPath, funcName string, args []byte) (*Session, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
// Read buffer addresses if buffers were allocated.
|
||||||
|
var bufAddrs []uint64
|
||||||
|
if bufSpec != "" {
|
||||||
|
addrFile := filepath.Join(tmpDir, "bufaddrs")
|
||||||
|
if data, err := os.ReadFile(addrFile); err == nil {
|
||||||
|
for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") {
|
||||||
|
var addr uint64
|
||||||
|
if _, err := fmt.Sscanf(line, "%d", &addr); err == nil {
|
||||||
|
bufAddrs = append(bufAddrs, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, bufAddrs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait waits for the debuggee to stop and returns the wait status.
|
// wait waits for the debuggee to stop and returns the wait status.
|
||||||
@@ -144,6 +194,63 @@ func (s *Session) SetRegs(regs *Regs) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FPRegs holds the x87 FPU and SSE (XMM) register state from PTRACE_GETFPREGS.
|
||||||
|
type FPRegs struct {
|
||||||
|
FCW uint16
|
||||||
|
FSW uint16
|
||||||
|
FTW byte
|
||||||
|
FOP uint16
|
||||||
|
FIP uint64
|
||||||
|
FCS uint16
|
||||||
|
FDP uint64
|
||||||
|
FDS uint16
|
||||||
|
MXCSR uint32
|
||||||
|
MXCSRMask uint32
|
||||||
|
ST [8][16]byte // x87 stack (10 bytes per reg, padded to 16)
|
||||||
|
XMM [16][16]byte // XMM0-15
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFPRegs retrieves the FPU/SSE register state of the stopped debuggee.
|
||||||
|
func (s *Session) GetFPRegs() (FPRegs, error) {
|
||||||
|
var fp FPRegs
|
||||||
|
_, _, errno := syscall.Syscall6(
|
||||||
|
syscall.SYS_PTRACE,
|
||||||
|
uintptr(syscall.PTRACE_GETFPREGS),
|
||||||
|
uintptr(s.pid),
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&fp)),
|
||||||
|
0, 0,
|
||||||
|
)
|
||||||
|
if errno != 0 {
|
||||||
|
return fp, fmt.Errorf("debug: PTRACE_GETFPREGS: %w", errno)
|
||||||
|
}
|
||||||
|
return fp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VectorRegs holds the YMM register state extracted from XSAVE.
|
||||||
|
type VectorRegs struct {
|
||||||
|
YMM [16][32]byte // YMM0-15 (full 256-bit values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVectorRegs retrieves the YMM registers via PTRACE_GETREGSET + XSAVE.
|
||||||
|
// Falls back to XMM if XSAVE is unavailable.
|
||||||
|
func (s *Session) GetVectorRegs() (VectorRegs, error) {
|
||||||
|
var v VectorRegs
|
||||||
|
fp, err := s.GetFPRegs()
|
||||||
|
if err != nil {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
// PTRACE_GETFPREGS gives XMM registers (lower 128 bits).
|
||||||
|
// For YMM we'd need XSAVE; for now, copy XMM and zero the upper half.
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
for j := 0; j < 16; j++ {
|
||||||
|
v.YMM[i][j] = fp.XMM[i][j]
|
||||||
|
}
|
||||||
|
// Upper 128 bits would come from XSAVE, not available via GETFPREGS.
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Peek reads a word (8 bytes) from the debuggee's memory at addr.
|
// Peek reads a word (8 bytes) from the debuggee's memory at addr.
|
||||||
// Uses /proc/pid/mem which works reliably with Go's multi-threaded runtime.
|
// Uses /proc/pid/mem which works reliably with Go's multi-threaded runtime.
|
||||||
func (s *Session) Peek(addr uint64) (uint64, error) {
|
func (s *Session) Peek(addr uint64) (uint64, error) {
|
||||||
|
|||||||
+28
-15
@@ -34,21 +34,7 @@ type SourceLine struct {
|
|||||||
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine) {
|
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine) {
|
||||||
entryAddr := codeBase + uint64(funcOffset)
|
entryAddr := codeBase + uint64(funcOffset)
|
||||||
|
|
||||||
// Run to the function entry.
|
// The debuggee is already stopped at the function entry point.
|
||||||
bp, err := bm.Set(entryAddr, "(entry)")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("warning: cannot set entry breakpoint: %v\n", err)
|
|
||||||
} else {
|
|
||||||
if err := s.Continue(); err != nil {
|
|
||||||
fmt.Printf("warning: continue to entry: %v\n", err)
|
|
||||||
}
|
|
||||||
regs, _ := s.GetRegs()
|
|
||||||
bm.HandleTrap(®s)
|
|
||||||
// Remove the temporary entry breakpoint.
|
|
||||||
bm.Clear(entryAddr)
|
|
||||||
_ = bp
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize)
|
fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize)
|
||||||
fmt.Println("commands: break <label|addr> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit")
|
fmt.Println("commands: break <label|addr> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit")
|
||||||
|
|
||||||
@@ -78,6 +64,13 @@ func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, ar
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
printRegs(®s, codeBase, uint64(funcOffset))
|
printRegs(®s, codeBase, uint64(funcOffset))
|
||||||
|
// Also show vector registers.
|
||||||
|
vregs, err := s.GetVectorRegs()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" (vector regs unavailable: %v)\n", err)
|
||||||
|
} else {
|
||||||
|
printVectorRegs(&vregs)
|
||||||
|
}
|
||||||
|
|
||||||
case "step", "s":
|
case "step", "s":
|
||||||
n := 1
|
n := 1
|
||||||
@@ -508,6 +501,26 @@ func printRegs(regs *Regs, codeBase, funcOff uint64) {
|
|||||||
fmt.Printf(" RFLAGS = %#x [%s]\n", regs.RFLAGS, decodeRflags(regs.RFLAGS))
|
fmt.Printf(" RFLAGS = %#x [%s]\n", regs.RFLAGS, decodeRflags(regs.RFLAGS))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printVectorRegs displays the YMM registers.
|
||||||
|
func printVectorRegs(v *VectorRegs) {
|
||||||
|
fmt.Println("\n Vector registers (YMM):")
|
||||||
|
for i := 0; i < 16; i += 2 {
|
||||||
|
fmt.Printf(" YMM%-2d = ", i)
|
||||||
|
printYMM(v.YMM[i][:])
|
||||||
|
fmt.Printf(" YMM%-2d = ", i+1)
|
||||||
|
printYMM(v.YMM[i+1][:])
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printYMM(b []byte) {
|
||||||
|
// Show as 8 32-bit values.
|
||||||
|
for j := 0; j < 32; j += 4 {
|
||||||
|
v := uint32(b[j]) | uint32(b[j+1])<<8 | uint32(b[j+2])<<16 | uint32(b[j+3])<<24
|
||||||
|
fmt.Printf("%08x ", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func decodeRflags(f uint64) string {
|
func decodeRflags(f uint64) string {
|
||||||
var flags string
|
var flags string
|
||||||
if f&1 != 0 {
|
if f&1 != 0 {
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
package debug
|
package debug
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
@@ -85,6 +88,15 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
|||||||
args = padded
|
args = padded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read buffer specification if present.
|
||||||
|
bufSpecFile := tmpDir + "/bufspec"
|
||||||
|
if bufSpec, err := os.ReadFile(bufSpecFile); err == nil && len(bufSpec) > 0 {
|
||||||
|
args, err = setupBuffers(string(bufSpec), args, fl.Args, tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("debug target: setup buffers: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Lock this goroutine to the current OS thread so the parent's
|
// Lock this goroutine to the current OS thread so the parent's
|
||||||
// ptrace (attached to this thread) controls the JIT execution.
|
// ptrace (attached to this thread) controls the JIT execution.
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
@@ -100,6 +112,11 @@ func RunTarget(asmPath, funcName, argsFile, tmpDir string) error {
|
|||||||
|
|
||||||
// --- Execution resumes here after the parent continues us ---
|
// --- Execution resumes here after the parent continues us ---
|
||||||
|
|
||||||
|
// Stop at the function entry point so the debugger can set breakpoints.
|
||||||
|
// The parent will continue us when ready.
|
||||||
|
os.WriteFile(tmpDir+"/entry", []byte("ok"), 0o644)
|
||||||
|
syscall.Kill(syscall.Getpid(), syscall.SIGSTOP)
|
||||||
|
|
||||||
// Prepare the ABI0 stack and call the function.
|
// Prepare the ABI0 stack and call the function.
|
||||||
fnAddr := codeBase + uintptr(fl.Offset)
|
fnAddr := codeBase + uintptr(fl.Offset)
|
||||||
stackArgs := make([]byte, fl.Args)
|
stackArgs := make([]byte, fl.Args)
|
||||||
@@ -129,3 +146,82 @@ func mapRWX(code []byte) ([]byte, error) {
|
|||||||
copy(mem, code)
|
copy(mem, code)
|
||||||
return mem, nil
|
return mem, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setupBuffers allocates buffers in the debuggee's memory and updates the
|
||||||
|
// argument block with pointers to them.
|
||||||
|
// Format: name:size:pattern[,name:size:pattern...]
|
||||||
|
// Patterns: zero, ones, seq, or hex (e.g. "deadbeef").
|
||||||
|
func setupBuffers(spec string, args []byte, argSize int, tmpDir string) ([]byte, error) {
|
||||||
|
// Parse the buffer spec.
|
||||||
|
type bufSpec struct {
|
||||||
|
name string
|
||||||
|
size int
|
||||||
|
pattern string
|
||||||
|
}
|
||||||
|
var specs []bufSpec
|
||||||
|
for _, part := range strings.Split(spec, ",") {
|
||||||
|
fields := strings.SplitN(part, ":", 3)
|
||||||
|
if len(fields) != 3 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
size, err := strconv.Atoi(fields[1])
|
||||||
|
if err != nil || size <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
specs = append(specs, bufSpec{name: fields[0], size: size, pattern: fields[2]})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(specs) == 0 {
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate buffers and write their addresses to a file for the parent.
|
||||||
|
var bufAddrs []uint64
|
||||||
|
for _, s := range specs {
|
||||||
|
buf, err := syscall.Mmap(-1, 0, s.size,
|
||||||
|
syscall.PROT_READ|syscall.PROT_WRITE,
|
||||||
|
syscall.MAP_PRIVATE|syscall.MAP_ANON)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("mmap buffer %s: %w", s.name, err)
|
||||||
|
}
|
||||||
|
fillBuffer(buf, s.pattern)
|
||||||
|
bufAddrs = append(bufAddrs, uint64(uintptr(unsafe.Pointer(&buf[0]))))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write buffer addresses to a file for the parent to read.
|
||||||
|
addrFile, err := os.Create(tmpDir + "/bufaddrs")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, addr := range bufAddrs {
|
||||||
|
fmt.Fprintf(addrFile, "%d\n", addr)
|
||||||
|
}
|
||||||
|
addrFile.Close()
|
||||||
|
|
||||||
|
// For now, return the args unchanged. The parent will read bufaddrs
|
||||||
|
// and construct the final argument block with the correct pointers.
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillBuffer fills a buffer with the specified pattern.
|
||||||
|
func fillBuffer(buf []byte, pattern string) {
|
||||||
|
switch pattern {
|
||||||
|
case "zero":
|
||||||
|
// Already zeroed by mmap.
|
||||||
|
case "ones":
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = 0xFF
|
||||||
|
}
|
||||||
|
case "seq":
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = byte(i)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Try to parse as hex.
|
||||||
|
if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 {
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = data[i%len(data)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+25
-3
@@ -194,6 +194,13 @@ with the Plan 9 operand order (source first) mapped onto the x86 encoding.
|
|||||||
Every encoding is validated by decoding it again with `golang.org/x/arch` — the
|
Every encoding is validated by decoding it again with `golang.org/x/arch` — the
|
||||||
one module dependency, used in tests only and never linked into the binary.
|
one module dependency, used in tests only and never linked into the binary.
|
||||||
|
|
||||||
|
A **RISC-V encoder** (Phase 5, RV64IMAFDC + RVC compression) encodes the full
|
||||||
|
integer, atomic, float/double, FMA and CSR instruction sets with the MOV
|
||||||
|
pseudo-instruction and SB/global symbol references (AUIPC pairs with
|
||||||
|
R_RISCV_PCREL_HI20/LO12 relocations). The encoder compresses eligible
|
||||||
|
instructions to 16-bit RVC forms and is validated byte-for-byte against
|
||||||
|
`GOARCH=riscv64 go tool asm`.
|
||||||
|
|
||||||
On top of the encoder, `Assemble` walks a parsed `TEXT` body, converts each
|
On top of the encoder, `Assemble` walks a parsed `TEXT` body, converts each
|
||||||
operand to an encoder operand, and lays the instructions out so local labels
|
operand to an encoder operand, and lays the instructions out so local labels
|
||||||
resolve to relative jump offsets: jumps start in the short (rel8) form and
|
resolve to relative jump offsets: jumps start in the short (rel8) form and
|
||||||
@@ -281,7 +288,9 @@ boundaries, plus flat `pcfile`, `pcline` and `pcinline` tables — so a
|
|||||||
gasm-assembled object drops into a `go build` in place of the toolchain's.
|
gasm-assembled object drops into a `go build` in place of the toolchain's.
|
||||||
The object preamble (the version-and-experiment header the linker compares
|
The object preamble (the version-and-experiment header the linker compares
|
||||||
verbatim) is captured from the installed `go tool asm`, so the output is
|
verbatim) is captured from the installed `go tool asm`, so the output is
|
||||||
always consistent with the toolchain that links it. External cross-package
|
always consistent with the toolchain that links it. RISC-V GOOBJ emission
|
||||||
|
uses the same format with the RISC-V architecture marker and RISC-V relocation
|
||||||
|
types. External cross-package
|
||||||
references and the implicit funcdata/DWARF symbols remain future work (the
|
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
|
linker fills the latter's defaults); the rest of Phase 2 is those, the
|
||||||
remaining EVEX forms and the other architectures.
|
remaining EVEX forms and the other architectures.
|
||||||
@@ -309,7 +318,18 @@ assembler’s `Image.Bytes()` provides the code-and-data concatenation.
|
|||||||
|
|
||||||
The `gasm verify` CLI subcommand exposes this: it loads a file, reports the
|
The `gasm verify` CLI subcommand exposes this: it loads a file, reports the
|
||||||
available functions and (with `-smoke`) calls each NOSPLIT function with zeroed
|
available functions and (with `-smoke`) calls each NOSPLIT function with zeroed
|
||||||
arguments to confirm the trampoline round-trips.
|
arguments to confirm the trampoline round-trips. `gasm verify --fuzz` combines
|
||||||
|
ABI checks (sentinel registers, canary, stack bounds) with differential fuzz
|
||||||
|
testing, comparing the JIT-assembled kernel against the portable Go reference
|
||||||
|
bit-for-bit while verifying the ABI contract on every iteration. When a fuzz
|
||||||
|
iteration crashes or mismatches, `FuzzResult.CrashInput` stores the exact input
|
||||||
|
for reproducibility. `gasm verify --call <func> --buf name:size:pattern`
|
||||||
|
invokes a single function with user-supplied buffers (patterns: zero, ones,
|
||||||
|
seq, or hex), printing the ABI0 argument block before and after the call —
|
||||||
|
useful for partial functions (e.g. decoders) that crash on random input but
|
||||||
|
should succeed on valid data. `gasm verify --ground-truth` compares the
|
||||||
|
assembled machine code byte-for-byte against `go tool asm` (relocation sites
|
||||||
|
masked), reporting any encoding drift.
|
||||||
|
|
||||||
### `debug`
|
### `debug`
|
||||||
|
|
||||||
@@ -320,7 +340,9 @@ execution. Breakpoints are patched as INT3 bytes through `/proc/pid/mem`
|
|||||||
(PTRACE_PEEKTEXT is unreliable with Go's multi-threaded runtime).
|
(PTRACE_PEEKTEXT is unreliable with Go's multi-threaded runtime).
|
||||||
The child pins its goroutine to the OS thread with `runtime.LockOSThread`
|
The child pins its goroutine to the OS thread with `runtime.LockOSThread`
|
||||||
so the traced thread is the one executing JIT code. The REPL provides
|
so the traced thread is the one executing JIT code. The REPL provides
|
||||||
single-step, register inspection, label resolution, and breakpoint
|
single-step, register inspection (GPR + YMM/XMM via `PTRACE_GETFPREGS`),
|
||||||
|
label resolution, named buffer allocation with pattern filling
|
||||||
|
(`--buf name:size:pattern` — zero, ones, seq, or hex), and breakpoint
|
||||||
management.
|
management.
|
||||||
|
|
||||||
## Extension points
|
## Extension points
|
||||||
|
|||||||
+38
-3
@@ -70,14 +70,25 @@ Assemble FILE, map it into executable memory, and run dynamic checks.
|
|||||||
| `--fuzz` | Differential fuzz: JIT both gasm and go-tool-asm, compare outputs |
|
| `--fuzz` | Differential fuzz: JIT both gasm and go-tool-asm, compare outputs |
|
||||||
| `-n` | Fuzz iterations per function (default: 1000) |
|
| `-n` | Fuzz iterations per function (default: 1000) |
|
||||||
| `--abi` | Run ABI-checking calls (sentinel registers + red zone) |
|
| `--abi` | Run ABI-checking calls (sentinel registers + red zone) |
|
||||||
|
| `--abi-n` | Number of ABI check iterations with varied inputs (default: 100) |
|
||||||
| `--profile` | List basic-block structure per function |
|
| `--profile` | List basic-block structure per function |
|
||||||
| `--smoke` | Call each NOSPLIT function with zeroed args |
|
| `--smoke` | Call each NOSPLIT function with zeroed args |
|
||||||
|
| `--call <func>` | Invoke a single function with `--buf` instead of the sweeps |
|
||||||
|
| `--buf <spec>` | Buffer spec for `--call`: `name:size:pattern[,name:size:pattern]` |
|
||||||
|
| `--repeat <n>` | Number of times to repeat a `--call` invocation (default: 1) |
|
||||||
|
|
||||||
The `--fuzz` mode runs each function in a subprocess; a partial function
|
The `--fuzz` mode runs each function in a subprocess; a partial function
|
||||||
(e.g. a decoder that faults on malformed input) is reported as
|
(e.g. a decoder that faults on malformed input) is reported as
|
||||||
`CRASH` without killing the parent. Use `--ground-truth` for decoders.
|
`CRASH` without killing the parent. Use `--call` with `--buf` to invoke
|
||||||
|
partial functions with valid data instead.
|
||||||
|
|
||||||
## `gasm debug --func <name> <file.s>`
|
The `--call` mode parses the `// func` signature, allocates the requested
|
||||||
|
buffers (`zero`, `ones`, `seq`, or a hex blob), builds the ABI0 argument
|
||||||
|
block with buffer pointers/lengths/capacities at the matching parameter
|
||||||
|
offsets, and prints the arg block before and after the call — showing
|
||||||
|
return values and any output written to the buffers.
|
||||||
|
|
||||||
|
## `gasm debug --func <name> [--buf spec] <file.s>`
|
||||||
|
|
||||||
Interactive debugger for JIT-assembled amd64 functions. Requires a
|
Interactive debugger for JIT-assembled amd64 functions. Requires a
|
||||||
compiled binary on `$PATH` (not `go run`).
|
compiled binary on `$PATH` (not `go run`).
|
||||||
@@ -85,6 +96,7 @@ compiled binary on `$PATH` (not `go run`).
|
|||||||
| Flag | Description |
|
| Flag | Description |
|
||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `--func` | Function to debug (required) |
|
| `--func` | Function to debug (required) |
|
||||||
|
| `--buf` | Buffer spec: `name:size:pattern[,name:size:pattern]` |
|
||||||
|
|
||||||
REPL commands:
|
REPL commands:
|
||||||
|
|
||||||
@@ -93,11 +105,34 @@ REPL commands:
|
|||||||
| `break <label\|addr>` | Set a breakpoint |
|
| `break <label\|addr>` | Set a breakpoint |
|
||||||
| `step [n]` | Single-step n instructions |
|
| `step [n]` | Single-step n instructions |
|
||||||
| `continue` | Run until next breakpoint or exit |
|
| `continue` | Run until next breakpoint or exit |
|
||||||
| `regs` | Print general-purpose registers |
|
| `regs` | Print general-purpose + YMM/XMM vector registers |
|
||||||
| `x [addr] [len]` | Hex-dump memory |
|
| `x [addr] [len]` | Hex-dump memory |
|
||||||
| `labels` | List function labels and offsets |
|
| `labels` | List function labels and offsets |
|
||||||
| `quit` | Kill the debuggee and exit |
|
| `quit` | Kill the debuggee and exit |
|
||||||
|
|
||||||
|
## `gasm diff [--map old=new,...] <file1.s> <file2.s>`
|
||||||
|
|
||||||
|
Compare the machine code produced by assembling two files. Shows which
|
||||||
|
functions differ and the first few differing bytes. Useful for verifying
|
||||||
|
that two implementations produce identical code, or for tracking encoding
|
||||||
|
changes between Go assembler versions.
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `--map` | Comma-separated `old=new` pairs to match functions with different names |
|
||||||
|
|
||||||
|
Without `--map`, functions are paired by exact name. With `--map`, a
|
||||||
|
function named `old` in the first file is compared against the function
|
||||||
|
named `new` in the second file (e.g. `--map wideCopyAVX2=wideCopyAVX512`
|
||||||
|
pairs AVX2 and AVX-512 variants regardless of suffix).
|
||||||
|
|
||||||
|
## `gasm profile <file.s>`
|
||||||
|
|
||||||
|
Show the basic-block structure of functions in an assembly file. Lists
|
||||||
|
each function's labels, their offsets, and the block boundaries. This is
|
||||||
|
the static structure; for runtime execution counts, use `gasm verify
|
||||||
|
--fuzz` which exercises the code paths.
|
||||||
|
|
||||||
## `gasm lsp`
|
## `gasm lsp`
|
||||||
|
|
||||||
Run the language server over standard input/output (JSON-RPC 2.0 with
|
Run the language server over standard input/output (JSON-RPC 2.0 with
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package format
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -172,11 +171,9 @@ func TestIdempotent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestRoundTrip checks that formatting produces source that still parses
|
// TestRoundTrip checks that formatting produces source that still parses
|
||||||
// cleanly, on the fixture and on the real go-flac kernels when present.
|
// cleanly on the in-repository fixture.
|
||||||
func TestRoundTrip(t *testing.T) {
|
func TestRoundTrip(t *testing.T) {
|
||||||
files := []string{"../testdata/sample_amd64.s"}
|
files := []string{"../testdata/sample_amd64.s"}
|
||||||
real, _ := filepath.Glob("../../go-libraries/go-*/*.s")
|
|
||||||
files = append(files, real...)
|
|
||||||
for _, path := range files {
|
for _, path := range files {
|
||||||
src, err := os.ReadFile(path)
|
src, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build integration
|
||||||
|
|
||||||
|
// Package format integration tests against the production go-libraries kernels.
|
||||||
|
// Excluded from the default test run so coverage is identical locally and in CI.
|
||||||
|
// Run explicitly with: go test -tags=integration ./format/
|
||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRoundTripRealGoLibraries checks that formatting the production kernels
|
||||||
|
// still produces source that parses cleanly.
|
||||||
|
func TestRoundTripRealGoLibraries(t *testing.T) {
|
||||||
|
real, _ := filepath.Glob("../../go-libraries/go-*/*.s")
|
||||||
|
if len(real) == 0 {
|
||||||
|
t.Skip("go-libraries repository not present")
|
||||||
|
}
|
||||||
|
for _, path := range real {
|
||||||
|
src, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
formatted := Source(path, string(src))
|
||||||
|
if _, errs := parser.Parse(path, formatted); len(errs) > 0 {
|
||||||
|
t.Errorf("formatted %s no longer parses: %v", path, errs)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(formatted) == "" {
|
||||||
|
t.Errorf("formatted %s is empty", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
|
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
|
||||||
|
|
||||||
version := "0.28.0"
|
version := "0.29.0"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@just --list
|
@just --list
|
||||||
@@ -18,8 +18,21 @@ build:
|
|||||||
@test -z "$(gofmt -l .)" || { echo "gofmt diff:"; gofmt -l .; exit 1; }
|
@test -z "$(gofmt -l .)" || { echo "gofmt diff:"; gofmt -l .; exit 1; }
|
||||||
|
|
||||||
# Full test suite + race detector + 80 % coverage gate.
|
# Full test suite + race detector + 80 % coverage gate.
|
||||||
|
# The coverage gate matches CI: it excludes packages that need hardware or
|
||||||
|
# are CLI glue (debug, cmd/gasm), so the number is identical locally and in CI.
|
||||||
test:
|
test:
|
||||||
go test -race -count=1 -coverprofile=coverage.out ./...
|
go test -race -count=1 ./...
|
||||||
|
go test -count=1 -coverprofile=coverage.out \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/arch \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/asm \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/ast \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/format \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/lexer \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/lint \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/lsp \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/parser \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/token \
|
||||||
|
sourcedock.dev/petrbalvin/gasm-devkit/verify
|
||||||
go tool cover -func=coverage.out | awk '/^total:/{gsub("%","",$3);if($3+0<80){print "coverage "$3"% < 80%";exit 1}print "coverage "$3"%"}'
|
go tool cover -func=coverage.out | awk '/^total:/{gsub("%","",$3);if($3+0<80){print "coverage "$3"% < 80%";exit 1}print "coverage "$3"%"}'
|
||||||
|
|
||||||
# Format all Go sources.
|
# Format all Go sources.
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build integration
|
||||||
|
|
||||||
|
// Package lint integration tests against the production go-libraries kernels.
|
||||||
|
// Excluded from the default test run so coverage is identical locally and in CI.
|
||||||
|
// Run explicitly with: go test -tags=integration ./lint/
|
||||||
|
package lint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRealGoLibrariesHasNoErrors asserts that the production go-flac kernels
|
||||||
|
// lint free of errors.
|
||||||
|
func TestRealGoLibrariesHasNoErrors(t *testing.T) {
|
||||||
|
matches, _ := filepath.Glob("../../go-libraries/go-*/*.s")
|
||||||
|
if len(matches) == 0 {
|
||||||
|
t.Skip("go-libraries repository not present")
|
||||||
|
}
|
||||||
|
for _, path := range matches {
|
||||||
|
src, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f, errs := parser.Parse(path, string(src))
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("parse %s: %v", path, errs)
|
||||||
|
}
|
||||||
|
a := arch.FromFilename(path)
|
||||||
|
diags := File(f, Config{Arch: a})
|
||||||
|
for _, d := range diags {
|
||||||
|
if d.Severity == Error {
|
||||||
|
t.Errorf("%s: %s %s: %s", filepath.Base(path), d.Pos, d.Code, d.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ package lint
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||||
@@ -234,29 +233,3 @@ TEXT ·f(SB), NOSPLIT, $0
|
|||||||
t.Fatalf("label rules should be suppressed in macro files: %+v", diags)
|
t.Fatalf("label rules should be suppressed in macro files: %+v", diags)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRealGoLibrariesHasNoErrors asserts that the production go-flac kernels
|
|
||||||
// lint free of errors. Skipped when the sibling repository is absent.
|
|
||||||
func TestRealGoLibrariesHasNoErrors(t *testing.T) {
|
|
||||||
matches, _ := filepath.Glob("../../go-libraries/go-*/*.s")
|
|
||||||
if len(matches) == 0 {
|
|
||||||
t.Skip("go-libraries repository not present")
|
|
||||||
}
|
|
||||||
for _, path := range matches {
|
|
||||||
src, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
f, errs := parser.Parse(path, string(src))
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("parse %s: %v", path, errs)
|
|
||||||
}
|
|
||||||
a := arch.FromFilename(path)
|
|
||||||
diags := File(f, Config{Arch: a})
|
|
||||||
for _, d := range diags {
|
|
||||||
if d.Severity == Error {
|
|
||||||
t.Errorf("%s: %s %s: %s", filepath.Base(path), d.Pos, d.Code, d.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -90,6 +90,41 @@ func (s *Server) hover(p hoverParams) *Hover {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// definition returns the location of the label definition for a label reference.
|
||||||
|
func (s *Server) definition(p definitionParams) []Location {
|
||||||
|
text := s.docs[p.TextDocument.URI]
|
||||||
|
word, _ := wordAt(text, p.Position)
|
||||||
|
if word == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the document to find label definitions.
|
||||||
|
f, errs := parser.Parse(uriPath(p.TextDocument.URI), text)
|
||||||
|
if f == nil || len(errs) > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the label definition.
|
||||||
|
for _, d := range f.Decls {
|
||||||
|
if t, ok := d.(*ast.Text); ok {
|
||||||
|
for _, stmt := range t.Body {
|
||||||
|
if lbl, ok := stmt.(*ast.Label); ok {
|
||||||
|
if lbl.Name.Text == word {
|
||||||
|
return []Location{{
|
||||||
|
URI: p.TextDocument.URI,
|
||||||
|
Range: Range{
|
||||||
|
Start: Position{Line: lbl.Name.Pos.Line - 1, Character: lbl.Name.Pos.Column - 1},
|
||||||
|
End: Position{Line: lbl.Name.Pos.Line - 1, Character: lbl.Name.Pos.Column - 1 + len(word)},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// documentSymbols returns functions and their labels, plus global symbols.
|
// documentSymbols returns functions and their labels, plus global symbols.
|
||||||
func (s *Server) documentSymbols(p documentSymbolParams) []DocumentSymbol {
|
func (s *Server) documentSymbols(p documentSymbolParams) []DocumentSymbol {
|
||||||
text := s.docs[p.TextDocument.URI]
|
text := s.docs[p.TextDocument.URI]
|
||||||
|
|||||||
@@ -152,6 +152,11 @@ type hoverParams struct {
|
|||||||
Position Position `json:"position"`
|
Position Position `json:"position"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type definitionParams struct {
|
||||||
|
TextDocument textDocumentIdentifier `json:"textDocument"`
|
||||||
|
Position Position `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
// Hover is the hover response.
|
// Hover is the hover response.
|
||||||
type Hover struct {
|
type Hover struct {
|
||||||
Contents markupContent `json:"contents"`
|
Contents markupContent `json:"contents"`
|
||||||
|
|||||||
@@ -170,6 +170,11 @@ func (s *Server) dispatch(msg *rpcMessage) (exit bool) {
|
|||||||
json.Unmarshal(msg.Params, &p)
|
json.Unmarshal(msg.Params, &p)
|
||||||
s.respond(msg.ID, s.hover(p))
|
s.respond(msg.ID, s.hover(p))
|
||||||
|
|
||||||
|
case "textDocument/definition":
|
||||||
|
var p definitionParams
|
||||||
|
json.Unmarshal(msg.Params, &p)
|
||||||
|
s.respond(msg.ID, s.definition(p))
|
||||||
|
|
||||||
case "textDocument/documentSymbol":
|
case "textDocument/documentSymbol":
|
||||||
var p documentSymbolParams
|
var p documentSymbolParams
|
||||||
json.Unmarshal(msg.Params, &p)
|
json.Unmarshal(msg.Params, &p)
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
//go:build integration
|
||||||
|
|
||||||
|
// Package parser integration tests against the production go-libraries kernels.
|
||||||
|
// Excluded from the default test run so coverage is identical locally and in CI.
|
||||||
|
// Run explicitly with: go test -tags=integration ./parser/
|
||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestParseRealGoLibraries parses every .s file in the sibling go-libraries
|
||||||
|
// repository when it is checked out, asserting a clean, error-free parse.
|
||||||
|
func TestParseRealGoLibraries(t *testing.T) {
|
||||||
|
matches, _ := filepath.Glob("../../go-libraries/go-*/*.s")
|
||||||
|
if len(matches) == 0 {
|
||||||
|
t.Skip("go-libraries repository not present next to gasm-devkit")
|
||||||
|
}
|
||||||
|
for _, path := range matches {
|
||||||
|
src, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read %s: %v", path, err)
|
||||||
|
}
|
||||||
|
file, errs := Parse(path, string(src))
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Errorf("parse %s: %v", path, errs)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(texts(file)) == 0 {
|
||||||
|
t.Errorf("parse %s: no TEXT functions found", path)
|
||||||
|
}
|
||||||
|
t.Logf("%s: %d decls, %d functions", filepath.Base(path), len(file.Decls), len(texts(file)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ package parser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||||
@@ -254,28 +253,3 @@ func TestDataWidthAndStatic(t *testing.T) {
|
|||||||
t.Errorf("mask24 DATA should be static, got %+v", datas[2].Name)
|
t.Errorf("mask24 DATA should be static, got %+v", datas[2].Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestParseRealGoLibraries parses every .s file in the sibling go-libraries
|
|
||||||
// repository when it is checked out, asserting a clean, error-free parse. It
|
|
||||||
// is skipped when the repository is not present.
|
|
||||||
func TestParseRealGoLibraries(t *testing.T) {
|
|
||||||
matches, _ := filepath.Glob("../../go-libraries/go-*/*.s")
|
|
||||||
if len(matches) == 0 {
|
|
||||||
t.Skip("go-libraries repository not present next to gasm-devkit")
|
|
||||||
}
|
|
||||||
for _, path := range matches {
|
|
||||||
src, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read %s: %v", path, err)
|
|
||||||
}
|
|
||||||
file, errs := Parse(path, string(src))
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Errorf("parse %s: %v", path, errs)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(texts(file)) == 0 {
|
|
||||||
t.Errorf("parse %s: no TEXT functions found", path)
|
|
||||||
}
|
|
||||||
t.Logf("%s: %d decls, %d functions", filepath.Base(path), len(file.Decls), len(texts(file)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package verify
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"unsafe"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadABIKernel(t *testing.T) *Kernel {
|
func loadABIKernel(t *testing.T) *Kernel {
|
||||||
@@ -79,55 +78,6 @@ func TestABIR14Clobbered(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func TestCallFuncCheckedErrors(t *testing.T) {
|
||||||
k := loadABIKernel(t)
|
k := loadABIKernel(t)
|
||||||
|
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
// 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,150 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BufSpec is one buffer allocation request parsed from the user's --buf spec.
|
||||||
|
type BufSpec struct {
|
||||||
|
Name string
|
||||||
|
Size int // declared slice length and capacity
|
||||||
|
Pattern string // "zero", "ones", "seq", or a hex blob
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBufSpec parses a "name:size:pattern[,name:size:pattern]" spec string
|
||||||
|
// into individual buffer specs. Empty input yields an empty slice.
|
||||||
|
func ParseBufSpec(spec string) ([]BufSpec, error) {
|
||||||
|
if spec == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var out []BufSpec
|
||||||
|
for _, part := range strings.Split(spec, ",") {
|
||||||
|
fields := strings.SplitN(part, ":", 3)
|
||||||
|
if len(fields) != 3 {
|
||||||
|
return nil, fmt.Errorf("verify: invalid buffer spec %q (expected name:size:pattern)", part)
|
||||||
|
}
|
||||||
|
var size int
|
||||||
|
if _, err := fmt.Sscanf(fields[1], "%d", &size); err != nil || size <= 0 {
|
||||||
|
return nil, fmt.Errorf("verify: invalid buffer size %q in %q", fields[1], part)
|
||||||
|
}
|
||||||
|
out = append(out, BufSpec{Name: fields[0], Size: size, Pattern: fields[2]})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// allocatedBuf is one live buffer in a pool.
|
||||||
|
type allocatedBuf struct {
|
||||||
|
spec BufSpec
|
||||||
|
data []byte // Size + safetyMargin bytes; the first Size are the live region
|
||||||
|
}
|
||||||
|
|
||||||
|
// safetyMargin is the extra bytes allocated past the declared size so SIMD
|
||||||
|
// over-reads and functions that read slightly past len never touch unmapped
|
||||||
|
// memory. Matches the margin used by the fuzz generator.
|
||||||
|
const safetyMargin = 8192
|
||||||
|
|
||||||
|
// BufPool is a set of allocated buffers held alive for the duration of one or
|
||||||
|
// more calls. Buffers live on the Go heap (the JIT call is in-process); the
|
||||||
|
// pool keeps the backing slices referenced so the GC does not collect them
|
||||||
|
// before the call returns.
|
||||||
|
type BufPool struct {
|
||||||
|
bufs []allocatedBuf
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alloc allocates and fills the buffers described by specs. The returned
|
||||||
|
// pool must be kept alive until every call using it has returned.
|
||||||
|
func (p *BufPool) Alloc(specs []BufSpec) error {
|
||||||
|
for _, s := range specs {
|
||||||
|
data := make([]byte, s.Size+safetyMargin)
|
||||||
|
fillBuffer(data, s.Pattern)
|
||||||
|
p.bufs = append(p.bufs, allocatedBuf{spec: s, data: data})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases the pool. No-op for Go-heap buffers, but keeps the API
|
||||||
|
// symmetric with debug's mmap-backed pool.
|
||||||
|
func (p *BufPool) Close() {
|
||||||
|
p.bufs = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// findByName returns the buffer with the given spec name, if any.
|
||||||
|
func (p *BufPool) findByName(name string) *allocatedBuf {
|
||||||
|
for i := range p.bufs {
|
||||||
|
if p.bufs[i].spec.Name == name {
|
||||||
|
return &p.bufs[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildArgs constructs an ABI0 argument block of argSize bytes for the given
|
||||||
|
// layout, placing each buffer's pointer/length/capacity at the matching
|
||||||
|
// parameter offset. Parameters whose names match a buffer spec get the
|
||||||
|
// buffer address; non-pointer parameters and unmatched pointers are zeroed.
|
||||||
|
//
|
||||||
|
// Matching is by exact name, then by prefix (a buffer named "src" matches a
|
||||||
|
// parameter named "src" or "srcBuf"), mirroring the debug allocator.
|
||||||
|
func (p *BufPool) BuildArgs(layout []ArgOffset, argSize int) []byte {
|
||||||
|
args := make([]byte, argSize)
|
||||||
|
for _, a := range layout {
|
||||||
|
if !a.IsPtr {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
buf := p.matchBuf(a.Name)
|
||||||
|
if buf == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if a.Offset+8 <= len(args) {
|
||||||
|
binary.LittleEndian.PutUint64(args[a.Offset:a.Offset+8], uint64(uintptr(unsafe.Pointer(&buf.data[0]))))
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(a.Typ, "[]") && a.Offset+24 <= len(args) {
|
||||||
|
binary.LittleEndian.PutUint64(args[a.Offset+8:a.Offset+16], uint64(buf.spec.Size))
|
||||||
|
binary.LittleEndian.PutUint64(args[a.Offset+16:a.Offset+24], uint64(buf.spec.Size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchBuf finds a buffer matching the parameter name (exact, then prefix).
|
||||||
|
func (p *BufPool) matchBuf(name string) *allocatedBuf {
|
||||||
|
if b := p.findByName(name); b != nil {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
for i := range p.bufs {
|
||||||
|
if strings.HasPrefix(name, p.bufs[i].spec.Name) {
|
||||||
|
return &p.bufs[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fillBuffer fills buf with the named pattern: "zero" (no-op, already zeroed),
|
||||||
|
// "ones" (0xFF), "seq" (i mod 256), or a hex blob repeated to fill.
|
||||||
|
func fillBuffer(buf []byte, pattern string) {
|
||||||
|
switch pattern {
|
||||||
|
case "zero":
|
||||||
|
// Already zeroed by make.
|
||||||
|
case "ones":
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = 0xFF
|
||||||
|
}
|
||||||
|
case "seq":
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = byte(i)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if data, err := hex.DecodeString(pattern); err == nil && len(data) > 0 {
|
||||||
|
for i := range buf {
|
||||||
|
buf[i] = data[i%len(data)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseParamsExported(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input string
|
||||||
|
names []string
|
||||||
|
types []string
|
||||||
|
isPtr []bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "dst []byte, src []byte",
|
||||||
|
names: []string{"dst", "src"},
|
||||||
|
types: []string{"[]byte", "[]byte"},
|
||||||
|
isPtr: []bool{true, true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "dst, src []byte",
|
||||||
|
names: []string{"dst", "src"},
|
||||||
|
types: []string{"[]byte", "[]byte"},
|
||||||
|
isPtr: []bool{true, true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "a, b int",
|
||||||
|
names: []string{"a", "b"},
|
||||||
|
types: []string{"int", "int"},
|
||||||
|
isPtr: []bool{false, false},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "src []byte, dst []byte",
|
||||||
|
names: []string{"src", "dst"},
|
||||||
|
types: []string{"[]byte", "[]byte"},
|
||||||
|
isPtr: []bool{true, true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "swin []int32, dstP []uint32, hist *[32]uint16",
|
||||||
|
names: []string{"swin", "dstP", "hist"},
|
||||||
|
types: []string{"[]int32", "[]uint32", "*[32]uint16"},
|
||||||
|
isPtr: []bool{true, true, true},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "n int, code int",
|
||||||
|
names: []string{"n", "code"},
|
||||||
|
types: []string{"int", "int"},
|
||||||
|
isPtr: []bool{false, false},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
params := parseParamsExported(tt.input)
|
||||||
|
if len(params) != len(tt.names) {
|
||||||
|
t.Errorf("parseParamsExported(%q): got %d params, want %d", tt.input, len(params), len(tt.names))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i, p := range params {
|
||||||
|
if p.Name != tt.names[i] {
|
||||||
|
t.Errorf("parseParamsExported(%q)[%d].Name = %q, want %q", tt.input, i, p.Name, tt.names[i])
|
||||||
|
}
|
||||||
|
if p.Typ != tt.types[i] {
|
||||||
|
t.Errorf("parseParamsExported(%q)[%d].Typ = %q, want %q", tt.input, i, p.Typ, tt.types[i])
|
||||||
|
}
|
||||||
|
if p.IsPointer() != tt.isPtr[i] {
|
||||||
|
t.Errorf("parseParamsExported(%q)[%d].IsPointer() = %v, want %v", tt.input, i, p.IsPointer(), tt.isPtr[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseBufSpec(t *testing.T) {
|
||||||
|
t.Run("empty", func(t *testing.T) {
|
||||||
|
specs, err := ParseBufSpec("")
|
||||||
|
if err != nil || len(specs) != 0 {
|
||||||
|
t.Errorf("ParseBufSpec(\"\") = %v, %v; want nil, nil", specs, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("single", func(t *testing.T) {
|
||||||
|
specs, err := ParseBufSpec("dst:64:zero")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(specs) != 1 || specs[0].Name != "dst" || specs[0].Size != 64 || specs[0].Pattern != "zero" {
|
||||||
|
t.Errorf("ParseBufSpec(\"dst:64:zero\") = %+v; want [{dst 64 zero}]", specs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple", func(t *testing.T) {
|
||||||
|
specs, err := ParseBufSpec("dst:64:zero,src:128:seq")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(specs) != 2 {
|
||||||
|
t.Fatalf("got %d specs, want 2", len(specs))
|
||||||
|
}
|
||||||
|
if specs[0].Name != "dst" || specs[1].Name != "src" {
|
||||||
|
t.Errorf("names = %s, %s; want dst, src", specs[0].Name, specs[1].Name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid", func(t *testing.T) {
|
||||||
|
_, err := ParseBufSpec("bad")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("ParseBufSpec(\"bad\") should error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("zero-size", func(t *testing.T) {
|
||||||
|
_, err := ParseBufSpec("dst:0:zero")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("ParseBufSpec(\"dst:0:zero\") should error on zero size")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBufPoolBuildArgs(t *testing.T) {
|
||||||
|
specs, err := ParseBufSpec("dst:64:seq,src:128:zero")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var pool BufPool
|
||||||
|
if err := pool.Alloc(specs); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer pool.Close()
|
||||||
|
|
||||||
|
// Layout for wideCopyAVX2(dst, src []byte): dst at 0, src at 24.
|
||||||
|
layout := []ArgOffset{
|
||||||
|
{Name: "dst", Typ: "[]byte", Offset: 0, Size: 24, IsPtr: true},
|
||||||
|
{Name: "src", Typ: "[]byte", Offset: 24, Size: 24, IsPtr: true},
|
||||||
|
}
|
||||||
|
args := pool.BuildArgs(layout, 48)
|
||||||
|
|
||||||
|
// dst.ptr should be non-zero.
|
||||||
|
if args[0] == 0 && args[1] == 0 && args[2] == 0 && args[3] == 0 {
|
||||||
|
t.Error("dst.ptr is zero; expected a buffer address")
|
||||||
|
}
|
||||||
|
// dst.len should be 64 (0x40).
|
||||||
|
if args[8] != 0x40 {
|
||||||
|
t.Errorf("dst.len = %d, want 64", args[8])
|
||||||
|
}
|
||||||
|
// dst.cap should be 64.
|
||||||
|
if args[16] != 0x40 {
|
||||||
|
t.Errorf("dst.cap = %d, want 64", args[16])
|
||||||
|
}
|
||||||
|
// src.ptr should be non-zero.
|
||||||
|
if args[24] == 0 && args[25] == 0 && args[26] == 0 && args[27] == 0 {
|
||||||
|
t.Error("src.ptr is zero; expected a buffer address")
|
||||||
|
}
|
||||||
|
// src.len should be 128 (0x80).
|
||||||
|
if args[32] != 0x80 {
|
||||||
|
t.Errorf("src.len = %d, want 128", args[32])
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ package verify
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"unsafe"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBlocks(t *testing.T) {
|
func TestBlocks(t *testing.T) {
|
||||||
@@ -24,62 +23,3 @@ func TestBlocks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Logf("sum blocks: %v", blocks)
|
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))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,295 +0,0 @@
|
|||||||
// 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))
|
|
||||||
}
|
|
||||||
@@ -1,585 +0,0 @@
|
|||||||
// 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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+38
-18
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
@@ -20,6 +21,7 @@ type FuzzResult struct {
|
|||||||
Matches int
|
Matches int
|
||||||
Mismatches int
|
Mismatches int
|
||||||
FirstFail string // description of the first mismatch ("" if none)
|
FirstFail string // description of the first mismatch ("" if none)
|
||||||
|
CrashInput []byte // input that caused the last crash/mismatch (nil if none)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OK returns true when all iterations matched.
|
// OK returns true when all iterations matched.
|
||||||
@@ -30,8 +32,12 @@ func (r FuzzResult) String() string {
|
|||||||
if r.OK() {
|
if r.OK() {
|
||||||
return fmt.Sprintf("%s: %d/%d iterations match", r.Func, r.Matches, r.Iterations)
|
return fmt.Sprintf("%s: %d/%d iterations match", r.Func, r.Matches, r.Iterations)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s: %d/%d match, %d MISMATCH — %s",
|
s := fmt.Sprintf("%s: %d/%d match, %d MISMATCH — %s",
|
||||||
r.Func, r.Matches, r.Iterations, r.Mismatches, r.FirstFail)
|
r.Func, r.Matches, r.Iterations, r.Mismatches, r.FirstFail)
|
||||||
|
if len(r.CrashInput) > 0 {
|
||||||
|
s += fmt.Sprintf("\n input: %x", r.CrashInput)
|
||||||
|
}
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// funcSig is a parsed // func signature from the assembly source.
|
// funcSig is a parsed // func signature from the assembly source.
|
||||||
@@ -74,22 +80,39 @@ func parseParams(s string) []param {
|
|||||||
if s == "" {
|
if s == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
fields := strings.Split(s, ",")
|
||||||
|
// First pass: extract the type from each field (if present).
|
||||||
|
types := make([]string, len(fields))
|
||||||
|
for i, field := range fields {
|
||||||
|
parts := strings.Fields(strings.TrimSpace(field))
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
types[i] = parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Propagate types backward: a field without a type inherits from the next
|
||||||
|
// field that has one (e.g. "dst" inherits "[]byte" from "src []byte").
|
||||||
|
for i := range fields {
|
||||||
|
if types[i] == "" {
|
||||||
|
for j := i + 1; j < len(fields); j++ {
|
||||||
|
if types[j] != "" {
|
||||||
|
types[i] = types[j]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
var out []param
|
var out []param
|
||||||
for _, field := range strings.Split(s, ",") {
|
for i, field := range fields {
|
||||||
field = strings.TrimSpace(field)
|
field = strings.TrimSpace(field)
|
||||||
if field == "" {
|
if field == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
parts := strings.Fields(field)
|
parts := strings.Fields(field)
|
||||||
if len(parts) == 1 {
|
typ := types[i]
|
||||||
// Unnamed: "int" or "[]byte".
|
if typ == "" {
|
||||||
out = append(out, param{typ: parts[0]})
|
out = append(out, param{typ: parts[0]})
|
||||||
} else {
|
} else {
|
||||||
// Named: "a []byte" or shared "a, b []int32" (handled by the
|
out = append(out, param{name: parts[0], typ: typ})
|
||||||
// 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
|
return out
|
||||||
@@ -160,6 +183,9 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
|
|||||||
// (e.g. histogram increments) don't corrupt the other's input.
|
// (e.g. histogram increments) don't corrupt the other's input.
|
||||||
gasmArgs, goArgs, bufs := genDualArgs(rng, sig, fl.Args)
|
gasmArgs, goArgs, bufs := genDualArgs(rng, sig, fl.Args)
|
||||||
|
|
||||||
|
// Save the current input for crash diagnostics.
|
||||||
|
result.CrashInput = gasmArgs
|
||||||
|
|
||||||
// Call the gasm version.
|
// Call the gasm version.
|
||||||
gasmOut, err := k.CallFunc(name, gasmArgs)
|
gasmOut, err := k.CallFunc(name, gasmArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -167,7 +193,7 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
|
|||||||
if result.FirstFail == "" {
|
if result.FirstFail == "" {
|
||||||
result.FirstFail = fmt.Sprintf("iter %d: gasm call: %v", i, err)
|
result.FirstFail = fmt.Sprintf("iter %d: gasm call: %v", i, err)
|
||||||
}
|
}
|
||||||
releaseBufs(bufs)
|
runtime.KeepAlive(bufs)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +204,7 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
|
|||||||
if result.FirstFail == "" {
|
if result.FirstFail == "" {
|
||||||
result.FirstFail = fmt.Sprintf("iter %d: go call: %v", i, err)
|
result.FirstFail = fmt.Sprintf("iter %d: go call: %v", i, err)
|
||||||
}
|
}
|
||||||
releaseBufs(bufs)
|
runtime.KeepAlive(bufs)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +222,7 @@ func (k *Kernel) FuzzFunc(name string, sig funcSig, goCode []byte, iterations in
|
|||||||
} else {
|
} else {
|
||||||
result.Matches++
|
result.Matches++
|
||||||
}
|
}
|
||||||
releaseBufs(bufs)
|
runtime.KeepAlive(bufs)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -359,9 +385,3 @@ func equalBytes(a, b []byte) bool {
|
|||||||
}
|
}
|
||||||
return true
|
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,167 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFuzzResultString(t *testing.T) {
|
||||||
|
t.Run("ok", func(t *testing.T) {
|
||||||
|
r := FuzzResult{Func: "add", Iterations: 100, Matches: 100}
|
||||||
|
s := r.String()
|
||||||
|
if s != "add: 100/100 iterations match" {
|
||||||
|
t.Errorf("String() = %q", s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("mismatch", func(t *testing.T) {
|
||||||
|
r := FuzzResult{Func: "mul", Iterations: 100, Matches: 95, Mismatches: 5, FirstFail: "iter 23"}
|
||||||
|
s := r.String()
|
||||||
|
if s != "mul: 95/100 match, 5 MISMATCH — iter 23" {
|
||||||
|
t.Errorf("String() = %q", s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("crash", func(t *testing.T) {
|
||||||
|
r := FuzzResult{Func: "dec", Iterations: 100, Matches: 99, Mismatches: 1, FirstFail: "SIGSEGV", CrashInput: []byte{0x01, 0x02}}
|
||||||
|
s := r.String()
|
||||||
|
if s != "dec: 99/100 match, 1 MISMATCH — SIGSEGV\n input: 0102" {
|
||||||
|
t.Errorf("String() = %q", s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArrayLen(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
typ string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"*[32]uint16", 32},
|
||||||
|
{"*[16]int32", 16},
|
||||||
|
{"bad", 1},
|
||||||
|
{"*[]", 1},
|
||||||
|
{"*[0x]", 1},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := arrayLen(tt.typ); got != tt.want {
|
||||||
|
t.Errorf("arrayLen(%q) = %d, want %d", tt.typ, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestElemSizeFor(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
typ string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"[]byte", 1}, {"[]uint8", 1}, {"[]int8", 1},
|
||||||
|
{"[]uint16", 2}, {"[]int16", 2},
|
||||||
|
{"[]uint32", 4}, {"[]int32", 4}, {"[]float32", 4},
|
||||||
|
{"[]uint64", 8}, {"[]int64", 8}, {"[]float64", 8},
|
||||||
|
{"[]unknown", 8},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := elemSizeFor(tt.typ); got != tt.want {
|
||||||
|
t.Errorf("elemSizeFor(%q) = %d, want %d", tt.typ, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEqualBytes(t *testing.T) {
|
||||||
|
if !equalBytes([]byte{1, 2, 3}, []byte{1, 2, 3}) {
|
||||||
|
t.Error("expected equal")
|
||||||
|
}
|
||||||
|
if equalBytes([]byte{1, 2}, []byte{1, 2, 3}) {
|
||||||
|
t.Error("different length: expected not equal")
|
||||||
|
}
|
||||||
|
if equalBytes([]byte{1, 2, 3}, []byte{1, 2, 4}) {
|
||||||
|
t.Error("different content: expected not equal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamsSize(t *testing.T) {
|
||||||
|
sig := funcSig{
|
||||||
|
name: "test",
|
||||||
|
params: []param{{name: "a", typ: "[]byte"}, {name: "b", typ: "int"}},
|
||||||
|
results: []param{{name: "n", typ: "int"}},
|
||||||
|
}
|
||||||
|
if got := paramsSize(sig); got != 32 {
|
||||||
|
t.Errorf("paramsSize = %d, want 32 (24 for slice + 8 for int)", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlockCount(t *testing.T) {
|
||||||
|
k := loadBasic(t)
|
||||||
|
n, err := k.BlockCount("sum")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BlockCount(sum): %v", err)
|
||||||
|
}
|
||||||
|
if n < 2 {
|
||||||
|
t.Errorf("sum: expected at least 2 blocks, got %d", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFillBuffer(t *testing.T) {
|
||||||
|
t.Run("zero", func(t *testing.T) {
|
||||||
|
// fillBuffer("zero") is a no-op — relies on make already zeroing.
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
fillBuffer(buf, "zero")
|
||||||
|
for _, b := range buf {
|
||||||
|
if b != 0 {
|
||||||
|
t.Error("zero pattern: make should produce zeroed buffer")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("ones", func(t *testing.T) {
|
||||||
|
buf := make([]byte, 16)
|
||||||
|
fillBuffer(buf, "ones")
|
||||||
|
for _, b := range buf {
|
||||||
|
if b != 0xFF {
|
||||||
|
t.Error("ones pattern should fill with 0xFF")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("seq", func(t *testing.T) {
|
||||||
|
buf := make([]byte, 256)
|
||||||
|
fillBuffer(buf, "seq")
|
||||||
|
for i, b := range buf {
|
||||||
|
if b != byte(i) {
|
||||||
|
t.Errorf("seq[%d] = %d, want %d", i, b, i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("hex", func(t *testing.T) {
|
||||||
|
buf := make([]byte, 6)
|
||||||
|
fillBuffer(buf, "deadbeef")
|
||||||
|
want := []byte{0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD}
|
||||||
|
for i, b := range buf {
|
||||||
|
if b != want[i] {
|
||||||
|
t.Errorf("hex[%d] = %02x, want %02x", i, b, want[i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFuzzFuncChecked(t *testing.T) {
|
||||||
|
k := loadBasic(t)
|
||||||
|
sig := funcSig{
|
||||||
|
name: "sum",
|
||||||
|
params: []param{{name: "data", typ: "[]int64"}},
|
||||||
|
results: []param{{name: "r", typ: "int64"}},
|
||||||
|
}
|
||||||
|
result := k.FuzzFuncChecked("sum", sig, 10, 0)
|
||||||
|
if !result.OK() {
|
||||||
|
t.Errorf("FuzzFuncChecked(sum): %s", result)
|
||||||
|
}
|
||||||
|
// Test with non-existent function — should report failure.
|
||||||
|
result = k.FuzzFuncChecked("nope", sig, 10, 0)
|
||||||
|
if result.OK() {
|
||||||
|
t.Error("FuzzFuncChecked(nope): expected failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ package verify
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
@@ -68,6 +69,7 @@ func TestJITSum(t *testing.T) {
|
|||||||
PutUint64(args, 16, uint64(cap(tt.data)))
|
PutUint64(args, 16, uint64(cap(tt.data)))
|
||||||
|
|
||||||
out, err := k.CallFunc("sum", args)
|
out, err := k.CallFunc("sum", args)
|
||||||
|
runtime.KeepAlive(tt.data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CallFunc(sum, %v): %v", tt.data, err)
|
t.Fatalf("CallFunc(sum, %v): %v", tt.data, err)
|
||||||
}
|
}
|
||||||
@@ -111,6 +113,8 @@ func TestJITWideCopy(t *testing.T) {
|
|||||||
PutUint64(args, 40, uint64(tt.n)) // src_cap
|
PutUint64(args, 40, uint64(tt.n)) // src_cap
|
||||||
|
|
||||||
_, err := k.CallFunc("wideCopy", args)
|
_, err := k.CallFunc("wideCopy", args)
|
||||||
|
runtime.KeepAlive(dst)
|
||||||
|
runtime.KeepAlive(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CallFunc(wideCopy): %v", err)
|
t.Fatalf("CallFunc(wideCopy): %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,160 +0,0 @@
|
|||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FuncSig is a parsed // func signature from the assembly source.
|
||||||
|
type FuncSig struct {
|
||||||
|
Name string
|
||||||
|
Params []Param
|
||||||
|
Results []Param
|
||||||
|
}
|
||||||
|
|
||||||
|
// Param is a single function parameter.
|
||||||
|
type Param struct {
|
||||||
|
Name string
|
||||||
|
Typ string // "[]byte", "[]int32", "int", "*[32]uint16", etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPointer reports whether the parameter type is a pointer or slice
|
||||||
|
// (i.e., it needs a buffer address in the argument block).
|
||||||
|
func (p Param) IsPointer() bool {
|
||||||
|
return strings.HasPrefix(p.Typ, "[]") || strings.HasPrefix(p.Typ, "*")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = parseParamsExported(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 = parseParamsExported(res)
|
||||||
|
}
|
||||||
|
return sig, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseParamsExported splits a parameter list like "a []byte, b []int32" or
|
||||||
|
// "dst, src []byte" into typed parameters. Go syntax allows grouped names
|
||||||
|
// where the type at the end applies to every name in the group:// "dst, src []byte" means both dst and src are []byte.
|
||||||
|
func parseParamsExported(s string) []Param {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fields := strings.Split(s, ",")
|
||||||
|
// First pass: extract the type from each field (if present).
|
||||||
|
types := make([]string, len(fields))
|
||||||
|
for i, field := range fields {
|
||||||
|
parts := strings.Fields(strings.TrimSpace(field))
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
types[i] = parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Propagate types backward: a field without a type inherits the type from
|
||||||
|
// the next field that has one (e.g. "dst" inherits "[]byte" from "src []byte").
|
||||||
|
for i := range fields {
|
||||||
|
if types[i] == "" {
|
||||||
|
for j := i + 1; j < len(fields); j++ {
|
||||||
|
if types[j] != "" {
|
||||||
|
types[i] = types[j]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Second pass: build params.
|
||||||
|
var out []Param
|
||||||
|
for i, field := range fields {
|
||||||
|
field = strings.TrimSpace(field)
|
||||||
|
if field == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.Fields(field)
|
||||||
|
typ := types[i]
|
||||||
|
if typ == "" {
|
||||||
|
typ = parts[0] // unnamed: the whole field is the type
|
||||||
|
out = append(out, Param{Typ: typ})
|
||||||
|
} else {
|
||||||
|
out = append(out, Param{Name: parts[0], Typ: typ})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractFuncSig extracts the // func signature for the named function
|
||||||
|
// from the assembly source.
|
||||||
|
func ExtractFuncSig(src, funcName string) (FuncSig, bool) {
|
||||||
|
lines := strings.Split(src, "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
if sig, ok := ParseFuncSig(line); ok && sig.Name == funcName {
|
||||||
|
// Verify the next non-comment line is the TEXT directive.
|
||||||
|
for j := i + 1; j < len(lines); j++ {
|
||||||
|
trimmed := strings.TrimSpace(lines[j])
|
||||||
|
if trimmed == "" || strings.HasPrefix(trimmed, "//") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trimmed, "TEXT") {
|
||||||
|
return sig, true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FuncSig{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArgLayout computes the ABI0 argument layout for a function signature.
|
||||||
|
// Returns the offset of each parameter in the argument block.
|
||||||
|
func ArgLayout(sig FuncSig) []ArgOffset {
|
||||||
|
var offsets []ArgOffset
|
||||||
|
off := 0
|
||||||
|
for _, p := range sig.Params {
|
||||||
|
size := paramSize(p.Typ)
|
||||||
|
offsets = append(offsets, ArgOffset{Name: p.Name, Typ: p.Typ, Offset: off, Size: size, IsPtr: p.IsPointer()})
|
||||||
|
off += size
|
||||||
|
}
|
||||||
|
return offsets
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArgOffset describes one parameter's position in the argument block.
|
||||||
|
type ArgOffset struct {
|
||||||
|
Name string
|
||||||
|
Typ string
|
||||||
|
Offset int
|
||||||
|
Size int
|
||||||
|
IsPtr bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// paramSize returns the size in bytes of a parameter type in the ABI0 layout.
|
||||||
|
func paramSize(typ string) int {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(typ, "[]"):
|
||||||
|
// Slice: pointer + length + capacity = 24 bytes.
|
||||||
|
return 24
|
||||||
|
case strings.HasPrefix(typ, "*"):
|
||||||
|
// Pointer: 8 bytes.
|
||||||
|
return 8
|
||||||
|
case typ == "int", typ == "int64", typ == "uint64", typ == "uintptr":
|
||||||
|
return 8
|
||||||
|
case typ == "int32", typ == "uint32", typ == "float32":
|
||||||
|
return 4
|
||||||
|
case typ == "int16", typ == "uint16":
|
||||||
|
return 2
|
||||||
|
case typ == "int8", typ == "uint8", typ == "byte", typ == "bool":
|
||||||
|
return 1
|
||||||
|
default:
|
||||||
|
// Default to 8 bytes for unknown types.
|
||||||
|
return 8
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseFuncSigExported(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
comment string
|
||||||
|
wantName string
|
||||||
|
nParams int
|
||||||
|
nResults int
|
||||||
|
}{
|
||||||
|
{"// func add(a int64, b int64) int64", "add", 2, 1},
|
||||||
|
{"// func wideCopy(dst []byte, src []byte)", "wideCopy", 2, 0},
|
||||||
|
{"// func decodeBlockAVX2(src, dst []byte) (n int, code int)", "decodeBlockAVX2", 2, 2},
|
||||||
|
{"// func analyzeO1RangeAVX2(swin []int32, dstP []uint32, hist *[32]uint16) (partSum uint64, overflow bool)", "analyzeO1RangeAVX2", 3, 2},
|
||||||
|
{"// not a func", "", 0, 0},
|
||||||
|
{"// func", "", 0, 0},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
sig, ok := ParseFuncSig(tt.comment)
|
||||||
|
if tt.wantName == "" {
|
||||||
|
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.wantName {
|
||||||
|
t.Errorf("ParseFuncSig(%q).Name = %q, want %q", tt.comment, sig.Name, tt.wantName)
|
||||||
|
}
|
||||||
|
if len(sig.Params) != tt.nParams {
|
||||||
|
t.Errorf("ParseFuncSig(%q): %d params, want %d", tt.comment, len(sig.Params), tt.nParams)
|
||||||
|
}
|
||||||
|
if len(sig.Results) != tt.nResults {
|
||||||
|
t.Errorf("ParseFuncSig(%q): %d results, want %d", tt.comment, len(sig.Results), tt.nResults)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractFuncSig(t *testing.T) {
|
||||||
|
src := `// func add(a int64, b int64) int64
|
||||||
|
TEXT ·add(SB), NOSPLIT, $0-24
|
||||||
|
|
||||||
|
// func wideCopyAVX2(dst, src []byte)
|
||||||
|
TEXT ·wideCopyAVX2(SB), NOSPLIT, $0-48
|
||||||
|
`
|
||||||
|
sig, ok := ExtractFuncSig(src, "add")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("ExtractFuncSig(add): not found")
|
||||||
|
}
|
||||||
|
if sig.Name != "add" {
|
||||||
|
t.Errorf("Name = %q, want %q", sig.Name, "add")
|
||||||
|
}
|
||||||
|
if len(sig.Params) != 2 {
|
||||||
|
t.Errorf("params = %d, want 2", len(sig.Params))
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, ok = ExtractFuncSig(src, "wideCopyAVX2")
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("ExtractFuncSig(wideCopyAVX2): not found")
|
||||||
|
}
|
||||||
|
if len(sig.Params) != 2 {
|
||||||
|
t.Errorf("params = %d, want 2", len(sig.Params))
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok = ExtractFuncSig(src, "nonexistent")
|
||||||
|
if ok {
|
||||||
|
t.Error("ExtractFuncSig(nonexistent): expected not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgLayout(t *testing.T) {
|
||||||
|
sig := FuncSig{
|
||||||
|
Name: "wideCopyAVX2",
|
||||||
|
Params: []Param{
|
||||||
|
{Name: "dst", Typ: "[]byte"},
|
||||||
|
{Name: "src", Typ: "[]byte"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
layout := ArgLayout(sig)
|
||||||
|
if len(layout) != 2 {
|
||||||
|
t.Fatalf("ArgLayout: %d entries, want 2", len(layout))
|
||||||
|
}
|
||||||
|
// dst: offset 0, size 24 (slice)
|
||||||
|
if layout[0].Name != "dst" || layout[0].Offset != 0 || layout[0].Size != 24 || !layout[0].IsPtr {
|
||||||
|
t.Errorf("layout[0] = %+v", layout[0])
|
||||||
|
}
|
||||||
|
// src: offset 24, size 24 (slice)
|
||||||
|
if layout[1].Name != "src" || layout[1].Offset != 24 || layout[1].Size != 24 || !layout[1].IsPtr {
|
||||||
|
t.Errorf("layout[1] = %+v", layout[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgLayoutMixed(t *testing.T) {
|
||||||
|
sig := FuncSig{
|
||||||
|
Name: "decodeBlockAVX2",
|
||||||
|
Params: []Param{
|
||||||
|
{Name: "src", Typ: "[]byte"},
|
||||||
|
{Name: "dst", Typ: "[]byte"},
|
||||||
|
},
|
||||||
|
Results: []Param{
|
||||||
|
{Name: "n", Typ: "int"},
|
||||||
|
{Name: "code", Typ: "int"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
layout := ArgLayout(sig)
|
||||||
|
if len(layout) != 2 {
|
||||||
|
t.Fatalf("ArgLayout: %d entries, want 2", len(layout))
|
||||||
|
}
|
||||||
|
// Two slices: 24 + 24 = 48 bytes of params.
|
||||||
|
if layout[1].Offset != 24 {
|
||||||
|
t.Errorf("src offset = %d, want 24", layout[1].Offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParamSize(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
typ string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"[]byte", 24},
|
||||||
|
{"[]int32", 24},
|
||||||
|
{"*uint16", 8},
|
||||||
|
{"*[32]uint16", 8},
|
||||||
|
{"int", 8},
|
||||||
|
{"int64", 8},
|
||||||
|
{"uint64", 8},
|
||||||
|
{"uintptr", 8},
|
||||||
|
{"int32", 4},
|
||||||
|
{"uint32", 4},
|
||||||
|
{"float32", 4},
|
||||||
|
{"int16", 2},
|
||||||
|
{"uint16", 2},
|
||||||
|
{"int8", 1},
|
||||||
|
{"uint8", 1},
|
||||||
|
{"byte", 1},
|
||||||
|
{"bool", 1},
|
||||||
|
{"string", 8}, // unknown type defaults to 8
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := paramSize(tt.typ); got != tt.want {
|
||||||
|
t.Errorf("paramSize(%q) = %d, want %d", tt.typ, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@ package verify
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
||||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||||
@@ -114,6 +116,66 @@ func (k *Kernel) CallFuncChecked(name string, args []byte) ([]byte, ABIReport, e
|
|||||||
return CallChecked(fnAddr, args)
|
return CallChecked(fnAddr, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FuzzFuncCheckedByName is like FuzzFuncChecked but extracts the signature
|
||||||
|
// from the source code internally.
|
||||||
|
func (k *Kernel) FuzzFuncCheckedByName(name, src string, iterations int, seed int64) FuzzResult {
|
||||||
|
result := FuzzResult{Func: name, Iterations: iterations}
|
||||||
|
sig, ok := ExtractSignatures(src)[name]
|
||||||
|
if !ok {
|
||||||
|
result.Mismatches = iterations
|
||||||
|
result.FirstFail = "no // func signature found"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
return k.FuzzFuncChecked(name, sig, iterations, seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzFuncChecked combines fuzzing with ABI checks: it generates varied
|
||||||
|
// inputs and verifies that callee-saved registers and the red zone are
|
||||||
|
// preserved even on deep execution paths (not just early exits).
|
||||||
|
func (k *Kernel) FuzzFuncChecked(name string, sig funcSig, iterations int, seed int64) FuzzResult {
|
||||||
|
result := FuzzResult{Func: name, Iterations: iterations}
|
||||||
|
rng := rand.New(rand.NewSource(seed))
|
||||||
|
|
||||||
|
fl, err := k.Func(name)
|
||||||
|
if err != nil {
|
||||||
|
result.Mismatches = iterations
|
||||||
|
result.FirstFail = err.Error()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
violations := 0
|
||||||
|
for i := 0; i < iterations; i++ {
|
||||||
|
gasmArgs, _, bufs := genDualArgs(rng, sig, fl.Args)
|
||||||
|
result.CrashInput = gasmArgs
|
||||||
|
|
||||||
|
_, report, err := k.CallFuncChecked(name, gasmArgs)
|
||||||
|
if err != nil {
|
||||||
|
result.Mismatches++
|
||||||
|
if result.FirstFail == "" {
|
||||||
|
result.FirstFail = fmt.Sprintf("iter %d: call: %v", i, err)
|
||||||
|
}
|
||||||
|
runtime.KeepAlive(bufs)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !report.OK() {
|
||||||
|
violations++
|
||||||
|
result.Mismatches++
|
||||||
|
if result.FirstFail == "" {
|
||||||
|
result.FirstFail = fmt.Sprintf("iter %d: %s", i, report.String())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.Matches++
|
||||||
|
}
|
||||||
|
runtime.KeepAlive(bufs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if violations > 0 && result.FirstFail == "" {
|
||||||
|
result.FirstFail = fmt.Sprintf("%d ABI violations across %d iterations", violations, iterations)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// Close releases the executable mapping.
|
// Close releases the executable mapping.
|
||||||
func (k *Kernel) Close() {
|
func (k *Kernel) Close() {
|
||||||
if k.exec != nil {
|
if k.exec != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user