feat(asm): encode the arm64 system registers and structure loads
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package asm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
||||
)
|
||||
|
||||
// TestARM64SysRegsDifferential proves the whole system-register table against
|
||||
// the toolchain at once: one TEXT whose body reads every register the table
|
||||
// carries (and writes every writable one), assembled by gasm and by
|
||||
// go tool asm, must agree byte for byte. A single wrong op0/op1/CRn/CRm/op2
|
||||
// packing names its register through the first differing word.
|
||||
func TestARM64SysRegsDifferential(t *testing.T) {
|
||||
names := make([]string, 0, len(a64SysRegs))
|
||||
for name := range a64SysRegs {
|
||||
names = append(names, name)
|
||||
}
|
||||
slices.Sort(names)
|
||||
|
||||
var body strings.Builder
|
||||
for i, name := range names {
|
||||
// R18 is the arm64 platform register and R29-R31 carry dedicated
|
||||
// meanings; a plain read/write destination keeps to R0-R17.
|
||||
reg := fmt.Sprintf("R%d", i%18)
|
||||
if a64SysRegs[name].read {
|
||||
body.WriteString(fmt.Sprintf("\tMRS %s, %s\n", name, reg))
|
||||
}
|
||||
if a64SysRegs[name].write {
|
||||
body.WriteString(fmt.Sprintf("\tMSR %s, %s\n", reg, name))
|
||||
}
|
||||
}
|
||||
src := "#include \"textflag.h\"\n\nTEXT ·sysregs(SB), NOSPLIT, $0\n" + body.String() + "\tRET\n"
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "sysregs_arm64.s")
|
||||
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertARM64Differential(t, path, src, "sysregs")
|
||||
}
|
||||
|
||||
// TestARM64FamiliesDifferential pins the non-sysreg families the arm64
|
||||
// campaign added: the LSE compare-and-swap pairs, the VMOVI immediate, the
|
||||
// SIMD narrow/long shift pairs, the VLD2/VLD3/VLD4 and VST2/VST3/VST4
|
||||
// structure accesses with their post-index and replicate forms, LDPSW, the
|
||||
// pointer-authentication hint and the DC maintenance operation. Every
|
||||
// spelling is the toolchain's own, taken from its arm64 testdata, and the
|
||||
// bytes must agree word for word.
|
||||
func TestARM64FamiliesDifferential(t *testing.T) {
|
||||
src := `#include "textflag.h"
|
||||
|
||||
TEXT ·families(SB), NOSPLIT, $0
|
||||
CASPD (R2, R3), (R2), (R8, R9)
|
||||
CASPW (R6, R7), (R8), (R4, R5)
|
||||
VMOVI $82, V0.B16
|
||||
VMOVI $146, V22.B16
|
||||
VSSHLL $0, V1.B8, V2.H8
|
||||
VSSHLL $7, V1.B8, V2.H8
|
||||
VSSHLL2 $0, V1.B16, V2.H8
|
||||
VSHRN $7, V1.H8, V0.B8
|
||||
VSHRN2 $31, V1.D2, V0.S4
|
||||
VLD2 (R29), [V23.H8, V24.H8]
|
||||
VLD2.P 16(R0), [V18.B8, V19.B8]
|
||||
VLD2.P (R1)(R2), [V15.S2, V16.S2]
|
||||
VLD3 (R27), [V11.S4, V12.S4, V13.S4]
|
||||
VLD3.P 48(RSP), [V11.S4, V12.S4, V13.S4]
|
||||
VLD4 (R15), [V10.H4, V11.H4, V12.H4, V13.H4]
|
||||
VLD4.P 32(R24), [V31.B8, V0.B8, V1.B8, V2.B8]
|
||||
VLD1R (R1), [V9.B8]
|
||||
VLD1R.P (R0), [V0.B16]
|
||||
VLD1R.P 2(R1), [V2.H4]
|
||||
VLD2R (R15), [V15.H4, V16.H4]
|
||||
VLD2R.P 16(R0), [V0.D2, V1.D2]
|
||||
VLD4R (R0), [V0.B8, V1.B8, V2.B8, V3.B8]
|
||||
VLD4R.P 16(RSP), [V31.S4, V0.S4, V1.S4, V2.S4]
|
||||
VST2 [V22.H8, V23.H8], (R23)
|
||||
VST2.P [V14.H4, V15.H4], 16(R17)
|
||||
VST2.P [V14.H4, V15.H4], (R3)(R17)
|
||||
VST3 [V1.D2, V2.D2, V3.D2], (R11)
|
||||
VST3.P [V18.S4, V19.S4, V20.S4], 48(R25)
|
||||
VST4 [V22.D2, V23.D2, V24.D2, V25.D2], (R3)
|
||||
VST4.P [V14.D2, V15.D2, V16.D2, V17.D2], 64(R15)
|
||||
LDPSW (R0), (R1, R2)
|
||||
LDPSW 4(R0), (R1, R2)
|
||||
LDPSW -4(R0), (R1, R2)
|
||||
PACIASP
|
||||
DC IVAC, R1
|
||||
RET
|
||||
`
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "families_arm64.s")
|
||||
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertARM64Differential(t, path, src, "families")
|
||||
}
|
||||
|
||||
// assertARM64Differential assembles the same source with gasm and with the
|
||||
// toolchain for arm64 and requires the named function's code bytes to agree.
|
||||
// The live oracle is a deliberate-run comparison, so -short skips it (the
|
||||
// push pipeline's mode); the golden bytes of the individual encoders are
|
||||
// pinned separately in every mode.
|
||||
func assertARM64Differential(t *testing.T, path, src, fn string) {
|
||||
t.Helper()
|
||||
oracle := oracleFuncCode(t, toolAsmObject(t, path, "arm64"))
|
||||
|
||||
// The oracle keys its functions by the qualified object name
|
||||
// (pkg.name); match on the local part.
|
||||
want := map[string][]byte{}
|
||||
for name, code := range oracle {
|
||||
if _, after, ok := strings.Cut(name, "."); ok {
|
||||
want[after] = code
|
||||
} else {
|
||||
want[name] = code
|
||||
}
|
||||
}
|
||||
if want[fn] == nil {
|
||||
t.Fatalf("the oracle object carries no function %q (has %v)", fn, keysOf(want))
|
||||
}
|
||||
|
||||
f, perrs := parser.Parse(path, src)
|
||||
if len(perrs) > 0 {
|
||||
t.Fatalf("parse: %v", perrs[0])
|
||||
}
|
||||
img, err := AssembleFileARM64(f)
|
||||
if err != nil {
|
||||
t.Fatalf("AssembleFileARM64: %v", err)
|
||||
}
|
||||
got := trimTrailingZeroWords(img.Code)
|
||||
wantB := trimTrailingZeroWords(want[fn])
|
||||
if len(got) != len(wantB) {
|
||||
t.Fatalf("gasm %d bytes, oracle %d bytes", len(got), len(wantB))
|
||||
}
|
||||
for i := range wantB {
|
||||
if got[i] != wantB[i] {
|
||||
t.Fatalf("word %d differs: gasm %08x, oracle %08x", i/4,
|
||||
binary.LittleEndian.Uint32(got[i:i+4]), binary.LittleEndian.Uint32(wantB[i:i+4]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// trimTrailingZeroWords drops whole zero words off the end of a code span:
|
||||
// an object pads a function to its alignment, and the raw image does not.
|
||||
// A difference in the middle survives the trim untouched.
|
||||
func trimTrailingZeroWords(b []byte) []byte {
|
||||
for len(b) >= 4 {
|
||||
last := b[len(b)-4:]
|
||||
if last[0]|last[1]|last[2]|last[3] != 0 {
|
||||
break
|
||||
}
|
||||
b = b[:len(b)-4]
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user