Files
gasm-sdk/arch/amd64_ext_mem_test.go
T
2026-10-07 02:06:11 +02:00

202 lines
7.2 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package arch
import (
"encoding/hex"
"strings"
"testing"
)
// The memory-operand mechanism: the base-plus-displacement validation and
// the ModR/M, SIB and displacement byte choices. The expected words are
// built on the VMOVSH memory-load template, whose rows the binutils-gdb
// assembler testsuite quotes byte for byte; the rows marked GNU match the
// listing bytes. Note on the quoted disp8 rows: binutils mainline encodes
// EVEX displacements with the APX disp8*N scaling, so its source spellings
// (254 for the m16 rows, 8128 for the m512 ones) are N times the plain SDM
// displacement the disp8 bytes carry; the words here pin the bytes with the
// plain displacement those bytes encode.
func TestAmd64ExtMemoryEncoding(t *testing.T) {
load := []byte{0x62, 0x05, 0x06, 0x00, 0x10, 0xC0}
for _, tt := range []struct {
name string
base int
disp int64
want string
gnuSource string // the binutils source line the bytes serve, empty for a derived row
}{
{"zero displacement drops the disp bytes", 9, 0,
"62457e081031", "vmovsh (%r9),%xmm30"},
{"positive disp8", 1, 127,
"62657e0810717f", "vmovsh 254(%rcx),%xmm30 (Disp8(7f) under the disp8*N scaling)"},
{"negative disp8", 2, -128,
"62657e08107280", "vmovsh -256(%rdx),%xmm30 (Disp8(80) under the disp8*N scaling)"},
{"disp32 past the disp8 range", 2, 8128,
"62657e0810b2c01f0000", ""},
{"negative disp32", 13, -200,
"62457e0810b538ffffff", ""},
{"RSP base takes the SIB byte", 12, 0,
"62457e08103424", ""},
{"RSP base with a disp8", 12, 4,
"62457e0810742404", ""},
{"RBP base keeps a zero displacement explicit", 5, 0,
"62657e08107500", ""},
} {
got := amd64EncodeMemory(load, 30, -1, tt.base, tt.disp)
if hex.EncodeToString(got) != tt.want {
t.Errorf("%s:\n got %x\n want %s", tt.name, got, tt.want)
}
}
}
// TestAmd64ExtMemoryRejects checks the validation around the memory operand:
// the kinds and ranges the layer refuses before a byte is laid down.
func TestAmd64ExtMemoryRejects(t *testing.T) {
in := ExtInstr{Name: "TEST"}
for _, tt := range []struct {
name string
op ExtOperand
quote string
}{
{"a register where the memory operand belongs", ExtXmm(3),
"wants a memory operand"},
{"base beyond r15", ExtMemory(16, 0),
"outside 0-15"},
{"base under r0", ExtMemory(-1, 0),
"outside 0-15"},
{"displacement past the signed 32-bit ceiling", ExtMemory(8, 1<<31),
"outside the signed 32-bit range"},
{"displacement past the signed 32-bit floor", ExtMemory(8, -1<<31-1),
"outside the signed 32-bit range"},
{"shift on the memory operand", ExtOperand{Kind: ExtMem, Reg: 8, Imm: 4, Shift: 2, HasShift: true},
"take none"},
{"arrangement suffix on the memory operand", ExtOperand{Kind: ExtMem, Reg: 8, Arr: ExtArrH},
"arrangement"},
{"predicate qualifier on the memory operand", ExtOperand{Kind: ExtMem, Reg: 8, Qual: ExtQualZeroing},
"predicate qualifier"},
{"broadcast on an entry that takes none", ExtBroadcast(8, 0),
"takes none"},
} {
_, _, err := in.amd64Memory(tt.op, 1)
if err == nil {
t.Errorf("%s: validation succeeded, want an error", tt.name)
continue
}
if !strings.Contains(err.Error(), tt.quote) {
t.Errorf("%s: error %q lacks %q", tt.name, err, tt.quote)
}
}
// The broadcast spelling passes the flag gate on an entry that carries
// Bcast and then meets the same base and displacement checks.
bcast := ExtInstr{Name: "TEST", Bcast: true}
for _, tt := range []struct {
name string
op ExtOperand
quote string
}{
{"broadcast base beyond r15", ExtOperand{Kind: ExtMem, Reg: 16, Imm: 0, Broadcast: true},
"outside 0-15"},
{"broadcast displacement past the signed 32-bit ceiling", ExtBroadcast(8, 1<<31),
"outside the signed 32-bit range"},
{"broadcast base under r0", ExtBroadcast(-1, 0),
"outside 0-15"},
} {
if _, _, err := bcast.amd64Memory(tt.op, 1); err == nil {
t.Errorf("%s: validation succeeded, want an error", tt.name)
} else if !strings.Contains(err.Error(), tt.quote) {
t.Errorf("%s: error %q lacks %q", tt.name, err, tt.quote)
}
}
}
// TestAmd64ExtBroadcastEncoding pins the broadcast layer over the memory
// encoding: EVEX.b, bit 4 of byte three, set on the VADDPH 512-bit template
// while the ModR/M mod bits, the SIB byte and the displacement choices keep
// the plain semantics amd64EncodeMemory chooses.
func TestAmd64ExtBroadcastEncoding(t *testing.T) {
add := []byte{0x62, 0x05, 0x04, 0x40, 0x58, 0xC0}
for _, tt := range []struct {
name string
base int
disp int64
want string
}{
{"zero displacement keeps the mod-00 form under the broadcast bit", 9, 0,
"624514505831"},
{"disp8 semantics unchanged", 1, 127,
"6265145058717f"},
{"disp32 semantics unchanged", 2, 8128,
"6265145058b2c01f0000"},
{"RBP keeps the forced displacement", 5, 0,
"62651450587500"},
{"RSP keeps the SIB byte", 12, 0,
"62451450583424"},
} {
got := amd64EncodeBroadcast(add, 30, 29, tt.base, tt.disp)
if hex.EncodeToString(got) != tt.want {
t.Errorf("%s:\n got %x\n want %s", tt.name, got, tt.want)
}
}
}
// TestAmd64ExtScaledMemoryEncoding pins the SIB layer over the memory
// encoding: the scale field, the index and the base in one byte, the r/m
// field 100, EVEX.X clearing on an index above 7, and the ModR/M and
// displacement choices keeping the plain semantics, RBP's forced
// displacement included.
func TestAmd64ExtScaledMemoryEncoding(t *testing.T) {
add := []byte{0x62, 0x05, 0x04, 0x40, 0x58, 0xC0}
for _, tt := range []struct {
name string
base int
index int
scale int
disp int64
want string
}{
{"scale 1 encodes the scale field zero", 1, 2, 1, 0,
"62651440583411"},
{"scale 2", 1, 2, 2, 0,
"62651440583451"},
{"scale 4", 1, 2, 4, 0,
"62651440583491"},
{"scale 8", 1, 2, 8, 0,
"626514405834d1"},
{"an index above 7 clears EVEX.X", 1, 12, 2, 0,
"62251440583461"},
{"RBP base keeps the forced displacement", 5, 14, 8, 0,
"622514405874f500"},
{"RSP base takes the SIB byte with the index", 12, 3, 4, 0,
"6245144058349c"},
} {
got := amd64EncodeScaledMemory(add, 30, 29, tt.base, tt.index, tt.scale, tt.disp)
if hex.EncodeToString(got) != tt.want {
t.Errorf("%s:\n got %x\n want %s", tt.name, got, tt.want)
}
}
}
// TestAmd64ExtMemoryVocabulary pins the names the shared layer gives the
// memory operand and its two forms.
func TestAmd64ExtMemoryVocabulary(t *testing.T) {
if got := ExtMem.String(); got != "memory operand" {
t.Errorf("ExtMem = %q, want %q", got, "memory operand")
}
if got := ExtFormAmdMemVec.String(); got != "memory into a vector" {
t.Errorf("ExtFormAmdMemVec = %q, want %q", got, "memory into a vector")
}
if got := ExtFormAmdVecMem.String(); got != "a vector into memory" {
t.Errorf("ExtFormAmdVecMem = %q, want %q", got, "a vector into memory")
}
for _, f := range []ExtForm{ExtFormAmdMemVec, ExtFormAmdVecMem} {
if got := f.Arity(); got != 2 {
t.Errorf("%s takes %d operands, want 2", f, got)
}
}
if got := ExtMemory(9, 4096); got.Kind != ExtMem || got.Reg != 9 || got.Imm != 4096 {
t.Errorf("ExtMemory(9, 4096) = %+v, want base 9 with displacement 4096", got)
}
}