feat(asm): encode the amd64 x87 family

The x87 stack controls, the D8/DC arithmetic pair, the conditional moves,
the register compares, FADDDP, the memory loads and the FXSAVE pair, each
pinned byte for byte against go tool asm through every corpus line the
toolchain's own amd64enc.s carries for the family (78 lines).

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-06 23:59:47 +02:00
1 parent 1040739fbb
commit f0b08ccea0
5 files changed
+401

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
// 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
// falls through to the scalar dispatch in (*enc).encode untouched.
func (e *enc) encodeAmd64Family(upper string, ops []Operand) (bool, error) {
if ok, err := e.encodeX87(upper, ops); ok {
return true, err
}
return false, nil
}
// amd64FamilyEncodable mirrors encodeAmd64Family for the lint-time
// predicate: true when some family owns the mnemonic, whatever the operand
// shapes. It must claim exactly the names encodeAmd64Family does.
func amd64FamilyEncodable(upper string) bool {
if _, ok := x87NoOperand[upper]; ok {
return true
}
if _, ok := x87Arith[upper]; ok {
return true
}
if _, ok := x87FCmov[upper]; ok {
return true
}
if _, ok := x87Compare[upper]; ok {
return true
}
if _, ok := x87MemUnary[upper]; ok {
return true
}
if _, ok := x87Fxsav[upper]; ok {
return true
}
return upper == "FADDDP"
}
+237
View File
@@ -0,0 +1,237 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import "fmt"
// This file implements the x87 floating-point family the Go assembler
// carries: the no-operand stack controls, the two-register arithmetic pair,
// the register compares, the memory loads and stores and the FXSAVE pair.
// Every encoding here is pinned byte for byte against go tool asm through
// the corpus lines in amd64_x87_test.go.
// x87NoOperand maps the no-operand x87 instruction to its postfix byte
// inside the D9 escape: D9 <postfix>, no ModR/M, no operand.
var x87NoOperand = map[string]byte{
"F2XM1": 0xF0,
"FABS": 0xE1,
"FCHS": 0xE0,
"FCOS": 0xFF,
"FDECSTP": 0xF6,
"FINCSTP": 0xF7,
"FLD1": 0xE8,
"FLDL2E": 0xEA,
"FLDL2T": 0xE9,
"FLDLG2": 0xEC,
"FLDPI": 0xEB,
"FNOP": 0xD0,
"FPATAN": 0xF3,
"FPREM": 0xF8,
"FPREM1": 0xF5,
"FPTAN": 0xF2,
"FRNDINT": 0xFC,
"FSCALE": 0xFD,
"FSIN": 0xFE,
"FSINCOS": 0xFB,
"FSQRT": 0xFA,
"FTST": 0xE4,
"FXAM": 0xE5,
"FXTRACT": 0xF4,
"FYL2X": 0xF1,
"FYL2XP1": 0xF9,
}
// x87ArithSpec describes one member of the D8/DC two-register arithmetic
// pair. The D8 form reads ST(0) as its second operand (FADDD F2, F0), the
// DC form ST(0) as its first (FADDD F0, F2) or a memory source (FADDD (BX),
// F0). FDIV is the odd member: with ST(0) as the first operand the toolchain
// assembles the reversed register form (DC F8+i, the FDIVR digit), so the DC
// digit differs from the memory digit there.
type x87ArithSpec struct {
d8Digit int // the /digit of the D8 form (dst = ST(0))
dcDigit int // the /digit of the DC register form (src = ST(0))
memDigit int // the /digit of the DC memory form (dst = ST(0))
}
// x87Arith maps the arithmetic mnemonics to their digits.
var x87Arith = map[string]x87ArithSpec{
"FADDD": {0, 0, 0},
"FCOMD": {2, 2, 2},
"FDIVD": {6, 7, 6},
}
// x87FCmov maps the conditional x87 moves to their escape byte and postfix
// base (the C0/C8/D0/D8 group the condition selects); the compared register
// rides the postfix's low three bits.
var x87FCmov = map[string][2]byte{
"FCMOVB": {0xDA, 0xC0},
"FCMOVBE": {0xDA, 0xD0},
"FCMOVE": {0xDA, 0xC8},
"FCMOVNB": {0xDB, 0xC0},
"FCMOVNBE": {0xDB, 0xD0},
"FCMOVNE": {0xDB, 0xC8},
"FCMOVNU": {0xDB, 0xD8},
"FCMOVU": {0xDA, 0xD8},
}
// x87Compare maps the register compare pair to their escape byte; the
// register form is escape F0+i (mod 11, reg 110, rm = the compared stack
// register), ST(0) fixed as the second operand.
var x87Compare = map[string]byte{
"FCOMI": 0xDB,
"FCOMIP": 0xDF,
}
// x87MemUnary maps the one-memory-operand x87 controls to their escape byte
// and /digit.
var x87MemUnary = map[string]struct {
escape byte
digit int
}{
"FBLD": {0xDF, 4},
"FBSTP": {0xDF, 6},
"FLDCW": {0xD9, 5},
}
// x87Fxsav maps the FXSAVE pair to their /digit in the 0F AE group; the 64
// spellings carry REX.W.
var x87Fxsav = map[string]struct {
digit int
rexW bool
}{
"FXSAVE": {0, false},
"FXSAVE64": {0, true},
"FXRSTOR": {1, false},
"FXRSTOR64": {1, true},
}
// encodeX87 encodes the x87 family. It reports whether the mnemonic belongs
// to the family; a false result hands the mnemonic back to the caller, an
// error result a failed attempt to encode it.
func (e *enc) encodeX87(upper string, ops []Operand) (bool, error) {
if post, ok := x87NoOperand[upper]; ok {
if len(ops) != 0 {
return true, fmt.Errorf("%s takes no operands, got %d", upper, len(ops))
}
return true, e.emit(&instr{opcode: []byte{0xD9, post}, modrm: -1, sib: -1})
}
if spec, ok := x87Arith[upper]; ok {
return true, e.encodeX87Arith(upper, spec, ops)
}
if esc, ok := x87FCmov[upper]; ok {
src, _, err := x87PairOperands(upper, ops)
if err != nil {
return true, err
}
// The condition applies between the named register and ST(0), so the
// second operand is always F0; the first rides the postfix's low bits.
return true, e.emit(&instr{opcode: []byte{esc[0], esc[1] | byte(src.idx&7)}, modrm: -1, sib: -1})
}
if escape, ok := x87Compare[upper]; ok {
if _, _, err := x87PairOperands(upper, ops); err != nil {
return true, err
}
src, _ := ops[0].(Reg)
return true, e.emit(&instr{opcode: []byte{escape, 0xF0 | byte(src.idx&7)}, modrm: -1, sib: -1})
}
if upper == "FADDDP" {
if len(ops) != 2 {
return true, fmt.Errorf("FADDDP expects 2 operands, got %d", len(ops))
}
first, ok1 := ops[0].(Reg)
second, ok2 := ops[1].(Reg)
if !ok1 || !ok2 || !first.fp || !second.fp {
return true, fmt.Errorf("FADDDP takes two x87 stack registers")
}
if first.idx != 0 {
return true, fmt.Errorf("FADDDP: the first operand must be F0")
}
return true, e.emit(&instr{opcode: []byte{0xDE, 0xC0 | byte(second.idx&7)}, modrm: -1, sib: -1})
}
if m, ok := x87MemUnary[upper]; ok {
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: []byte{m.escape}, modrm: -1, sib: -1}
if err := setRMDigit(i, m.digit, ops[0], 8); err != nil {
return true, err
}
return true, e.emit(i)
}
if m, ok := x87Fxsav[upper]; ok {
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 := newInstr(0, []byte{0x0F, 0xAE})
i.rexW = m.rexW
if err := setRMDigit(i, m.digit, ops[0], 8); err != nil {
return true, err
}
return true, e.emit(i)
}
return false, nil
}
// encodeX87Arith encodes one member of the D8/DC arithmetic pair. The tool-
// chain's shape set: (Fn, F0) rides D8, (F0, Fn) rides DC, ((m), F0) rides
// the DC memory digit; every other pairing is an error.
func (e *enc) encodeX87Arith(mnem string, spec x87ArithSpec, ops []Operand) error {
if len(ops) != 2 {
return fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
src, dst := ops[0], ops[1]
dstReg, dstIsF := dst.(Reg)
if !dstIsF || !dstReg.fp {
return fmt.Errorf("%s: the destination must be an x87 stack register", mnem)
}
switch s := src.(type) {
case Reg:
if !s.fp {
return fmt.Errorf("%s: the source must be an x87 stack register", mnem)
}
switch {
case dstReg.idx == 0:
return e.emit(&instr{opcode: []byte{0xD8, 0xC0 | byte(spec.d8Digit)<<3 | byte(s.idx&7)}, modrm: -1, sib: -1})
case s.idx == 0:
return e.emit(&instr{opcode: []byte{0xDC, 0xC0 | byte(spec.dcDigit)<<3 | byte(dstReg.idx&7)}, modrm: -1, sib: -1})
default:
return fmt.Errorf("%s: one operand must be F0", mnem)
}
default:
if !isX86Mem(src) {
return fmt.Errorf("%s: the source must be an x87 stack register or memory", mnem)
}
if dstReg.idx != 0 {
return fmt.Errorf("%s: the destination must be F0 with a memory source", mnem)
}
i := &instr{opcode: []byte{0xDC}, modrm: -1, sib: -1}
if err := setRMDigit(i, spec.memDigit, src, 8); err != nil {
return err
}
return e.emit(i)
}
}
// x87PairOperands validates the (register, F0) shape the conditional moves
// and compares take and returns the two registers.
func x87PairOperands(mnem string, ops []Operand) (Reg, Reg, error) {
if len(ops) != 2 {
return Reg{}, Reg{}, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
src, ok1 := ops[0].(Reg)
dst, ok2 := ops[1].(Reg)
if !ok1 || !ok2 || !src.fp || !dst.fp {
return Reg{}, Reg{}, fmt.Errorf("%s takes two x87 stack registers", mnem)
}
if dst.idx != 0 {
return Reg{}, Reg{}, fmt.Errorf("%s: the second operand must be F0", mnem)
}
return src, dst, nil
}
+110
View File
@@ -0,0 +1,110 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import "testing"
// amd64X87Corpus holds every line the Go toolchain's own
// amd64enc.s carries for the x87 family (stack controls, the arithmetic pair, the compares, the memory loads and the FXSAVE pair), with the bytes go tool asm
// emits for each: the differential ground truth the family is proven
// against, line for line.
var amd64X87Corpus = []struct {
line string
want string
}{
{"F2XM1", "d9 f0"},
{"FABS", "d9 e1"},
{"FADDD F2, F0", "d8 c2"},
{"FADDD F3, F0", "d8 c3"},
{"FADDD F0, F2", "dc c2"},
{"FADDD F0, F3", "dc c3"},
{"FADDD (BX), F0", "dc 03"},
{"FADDD (R11), F0", "41 dc 03"},
{"FADDDP F0, F2", "de c2"},
{"FADDDP F0, F3", "de c3"},
{"FBLD (BX)", "df 23"},
{"FBLD (R11)", "41 df 23"},
{"FBSTP (BX)", "df 33"},
{"FBSTP (R11)", "41 df 33"},
{"FCHS", "d9 e0"},
{"FCMOVB F2, F0", "da c2"},
{"FCMOVB F3, F0", "da c3"},
{"FCMOVBE F2, F0", "da d2"},
{"FCMOVBE F3, F0", "da d3"},
{"FCMOVE F2, F0", "da ca"},
{"FCMOVE F3, F0", "da cb"},
{"FCMOVNB F2, F0", "db c2"},
{"FCMOVNB F3, F0", "db c3"},
{"FCMOVNBE F2, F0", "db d2"},
{"FCMOVNBE F3, F0", "db d3"},
{"FCMOVNE F2, F0", "db ca"},
{"FCMOVNE F3, F0", "db cb"},
{"FCMOVNU F2, F0", "db da"},
{"FCMOVNU F3, F0", "db db"},
{"FCMOVU F2, F0", "da da"},
{"FCMOVU F3, F0", "da db"},
{"FCOMD F2, F0", "d8 d2"},
{"FCOMD F3, F0", "d8 d3"},
{"FCOMD (BX), F0", "dc 13"},
{"FCOMD (R11), F0", "41 dc 13"},
{"FCOMI F2, F0", "db f2"},
{"FCOMI F3, F0", "db f3"},
{"FCOMIP F2, F0", "df f2"},
{"FCOMIP F3, F0", "df f3"},
{"FCOS", "d9 ff"},
{"FDECSTP", "d9 f6"},
{"FDIVD F2, F0", "d8 f2"},
{"FDIVD F3, F0", "d8 f3"},
{"FDIVD F0, F2", "dc fa"},
{"FDIVD F0, F3", "dc fb"},
{"FDIVD (BX), F0", "dc 33"},
{"FDIVD (R11), F0", "41 dc 33"},
{"FINCSTP", "d9 f7"},
{"FLD1", "d9 e8"},
{"FLDCW (BX)", "d9 2b"},
{"FLDCW (R11)", "41 d9 2b"},
{"FLDL2E", "d9 ea"},
{"FLDL2T", "d9 e9"},
{"FLDLG2", "d9 ec"},
{"FLDPI", "d9 eb"},
{"FNOP", "d9 d0"},
{"FPATAN", "d9 f3"},
{"FPREM", "d9 f8"},
{"FPREM1", "d9 f5"},
{"FPTAN", "d9 f2"},
{"FRNDINT", "d9 fc"},
{"FSCALE", "d9 fd"},
{"FSIN", "d9 fe"},
{"FSINCOS", "d9 fb"},
{"FSQRT", "d9 fa"},
{"FTST", "d9 e4"},
{"FXAM", "d9 e5"},
{"FXRSTOR (BX)", "0f ae 0b"},
{"FXRSTOR (R11)", "41 0f ae 0b"},
{"FXRSTOR64 (BX)", "48 0f ae 0b"},
{"FXRSTOR64 (R11)", "49 0f ae 0b"},
{"FXSAVE (BX)", "0f ae 03"},
{"FXSAVE (R11)", "41 0f ae 03"},
{"FXSAVE64 (BX)", "48 0f ae 03"},
{"FXSAVE64 (R11)", "49 0f ae 03"},
{"FXTRACT", "d9 f4"},
{"FYL2X", "d9 f1"},
{"FYL2XP1", "d9 f9"},
}
// TestAmd64X87Corpus assembles every corpus line and requires the same bytes
// go tool asm emits for it.
func TestAmd64X87Corpus(t *testing.T) {
for _, tc := range amd64X87Corpus {
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)
}
}
}
+7
View File
@@ -16,6 +16,13 @@ import "strings"
func Encodable(mnemonic string) bool { func Encodable(mnemonic string) bool {
upper := strings.ToUpper(mnemonic) upper := strings.ToUpper(mnemonic)
// The corpus families first, mirroring encode()'s dispatch order: a
// family owning the name decides encodability whatever the suffix split
// would make of it.
if amd64FamilyEncodable(upper) {
return true
}
// Fixed-name instructions (no size suffix). // Fixed-name instructions (no size suffix).
switch upper { switch upper {
case "RET", "NOP", "CALL", "JMP", case "RET", "NOP", "CALL", "JMP",
+7
View File
@@ -70,6 +70,13 @@ type encPatch struct {
func (e *enc) encode(mnem string, ops []Operand) error { func (e *enc) encode(mnem string, ops []Operand) error {
upper := strings.ToUpper(mnem) upper := strings.ToUpper(mnem)
// The corpus families (x87, the system and string controls, XSAVE)
// dispatch on the full name from the amd64 family files before the
// fixed-name switch, the way their tables spell the mnemonics out.
if handled, err := e.encodeAmd64Family(upper, ops); handled {
return err
}
// Fixed-name instructions (no size suffix). // Fixed-name instructions (no size suffix).
switch { switch {
case upper == "RET": case upper == "RET":