Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
382efe538a | ||
|
|
f8d28a42ba | ||
|
|
51a2854d7f |
+58
-1
@@ -8,6 +8,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -29,7 +30,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.22.0"
|
var version = "0.25.0"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) < 2 {
|
if len(os.Args) < 2 {
|
||||||
@@ -490,6 +491,7 @@ With -profile, the static basic-block structure is listed for each function.
|
|||||||
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)")
|
||||||
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")
|
||||||
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] [-profile] <file.s>")
|
||||||
@@ -511,6 +513,61 @@ With -profile, the static basic-block structure is listed for each function.
|
|||||||
names := k.FuncNames()
|
names := k.FuncNames()
|
||||||
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
|
||||||
|
|
||||||
|
// Ground-truth comparison: assemble with go tool asm and compare bytes.
|
||||||
|
if *groundTruth {
|
||||||
|
gt, err := verify.GroundTruth(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
matched, total := 0, 0
|
||||||
|
for _, name := range names {
|
||||||
|
fl, _ := k.Func(name)
|
||||||
|
gasmCode := k.Image().Code[fl.Offset : fl.Offset+fl.Size]
|
||||||
|
goCode, ok := gt[name]
|
||||||
|
if !ok {
|
||||||
|
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
total++
|
||||||
|
// Compare, masking relocation sites (disp32 fields that the
|
||||||
|
// Go linker fills at link time — gasm resolves them internally).
|
||||||
|
gasmCmp := make([]byte, len(gasmCode))
|
||||||
|
goCmp := make([]byte, len(goCode))
|
||||||
|
copy(gasmCmp, gasmCode)
|
||||||
|
copy(goCmp, goCode)
|
||||||
|
for _, r := range fl.Relocs {
|
||||||
|
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
|
||||||
|
gasmCmp[j] = 0
|
||||||
|
}
|
||||||
|
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
|
||||||
|
goCmp[j] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bytes.Equal(gasmCmp, goCmp) {
|
||||||
|
matched++
|
||||||
|
if len(fl.Relocs) > 0 {
|
||||||
|
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", name, fl.Size, len(fl.Relocs))
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s: MATCH (%d bytes)\n", name, fl.Size)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf(" %s: MISMATCH (gasm %d bytes, go %d bytes)\n", name, fl.Size, len(goCode))
|
||||||
|
for i := 0; i < len(gasmCmp) && i < len(goCmp); i++ {
|
||||||
|
if gasmCmp[i] != goCmp[i] {
|
||||||
|
fmt.Printf(" first diff at byte %d: gasm=%02x go=%02x\n", i, gasmCmp[i], goCmp[i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rc = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Printf("ground truth: %d/%d functions byte-identical\n", matched, total)
|
||||||
|
if matched < total {
|
||||||
|
rc = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
fl, _ := k.Func(name)
|
fl, _ := k.Func(name)
|
||||||
flags := ""
|
flags := ""
|
||||||
|
|||||||
@@ -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.22.0"
|
version := "0.25.0"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@just --list
|
@just --list
|
||||||
|
|||||||
@@ -97,6 +97,64 @@ func analyzeO1RangeGo(swin []int32, dstP []uint32, hist *[32]uint16) (partSum ui
|
|||||||
return
|
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 ---
|
// --- Differential tests ---
|
||||||
|
|
||||||
func TestFLACDecodeMono16(t *testing.T) {
|
func TestFLACDecodeMono16(t *testing.T) {
|
||||||
@@ -328,3 +386,200 @@ func TestFLACFastStereoSums(t *testing.T) {
|
|||||||
func foldAbs(v int32) uint64 {
|
func foldAbs(v int32) uint64 {
|
||||||
return uint64(uint32(v<<1) ^ uint32(v>>31))
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GroundTruth assembles the given .s file with the Go toolchain's own
|
||||||
|
// assembler and returns the machine code bytes for each TEXT function,
|
||||||
|
// keyed by the function's short name (the part after the middle dot).
|
||||||
|
// This is the universal oracle: any file that `go tool asm` accepts can
|
||||||
|
// be verified, with no hand-written reference.
|
||||||
|
func GroundTruth(path string) (map[string][]byte, error) {
|
||||||
|
goroot := runtime.GOROOT()
|
||||||
|
asmBin := filepath.Join(goroot, "pkg", "tool", runtime.GOOS+"_"+runtime.GOARCH, "asm")
|
||||||
|
if _, err := os.Stat(asmBin); err != nil {
|
||||||
|
return nil, fmt.Errorf("verify: go tool asm not found at %s: %w", asmBin, err)
|
||||||
|
}
|
||||||
|
includeDir := filepath.Join(goroot, "pkg", "include")
|
||||||
|
|
||||||
|
// Create a temp file for the object output.
|
||||||
|
tmpDir, err := os.MkdirTemp("", "gasm-verify-*")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("verify: tempdir: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
objPath := filepath.Join(tmpDir, "out.o")
|
||||||
|
|
||||||
|
// Derive a package name from the file name (the assembler needs -p).
|
||||||
|
base := filepath.Base(path)
|
||||||
|
pkg := strings.TrimSuffix(base, ".s")
|
||||||
|
pkg = strings.TrimSuffix(pkg, "_amd64")
|
||||||
|
|
||||||
|
cmd := exec.Command(asmBin, "-I", includeDir, "-p", pkg, "-o", objPath, path)
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return nil, fmt.Errorf("verify: go tool asm: %w\n%s", err, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
objData, err := os.ReadFile(objPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("verify: read object: %w", err)
|
||||||
|
}
|
||||||
|
return extractGOOBJCode(objData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GOOBJ block indices (cmd/internal/goobj).
|
||||||
|
const (
|
||||||
|
blkAutolib = iota
|
||||||
|
blkPkgIdx
|
||||||
|
blkFile
|
||||||
|
blkSymdef
|
||||||
|
blkHashed64def
|
||||||
|
blkHasheddef
|
||||||
|
blkNonpkgdef
|
||||||
|
blkNonpkgref
|
||||||
|
blkRefFlags
|
||||||
|
blkHash64
|
||||||
|
blkHash
|
||||||
|
blkRelocIdx
|
||||||
|
blkAuxIdx
|
||||||
|
blkDataIdx
|
||||||
|
blkReloc
|
||||||
|
blkAux
|
||||||
|
blkData
|
||||||
|
blkRefName
|
||||||
|
blkEnd
|
||||||
|
)
|
||||||
|
|
||||||
|
const goobjMagic = "\x00go120ld"
|
||||||
|
|
||||||
|
// extractGOOBJCode parses a GOOBJ payload and returns the code bytes for
|
||||||
|
// each non-package STEXT symbol (the functions).
|
||||||
|
func extractGOOBJCode(data []byte) (map[string][]byte, error) {
|
||||||
|
// Find the GOOBJ header (after the "go object ..." preamble).
|
||||||
|
i := bytes.Index(data, []byte(goobjMagic))
|
||||||
|
if i < 0 {
|
||||||
|
return nil, fmt.Errorf("verify: no GOOBJ magic in object file")
|
||||||
|
}
|
||||||
|
b := data[i:]
|
||||||
|
le := binary.LittleEndian
|
||||||
|
|
||||||
|
// Read block offsets (20 bytes into the header: 4 magic + 8 go version
|
||||||
|
// + 8 experiment = 20, then blkEnd+1 uint32 offsets).
|
||||||
|
var offs [blkEnd + 1]uint32
|
||||||
|
for j := 0; j <= blkEnd; j++ {
|
||||||
|
offs[j] = le.Uint32(b[20+4*j:])
|
||||||
|
}
|
||||||
|
blk := func(idx int) []byte { return b[offs[idx]:offs[idx+1]] }
|
||||||
|
|
||||||
|
// Parse non-package symbol definitions (blkNonpkgdef): each entry is
|
||||||
|
// 21 bytes: [nameLen:4][nameOff:4][abi:2][type:1][flag:1][flag2:1][size:4][align:4].
|
||||||
|
const symSize = 21
|
||||||
|
nonpkg := blk(blkNonpkgdef)
|
||||||
|
nSyms := len(nonpkg) / symSize
|
||||||
|
|
||||||
|
// Data index (blkDataIdx): one uint32 per defined symbol across ALL
|
||||||
|
// definition blocks (blkSymdef + blkHashed64def + blkHasheddef +
|
||||||
|
// blkNonpkgdef), in that order. We need the offset for the nonpkg
|
||||||
|
// symbols, which come last.
|
||||||
|
dataIdx := blk(blkDataIdx)
|
||||||
|
dataBlk := blk(blkData)
|
||||||
|
|
||||||
|
// Count symbols in the preceding definition blocks.
|
||||||
|
preceding := 0
|
||||||
|
for _, bi := range []int{blkSymdef, blkHashed64def, blkHasheddef} {
|
||||||
|
preceding += len(blk(bi)) / symSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symbol name offsets in the GOOBJ symbol table are absolute byte
|
||||||
|
// offsets from the start of the GOOBJ payload (the magic).
|
||||||
|
readStr := func(off, ln uint32) string {
|
||||||
|
if int(off+ln) > len(b) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(b[off : off+ln])
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string][]byte)
|
||||||
|
const kindSTEXT = 1
|
||||||
|
for s := 0; s < nSyms; s++ {
|
||||||
|
x := nonpkg[s*symSize:]
|
||||||
|
nameLen := le.Uint32(x[0:])
|
||||||
|
nameOff := le.Uint32(x[4:])
|
||||||
|
typ := x[10]
|
||||||
|
size := le.Uint32(x[13:])
|
||||||
|
|
||||||
|
if typ != kindSTEXT || size == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := readStr(nameOff, nameLen)
|
||||||
|
// Strip the package prefix (everything up to and including the
|
||||||
|
// last middle dot or period-dot).
|
||||||
|
name = stripPkg(name)
|
||||||
|
|
||||||
|
// Data offset from the index (nonpkg symbols follow the preceding blocks).
|
||||||
|
diIdx := preceding + s
|
||||||
|
if (diIdx+1)*4 > len(dataIdx) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dOff := le.Uint32(dataIdx[diIdx*4:])
|
||||||
|
if int(dOff+size) > len(dataBlk) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
code := make([]byte, size)
|
||||||
|
copy(code, dataBlk[dOff:dOff+size])
|
||||||
|
result[name] = code
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stripPkg removes the package path prefix from a symbol name, leaving
|
||||||
|
// just the function name. "pkg/path·FuncName" → "FuncName".
|
||||||
|
func stripPkg(name string) string {
|
||||||
|
if i := strings.LastIndex(name, "\u00B7"); i >= 0 {
|
||||||
|
return name[i+len("\u00B7"):]
|
||||||
|
}
|
||||||
|
if i := strings.LastIndex(name, "\"."); i >= 0 {
|
||||||
|
return name[i+2:]
|
||||||
|
}
|
||||||
|
if i := strings.LastIndex(name, "."); i >= 0 {
|
||||||
|
return name[i+1:]
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package verify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGroundTruthBasic(t *testing.T) {
|
||||||
|
// Use the simple test kernel — it assembles with go tool asm.
|
||||||
|
gt, err := GroundTruth("../testdata/verify/basic_amd64.s")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GroundTruth: %v", err)
|
||||||
|
}
|
||||||
|
if len(gt) == 0 {
|
||||||
|
t.Fatal("no functions extracted from ground truth")
|
||||||
|
}
|
||||||
|
// The "add" function should be present and non-empty.
|
||||||
|
code, ok := gt["add"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("function 'add' not found in ground truth; got: %v", keys(gt))
|
||||||
|
}
|
||||||
|
if len(code) == 0 {
|
||||||
|
t.Fatal("add: zero-length code")
|
||||||
|
}
|
||||||
|
t.Logf("ground truth functions: %v", keys(gt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroundTruthComparison(t *testing.T) {
|
||||||
|
// Assemble with gasm and compare against go tool asm.
|
||||||
|
k, err := Load("../testdata/verify/basic_amd64.s")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
defer k.Close()
|
||||||
|
|
||||||
|
gt, err := GroundTruth("../testdata/verify/basic_amd64.s")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GroundTruth: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range k.FuncNames() {
|
||||||
|
fl, _ := k.Func(name)
|
||||||
|
gasmCode := k.Image().Code[fl.Offset : fl.Offset+fl.Size]
|
||||||
|
goCode, ok := gt[name]
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("%s: not in ground truth", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(gasmCode) != len(goCode) {
|
||||||
|
t.Errorf("%s: size mismatch: gasm=%d go=%d", name, len(gasmCode), len(goCode))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := range gasmCode {
|
||||||
|
if gasmCode[i] != goCode[i] {
|
||||||
|
t.Errorf("%s: byte %d differs: gasm=%02x go=%02x", name, i, gasmCode[i], goCode[i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroundTruthBadFile(t *testing.T) {
|
||||||
|
_, err := GroundTruth("/nonexistent/file_amd64.s")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for nonexistent file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func keys(m map[string][]byte) []string {
|
||||||
|
out := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
out = append(out, k)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -120,3 +120,8 @@ func (k *Kernel) Close() {
|
|||||||
k.exec.Unmap()
|
k.exec.Unmap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Image returns the assembled image (code + data + metadata).
|
||||||
|
func (k *Kernel) Image() *asm.Image {
|
||||||
|
return k.img
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user