// 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 }