// Copyright (c) 2026 Petr BalvĂ­n (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) } } } // 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) } }