diff --git a/asm/amd64_families.go b/asm/amd64_families.go new file mode 100644 index 0000000..9812518 --- /dev/null +++ b/asm/amd64_families.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Petr Balvín (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" +} diff --git a/asm/amd64_x87.go b/asm/amd64_x87.go new file mode 100644 index 0000000..fca6370 --- /dev/null +++ b/asm/amd64_x87.go @@ -0,0 +1,237 @@ +// Copyright (c) 2026 Petr Balvín (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 , 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 +} diff --git a/asm/amd64_x87_test.go b/asm/amd64_x87_test.go new file mode 100644 index 0000000..793b069 --- /dev/null +++ b/asm/amd64_x87_test.go @@ -0,0 +1,110 @@ +// Copyright (c) 2026 Petr Balvín (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) + } + } +} diff --git a/asm/encodable.go b/asm/encodable.go index 31843e4..d73837f 100644 --- a/asm/encodable.go +++ b/asm/encodable.go @@ -16,6 +16,13 @@ import "strings" func Encodable(mnemonic string) bool { 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). switch upper { case "RET", "NOP", "CALL", "JMP", diff --git a/asm/encode.go b/asm/encode.go index f01bd88..da976ba 100644 --- a/asm/encode.go +++ b/asm/encode.go @@ -70,6 +70,13 @@ type encPatch struct { func (e *enc) encode(mnem string, ops []Operand) error { 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). switch { case upper == "RET":