feat(asm): encode the amd64 XSAVE family

XSAVE, XSAVEOPT, XSAVEC and XSAVES with their restore twins, plain and
64, each pinned byte for byte against go tool asm through every corpus
line the toolchain's own amd64enc.s carries for the family (24 lines).
The toolchain emits XSAVEOPT without the manual's 0x66 prefix; the bytes
are the oracle, so the family carries none.

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-06 23:59:47 +02:00
1 parent f83bc8ddef
commit 257feace6e
3 files changed
+131 -1

No files matched your search

+17 -1
View File
@@ -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
}
+58
View File
@@ -0,0 +1,58 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (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)
}
}
}