diff --git a/asm/amd64_families.go b/asm/amd64_families.go index a78c8c2..b537a17 100644 --- a/asm/amd64_families.go +++ b/asm/amd64_families.go @@ -3,6 +3,8 @@ package asm +import "strings" + // encodeAmd64Family routes a mnemonic through the amd64 corpus families the // dedicated amd64_*.go files implement. The first family that owns the name // decides the outcome: its bytes, or its error. A name no family claims @@ -14,6 +16,9 @@ func (e *enc) encodeAmd64Family(upper string, ops []Operand) (bool, error) { if ok, err := e.encodeSystem(upper, ops); ok { return true, err } + if ok, err := e.encodeXsave(upper, ops); ok { + return true, err + } return false, nil } @@ -83,5 +88,16 @@ func amd64FamilyEncodable(upper string) bool { if _, ok := farSegLoad[base(upper, 3)]; ok { return true } - return upper == "CMPXCHG8B" || upper == "CMPXCHG16B" + if upper == "CMPXCHG8B" || upper == "CMPXCHG16B" { + return true + } + if _, ok := xsaveTable[upper]; ok { + return true + } + if before, ok := strings.CutSuffix(upper, "64"); ok { + if _, ok := xsaveTable[before]; ok { + return true + } + } + return false } diff --git a/asm/amd64_xsave.go b/asm/amd64_xsave.go new file mode 100644 index 0000000..2f6faf6 --- /dev/null +++ b/asm/amd64_xsave.go @@ -0,0 +1,58 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import ( + "fmt" + "strings" +) + +// This file implements the XSAVE family: the extended-state save and restore +// pair in its four generations (XSAVE/XRSTOR, XSAVEOPT, XSAVEC, XSAVES) and +// their 64 spellings. Each takes one memory operand alone; the 64 spellings +// carry REX.W. Every encoding here is pinned byte for byte against +// go tool asm through the corpus lines in amd64_xsave_test.go. + +// xsaveSpec is one XSAVE family member: the opcode group and the /digit. +// XSAVEOPT carries no mandatory prefix in the toolchain's encoding despite +// the manual's 0x66, so the family has none anywhere. +type xsaveSpec struct { + op []byte + digit int +} + +// xsaveTable maps the save/restore mnemonics to their encodings. The 64 +// spellings share the base's digit with REX.W. +var xsaveTable = map[string]xsaveSpec{ + "XSAVE": {[]byte{0x0F, 0xAE}, 4}, + "XRSTOR": {[]byte{0x0F, 0xAE}, 5}, + "XSAVEOPT": {[]byte{0x0F, 0xAE}, 6}, + "XSAVEC": {[]byte{0x0F, 0xC7}, 4}, + "XSAVES": {[]byte{0x0F, 0xC7}, 5}, + "XRSTORS": {[]byte{0x0F, 0xC7}, 3}, +} + +// encodeXsave encodes the XSAVE family. It reports whether the mnemonic +// belongs to the family. +func (e *enc) encodeXsave(upper string, ops []Operand) (bool, error) { + name, rexW := upper, false + if base, ok := strings.CutSuffix(upper, "64"); ok { + name, rexW = base, true + } + spec, ok := xsaveTable[name] + if !ok { + return false, nil + } + if len(ops) != 1 { + return true, fmt.Errorf("%s expects 1 memory operand, got %d", upper, len(ops)) + } + if !isX86Mem(ops[0]) { + return true, fmt.Errorf("%s requires a memory operand", upper) + } + i := &instr{opcode: append([]byte(nil), spec.op...), modrm: -1, sib: -1, rexW: rexW} + if err := setRMDigit(i, spec.digit, ops[0], 8); err != nil { + return true, err + } + return true, e.emit(i) +} diff --git a/asm/amd64_xsave_test.go b/asm/amd64_xsave_test.go new file mode 100644 index 0000000..b5bf354 --- /dev/null +++ b/asm/amd64_xsave_test.go @@ -0,0 +1,56 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +package asm + +import "testing" + +// amd64XsaveCorpus holds every line the Go toolchain's own +// amd64enc.s carries for the XSAVE family (XSAVE, XSAVEOPT, XSAVEC, XSAVES and the restores, plain and 64), with the bytes go tool asm +// emits for each: the differential ground truth the family is proven +// against, line for line. +var amd64XsaveCorpus = []struct { + line string + want string +}{ + {"XRSTOR (BX)", "0f ae 2b"}, + {"XRSTOR (R11)", "41 0f ae 2b"}, + {"XRSTOR64 (BX)", "48 0f ae 2b"}, + {"XRSTOR64 (R11)", "49 0f ae 2b"}, + {"XRSTORS (BX)", "0f c7 1b"}, + {"XRSTORS (R11)", "41 0f c7 1b"}, + {"XRSTORS64 (BX)", "48 0f c7 1b"}, + {"XRSTORS64 (R11)", "49 0f c7 1b"}, + {"XSAVE (BX)", "0f ae 23"}, + {"XSAVE (R11)", "41 0f ae 23"}, + {"XSAVE64 (BX)", "48 0f ae 23"}, + {"XSAVE64 (R11)", "49 0f ae 23"}, + {"XSAVEC (BX)", "0f c7 23"}, + {"XSAVEC (R11)", "41 0f c7 23"}, + {"XSAVEC64 (BX)", "48 0f c7 23"}, + {"XSAVEC64 (R11)", "49 0f c7 23"}, + {"XSAVEOPT (BX)", "0f ae 33"}, + {"XSAVEOPT (R11)", "41 0f ae 33"}, + {"XSAVEOPT64 (BX)", "48 0f ae 33"}, + {"XSAVEOPT64 (R11)", "49 0f ae 33"}, + {"XSAVES (BX)", "0f c7 2b"}, + {"XSAVES (R11)", "41 0f c7 2b"}, + {"XSAVES64 (BX)", "48 0f c7 2b"}, + {"XSAVES64 (R11)", "49 0f c7 2b"}, +} + +// TestAmd64XsaveCorpus assembles every corpus line and requires the same bytes +// go tool asm emits for it. +func TestAmd64XsaveCorpus(t *testing.T) { + for _, tc := range amd64XsaveCorpus { + fn := firstText(t, "TEXT ·p(SB), 4, $0\n\t"+tc.line+"\n") + code, _, err := Assemble(fn) + if err != nil { + t.Errorf("%s: %v", tc.line, err) + continue + } + if got := hexBytes(code); got != tc.want { + t.Errorf("%s: got %s, want %s", tc.line, got, tc.want) + } + } +}