237 lines
7.5 KiB
Go
237 lines
7.5 KiB
Go
// 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
|
||
|
|
}
|