feat(verify): add the JIT execution substrate and gasm verify subcommand

Assisted-by: Qwen 3.8 Max Preview
This commit is contained in:
2026-08-02 23:11:30 +02:00
parent 5af12e15ac
commit c9775c2b95
11 changed files with 785 additions and 2 deletions
+59 -1
View File
@@ -24,11 +24,12 @@ import (
"sourcedock.dev/petrbalvin/gasm-devkit/lint"
"sourcedock.dev/petrbalvin/gasm-devkit/lsp"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
)
// version is the release version, stamped at build time via
// -ldflags "-X main.version=…" (defaulting to the current release).
var version = "0.16.0"
var version = "0.17.0"
func main() {
if len(os.Args) < 2 {
@@ -46,6 +47,8 @@ func main() {
os.Exit(cmdLint(os.Args[2:]))
case "asm":
os.Exit(cmdAsm(os.Args[2:]))
case "verify":
os.Exit(cmdVerify(os.Args[2:]))
case "lsp":
os.Exit(cmdLSP(os.Args[2:]))
case "version", "--version", "-V":
@@ -80,6 +83,7 @@ Commands:
fmt canonicalise formatting (gofmt for assembly)
lint run static checks
asm assemble .s files to machine code (amd64)
verify JIT-assemble and run dynamic checks (amd64)
lsp run the language server over stdio
version print the version (same as --version)
@@ -466,3 +470,57 @@ requires -p, the package path, and the installed Go toolchain).
}
return 0
}
func cmdVerify(args []string) int {
fs := newCommand("verify", "gasm verify <file.s>", `
Assemble FILE (amd64), map it into executable memory and report the available
functions. This confirms the assembled image is self-consistent (no
unresolved external symbols) and executable — the prerequisite for dynamic
testing.
With -smoke, each NOSPLIT function is called with a zeroed argument block to
confirm the JIT trampoline works end-to-end. This is safe only for functions
that tolerate nil pointers and zero lengths in their arguments.
`)
smoke := fs.Bool("smoke", false, "call each NOSPLIT function with zeroed args")
fs.Parse(args)
if fs.NArg() != 1 {
fmt.Fprintln(os.Stderr, "usage: gasm verify [-smoke] <file.s>")
return 2
}
path := fs.Arg(0)
if arch.FromFilename(path) != arch.AMD64 {
fmt.Fprintln(os.Stderr, "gasm verify: only amd64 is supported")
return 1
}
k, err := verify.Load(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
defer k.Close()
names := k.FuncNames()
fmt.Printf("%s: %d functions JIT-loaded\n", path, len(names))
rc := 0
for _, name := range names {
fl, _ := k.Func(name)
flags := ""
if fl.NoSplit {
flags = " NOSPLIT"
}
fmt.Printf(" %s: %d bytes, args=%d, frame=%d%s\n", name, fl.Size, fl.Args, fl.Frame, flags)
if *smoke && fl.NoSplit {
args := make([]byte, fl.Args)
_, err := k.CallFunc(name, args)
if err != nil {
fmt.Printf(" smoke: FAIL — %v\n", err)
rc = 1
} else {
fmt.Printf(" smoke: OK\n")
}
}
}
return rc
}
+25
View File
@@ -284,6 +284,31 @@ references and the implicit funcdata/DWARF symbols remain future work (the
linker fills the latter's defaults); the rest of Phase 2 is those, the
remaining EVEX forms and the other architectures.
### `verify`
The dynamic-analysis substrate (Phase 3). It JIT-loads assembled images into
executable memory and invokes them directly, enabling differential testing,
runtime ABI checks and coverage profiling.
The execution model is pure Go (stdlib only). `Map` copies machine code into
an anonymous `syscall.Mmap` mapping and enforces W^X (write the bytes, then
`mprotect` to read-execute). `Call` prepares a stack whose first word is the
address of an assembly trampoline (`leaveJIT`), lays the ABI0 argument
block after it, switches to that stack via `enterJIT` (which saves the Go
stack pointer in a package global and jumps to the target), and recovers
control when the function RETs into `leaveJIT` (which restores the Go stack
and returns). A 64-byte pad below the return address accommodates the
ABIInternal wrapper that the Go runtime interposes on assembly functions.
`Load` / `LoadSource` / `LoadAST` parse, assemble and map a `.s` file in one
step, returning a `Kernel` whose `CallFunc` method marshals the argument block
by name. The image must be self-contained (no external relocations); the
assembler’s `Image.Bytes()` provides the code-and-data concatenation.
The `gasm verify` CLI subcommand exposes this: it loads a file, reports the
available functions and (with `-smoke`) calls each NOSPLIT function with zeroed
arguments to confirm the trampoline round-trips.
## Extension points
- **New architecture:** add an entry to the generator in `_gen`, run
+1 -1
View File
@@ -3,7 +3,7 @@
# gasm-devkit — developer tooling for Go's Plan 9 assembler (GAsm).
version := "0.16.0"
version := "0.17.0"
default:
@just --list
+67
View File
@@ -0,0 +1,67 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
#include "textflag.h"
// func add(a, b int64) int64
TEXT ·add(SB), NOSPLIT, $0-24
MOVQ a+0(FP), AX
ADDQ b+8(FP), AX
MOVQ AX, ret+16(FP)
RET
// func sum(data []int64) int64
// Sums all elements of the slice.
TEXT ·sum(SB), NOSPLIT, $0-32
MOVQ data_base+0(FP), SI
MOVQ data_len+8(FP), CX
XORQ AX, AX
TESTQ CX, CX
JZ sum_done
sum_loop:
ADDQ (SI), AX
ADDQ $8, SI
DECQ CX
JNZ sum_loop
sum_done:
MOVQ AX, ret+24(FP)
RET
// func wideCopy(dst, src []byte)
// Non-overlapping copy of min(len(dst), len(src)) bytes using 32-byte moves.
TEXT ·wideCopy(SB), NOSPLIT, $0-48
MOVQ dst_base+0(FP), DI
MOVQ dst_len+8(FP), BX
MOVQ src_base+24(FP), SI
MOVQ src_len+32(FP), R8
CMPQ BX, R8
JLE wc_have_n
MOVQ R8, BX
wc_have_n:
CMPQ BX, $32
JB wc_small
VMOVDQU (SI), Y0
VMOVDQU Y0, (DI)
VMOVDQU -32(SI)(BX*1), Y0
VMOVDQU Y0, -32(DI)(BX*1)
VZEROUPPER
RET
wc_small:
TESTQ BX, BX
JZ wc_done
wc_byte:
MOVB (SI), R8B
MOVB R8B, (DI)
INCQ SI
INCQ DI
DECQ BX
JNZ wc_byte
wc_done:
RET
+77
View File
@@ -0,0 +1,77 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
//go:build amd64
package verify
import (
"encoding/binary"
"fmt"
"reflect"
"syscall"
"unsafe"
)
// savedSP holds the Go stack pointer while a JIT call is in flight.
// Referenced by the assembly trampoline (trampoline_amd64.s).
var savedSP uintptr
// enterJIT switches to the prepared stack and jumps to fn.
// It does not return normally; the JIT function's RET transfers control
// to leaveJIT, which restores the Go stack.
//
//go:nosplit
func enterJIT(fn uintptr, stack uintptr)
// leaveJIT restores the Go stack after a JIT function returns.
// Its address is placed as the return address on the prepared stack.
//
//go:nosplit
func leaveJIT()
// leaveJITAddr is the machine address of leaveJIT, resolved once at init.
var leaveJITAddr uintptr
func init() {
leaveJITAddr = reflect.ValueOf(leaveJIT).Pointer()
}
// stackPad is padding below the return address on the prepared stack.
// The ABIInternal wrapper that leaveJIT's address resolves to executes
// PUSHQ BP and CALL before reaching the raw assembly, writing up to 16
// bytes below the return-address slot. 64 bytes of headroom is ample.
const stackPad = 64
// Call invokes the assembled function at fnAddr with the given ABI0 argument
// block (the raw bytes that would appear at FP+0). It returns the argument
// block after the call, which contains any results the function wrote back
// (the ABI0 convention shares the argument area for inputs and outputs).
//
// The function must be NOSPLIT (no stack growth) and must not reference
// external symbols — the image is self-contained.
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
// Prepare the stack: [padding][leaveJIT addr][args...]
stackSize := stackPad + 8 + len(args) + 64 // padding + ret + args + safety
stackMem, err := syscall.Mmap(-1, 0, stackSize,
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON)
if err != nil {
return nil, fmt.Errorf("verify: stack mmap: %w", err)
}
defer syscall.Munmap(stackMem)
// The return address sits after the padding; the function's SP will
// point here, leaving stackPad bytes below for the wrapper's pushes.
retOff := stackPad
binary.LittleEndian.PutUint64(stackMem[retOff:retOff+8], uint64(leaveJITAddr))
// The ABI0 argument area follows the return address.
copy(stackMem[retOff+8:], args)
stackBase := uintptr(unsafe.Pointer(&stackMem[retOff]))
enterJIT(fnAddr, stackBase)
// Copy out the (possibly modified) argument area.
out := make([]byte, len(args))
copy(out, stackMem[retOff+8:retOff+8+len(args)])
return out, nil
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
//go:build !amd64
package verify
import "fmt"
// Call is unavailable on non-amd64 architectures.
func Call(fnAddr uintptr, args []byte) ([]byte, error) {
return nil, fmt.Errorf("verify: JIT execution requires amd64")
}
+88
View File
@@ -0,0 +1,88 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
// Package verify provides the dynamic-analysis substrate for gasm: it
// JIT-assembles Plan 9 amd64 kernels into executable memory and calls them
// directly, enabling differential testing against portable Go references,
// runtime ABI checks and basic-block coverage profiling.
//
// The execution model is pure Go (stdlib only): machine code is mapped with
// syscall.Mmap and invoked through an assembly trampoline that switches to a
// prepared ABI0 stack. No cgo, no external toolchain.
package verify
import (
"encoding/binary"
"fmt"
"syscall"
"unsafe"
)
// Executable maps a copy of code into a read-execute memory region suitable
// for direct invocation. The mapping is anonymous and private; the original
// slice is not retained. Call Unmap to release the region.
type Executable struct {
addr uintptr // base address of the mapping
size int
mem []byte // the mmap'd slice (for Unmap)
}
// Map copies code into a freshly allocated RX region and returns it.
// The mapping is PROT_READ|PROT_EXEC; writes are not permitted after the
// copy, matching W^X policy.
func Map(code []byte) (*Executable, error) {
size := len(code)
if size == 0 {
return nil, fmt.Errorf("verify: cannot map zero-length code")
}
// Round up to the page size.
const pageSize = 4096
mapSize := (size + pageSize - 1) &^ (pageSize - 1)
mem, err := syscall.Mmap(-1, 0, mapSize,
syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE|syscall.MAP_ANON)
if err != nil {
return nil, fmt.Errorf("verify: mmap: %w", err)
}
copy(mem, code)
// Remove write permission (W^X).
if err := syscall.Mprotect(mem, syscall.PROT_READ|syscall.PROT_EXEC); err != nil {
syscall.Munmap(mem)
return nil, fmt.Errorf("verify: mprotect: %w", err)
}
return &Executable{
addr: uintptr(unsafe.Pointer(&mem[0])),
size: size,
mem: mem,
}, nil
}
// Unmap releases the executable region.
func (e *Executable) Unmap() {
if e.mem != nil {
syscall.Munmap(e.mem)
e.mem = nil
}
}
// FuncAddr returns the absolute address of a function at the given offset
// within the mapped image.
func (e *Executable) FuncAddr(offset int) uintptr {
return e.addr + uintptr(offset)
}
// PutUint64 writes v into buf at byte offset off (little-endian).
func PutUint64(buf []byte, off int, v uint64) {
binary.LittleEndian.PutUint64(buf[off:off+8], v)
}
// GetUint64 reads a little-endian uint64 from buf at byte offset off.
func GetUint64(buf []byte, off int) uint64 {
return binary.LittleEndian.Uint64(buf[off : off+8])
}
// PutPtr writes a pointer value into buf at byte offset off.
func PutPtr(buf []byte, off int, p unsafe.Pointer) {
binary.LittleEndian.PutUint64(buf[off:off+8], uint64(uintptr(p)))
}
+159
View File
@@ -0,0 +1,159 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"bytes"
"testing"
"unsafe"
)
func loadBasic(t *testing.T) *Kernel {
t.Helper()
k, err := Load("../testdata/verify/basic_amd64.s")
if err != nil {
t.Fatalf("Load: %v", err)
}
t.Cleanup(k.Close)
return k
}
func TestJITAdd(t *testing.T) {
k := loadBasic(t)
tests := []struct {
a, b, want int64
}{
{0, 0, 0},
{1, 2, 3},
{-1, 1, 0},
{1 << 62, 1 << 62, -9223372036854775808}, // overflow wraps (MinInt64)
{-100, -200, -300},
}
for _, tt := range tests {
args := make([]byte, 24)
PutUint64(args, 0, uint64(tt.a))
PutUint64(args, 8, uint64(tt.b))
out, err := k.CallFunc("add", args)
if err != nil {
t.Fatalf("CallFunc(add, %d, %d): %v", tt.a, tt.b, err)
}
got := int64(GetUint64(out, 16))
if got != tt.want {
t.Errorf("add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.want)
}
}
}
func TestJITSum(t *testing.T) {
k := loadBasic(t)
tests := []struct {
data []int64
want int64
}{
{nil, 0},
{[]int64{1}, 1},
{[]int64{1, 2, 3, 4, 5}, 15},
{[]int64{-10, 20, -30, 40}, 20},
}
for _, tt := range tests {
args := make([]byte, 32)
if len(tt.data) > 0 {
PutPtr(args, 0, unsafe.Pointer(&tt.data[0]))
}
PutUint64(args, 8, uint64(len(tt.data)))
PutUint64(args, 16, uint64(cap(tt.data)))
out, err := k.CallFunc("sum", args)
if err != nil {
t.Fatalf("CallFunc(sum, %v): %v", tt.data, err)
}
got := int64(GetUint64(out, 24))
if got != tt.want {
t.Errorf("sum(%v) = %d, want %d", tt.data, got, tt.want)
}
}
}
func TestJITWideCopy(t *testing.T) {
k := loadBasic(t)
tests := []struct {
name string
n int
}{
{"empty", 0},
{"tiny", 7},
{"exact32", 32},
{"overlap_range", 48},
{"exact64", 64},
{"unaligned", 45},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
src := make([]byte, tt.n)
for i := range src {
src[i] = byte(i * 7)
}
dst := make([]byte, tt.n)
args := make([]byte, 48)
if tt.n > 0 {
PutPtr(args, 0, unsafe.Pointer(&dst[0]))
PutPtr(args, 24, unsafe.Pointer(&src[0]))
}
PutUint64(args, 8, uint64(tt.n)) // dst_len
PutUint64(args, 16, uint64(tt.n)) // dst_cap
PutUint64(args, 32, uint64(tt.n)) // src_len
PutUint64(args, 40, uint64(tt.n)) // src_cap
_, err := k.CallFunc("wideCopy", args)
if err != nil {
t.Fatalf("CallFunc(wideCopy): %v", err)
}
if !bytes.Equal(dst, src) {
t.Errorf("wideCopy: dst ≠ src\n got %x\n want %x", dst, src)
}
})
}
}
func TestKernelFuncNames(t *testing.T) {
k := loadBasic(t)
names := k.FuncNames()
want := []string{"add", "sum", "wideCopy"}
if len(names) != len(want) {
t.Fatalf("FuncNames() = %v, want %v", names, want)
}
for i, n := range names {
if n != want[i] {
t.Errorf("FuncNames()[%d] = %q, want %q", i, n, want[i])
}
}
}
func TestKernelFuncNotFound(t *testing.T) {
k := loadBasic(t)
_, err := k.CallFunc("nonexistent", make([]byte, 8))
if err == nil {
t.Fatal("expected error for nonexistent function")
}
}
func TestKernelArgTooSmall(t *testing.T) {
k := loadBasic(t)
_, err := k.CallFunc("add", make([]byte, 8)) // needs 24
if err == nil {
t.Fatal("expected error for too-small arg block")
}
}
func TestMapZeroLength(t *testing.T) {
_, err := Map(nil)
if err == nil {
t.Fatal("expected error for zero-length code")
}
}
+160
View File
@@ -0,0 +1,160 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"bytes"
"os"
"testing"
"unsafe"
)
// lz4KernelPath is the sibling repository's AVX2 kernel, used for
// integration testing. The test is skipped when the file is absent
// (e.g. in CI without the sibling checkout).
const lz4KernelPath = "../../go-libraries/go-lz4/avx2_amd64.s"
func loadLZ4Kernel(t *testing.T) *Kernel {
t.Helper()
if _, err := os.Stat(lz4KernelPath); err != nil {
t.Skipf("sibling kernel not available: %v", err)
}
k, err := Load(lz4KernelPath)
if err != nil {
t.Fatalf("Load(%s): %v", lz4KernelPath, err)
}
t.Cleanup(k.Close)
return k
}
// callDecodeBlockAVX2 invokes the JIT-assembled decodeBlockAVX2 with the
// given src and dst buffers, returning (n, code).
func callDecodeBlockAVX2(t *testing.T, k *Kernel, src, dst []byte) (int, int) {
t.Helper()
args := make([]byte, 64)
if len(src) > 0 {
PutPtr(args, 0, unsafe.Pointer(&src[0]))
}
PutUint64(args, 8, uint64(len(src)))
PutUint64(args, 16, uint64(cap(src)))
if len(dst) > 0 {
PutPtr(args, 24, unsafe.Pointer(&dst[0]))
}
PutUint64(args, 32, uint64(len(dst)))
PutUint64(args, 40, uint64(cap(dst)))
out, err := k.CallFunc("decodeBlockAVX2", args)
if err != nil {
t.Fatalf("CallFunc(decodeBlockAVX2): %v", err)
}
return int(GetUint64(out, 48)), int(GetUint64(out, 56))
}
func TestLZ4DecodeKnownAnswers(t *testing.T) {
k := loadLZ4Kernel(t)
tests := []struct {
name string
src []byte
dstSize int
wantDst []byte
wantN int
wantCode int
}{
{
name: "literals_only",
src: []byte{0x50, 'H', 'e', 'l', 'l', 'o'},
dstSize: 16,
wantDst: []byte("Hello"),
wantN: 5,
wantCode: 0,
},
{
name: "literals_and_match",
src: []byte{0x54, 'A', 'A', 'A', 'A', 'A', 0x05, 0x00, 0x30, 'B', 'B', 'B'},
dstSize: 32,
wantDst: []byte("AAAAAAAAAAAAABBB"),
wantN: 16,
wantCode: 0,
},
{
name: "overlapping_match",
// 1 literal 'X', then match offset=1 length=4+4=8 → "XXXXXXXXX",
// then final 1 literal 'Y'.
src: []byte{0x14, 'X', 0x01, 0x00, 0x10, 'Y'},
dstSize: 16,
wantDst: []byte("XXXXXXXXXY"),
wantN: 10,
wantCode: 0,
},
{
name: "malformed_truncated",
src: []byte{0x50, 'H', 'e'}, // claims 5 literals, has 2
dstSize: 16,
wantN: 0,
wantCode: 1,
},
{
name: "zero_offset",
src: []byte{0x14, 'X', 0x00, 0x00},
dstSize: 16,
wantN: 0,
wantCode: 2,
},
{
name: "empty_token",
src: []byte{0x00}, // 0 literals, end of block
dstSize: 16,
wantDst: nil,
wantN: 0,
wantCode: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dst := make([]byte, tt.dstSize)
n, code := callDecodeBlockAVX2(t, k, tt.src, dst)
if n != tt.wantN || code != tt.wantCode {
t.Fatalf("decodeBlockAVX2: got (n=%d, code=%d), want (n=%d, code=%d)",
n, code, tt.wantN, tt.wantCode)
}
if tt.wantCode == 0 && tt.wantDst != nil {
if !bytes.Equal(dst[:n], tt.wantDst) {
t.Errorf("output mismatch:\n got %q\n want %q", dst[:n], tt.wantDst)
}
}
})
}
}
func TestLZ4WideCopyAVX2(t *testing.T) {
k := loadLZ4Kernel(t)
sizes := []int{0, 1, 15, 16, 31, 32, 33, 63, 64, 100, 256, 1024}
for _, n := range sizes {
src := make([]byte, n)
for i := range src {
src[i] = byte(i*13 + 7)
}
dst := make([]byte, n)
args := make([]byte, 48)
if n > 0 {
PutPtr(args, 0, unsafe.Pointer(&dst[0]))
PutPtr(args, 24, unsafe.Pointer(&src[0]))
}
PutUint64(args, 8, uint64(n))
PutUint64(args, 16, uint64(n))
PutUint64(args, 32, uint64(n))
PutUint64(args, 40, uint64(n))
_, err := k.CallFunc("wideCopyAVX2", args)
if err != nil {
t.Fatalf("wideCopyAVX2(n=%d): %v", n, err)
}
if !bytes.Equal(dst, src) {
t.Errorf("wideCopyAVX2(n=%d): output mismatch", n)
}
}
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
#include "textflag.h"
// ABI0 JIT trampoline. enterJIT switches from the Go stack to a prepared
// stack and jumps to the assembled function; when the function RETs, control
// lands in leaveJIT, which restores the Go stack and returns to the Go caller.
//
// The prepared stack must begin with the address of leaveJIT (the return
// address the JIT function will pop), followed by the function's ABI0
// argument area.
//
// Single-threaded: savedSP is a package global, so only one JIT call may be
// in flight at a time. gasm verify runs sequentially.
// func enterJIT(fn uintptr, stack uintptr)
// Switches to the prepared stack and jumps to fn. Does not return normally;
// the JIT function's RET transfers control to leaveJIT.
TEXT ·enterJIT(SB), NOSPLIT, $0-16
MOVQ fn+0(FP), AX // target function address (before SP switch)
MOVQ SP, ·savedSP(SB) // preserve the Go stack pointer
MOVQ stack+8(FP), SP // switch to the prepared stack
JMP AX
// func leaveJIT()
// Restores the Go stack pointer and returns to enterJIT's caller.
TEXT ·leaveJIT(SB), NOSPLIT, $0-0
MOVQ ·savedSP(SB), SP
RET
+106
View File
@@ -0,0 +1,106 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package verify
import (
"fmt"
"os"
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
)
// Kernel is a JIT-loaded assembly image ready for direct invocation.
// It wraps an executable memory mapping and the function layout metadata
// needed to marshal ABI0 calls.
type Kernel struct {
exec *Executable
img *asm.Image
funcs map[string]int // function name → index into img.Funcs
}
// Load parses, assembles and maps a .s file into executable memory.
// The returned Kernel is ready for Call. The caller must call Close to
// release the mapping.
func Load(path string) (*Kernel, error) {
src, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("verify: %w", err)
}
return LoadSource(path, string(src))
}
// LoadSource parses, assembles and maps assembly source into executable memory.
func LoadSource(filename, src string) (*Kernel, error) {
file, errs := parser.Parse(filename, src)
if len(errs) > 0 {
return nil, fmt.Errorf("verify: parse %s: %v", filename, errs[0])
}
return LoadAST(file)
}
// LoadAST assembles a parsed AST file and maps the result into executable
// memory.
func LoadAST(file *ast.File) (*Kernel, error) {
img, err := asm.AssembleFile(file)
if err != nil {
return nil, fmt.Errorf("verify: assemble: %w", err)
}
if len(img.Externals) > 0 {
return nil, fmt.Errorf("verify: unresolved external symbols: %v", img.Externals)
}
code := img.Bytes()
exec, err := Map(code)
if err != nil {
return nil, err
}
funcs := make(map[string]int, len(img.Funcs))
for i, f := range img.Funcs {
funcs[f.Name] = i
}
return &Kernel{exec: exec, img: img, funcs: funcs}, nil
}
// Func returns the layout metadata for the named function.
func (k *Kernel) Func(name string) (asm.FuncLayout, error) {
idx, ok := k.funcs[name]
if !ok {
return asm.FuncLayout{}, fmt.Errorf("verify: function %q not found", name)
}
return k.img.Funcs[idx], nil
}
// FuncNames returns the names of all functions in the kernel, in source order.
func (k *Kernel) FuncNames() []string {
names := make([]string, len(k.img.Funcs))
for i, f := range k.img.Funcs {
names[i] = f.Name
}
return names
}
// CallFunc invokes the named function with the given ABI0 argument block.
// The arg block is the raw bytes of the function's argument/result area
// (as declared by the TEXT $frame-args suffix). Returns the arg block
// after the call (with any results written back by the function).
func (k *Kernel) CallFunc(name string, args []byte) ([]byte, error) {
idx, ok := k.funcs[name]
if !ok {
return nil, fmt.Errorf("verify: function %q not found", name)
}
fl := k.img.Funcs[idx]
if len(args) < fl.Args {
return nil, fmt.Errorf("verify: %s: arg block too small: got %d, need %d", name, len(args), fl.Args)
}
fnAddr := k.exec.FuncAddr(fl.Offset)
return Call(fnAddr, args)
}
// Close releases the executable mapping.
func (k *Kernel) Close() {
if k.exec != nil {
k.exec.Unmap()
}
}