2026-07-06 09:49:50 +02:00
|
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
|
|
package asm
|
|
|
|
|
|
|
|
|
|
|
|
import "fmt"
|
|
|
|
|
|
|
|
|
|
|
|
// aluOp maps an arithmetic/logic mnemonic to its base "r/m, r" opcode (for
|
|
|
|
|
|
// 16/32/64-bit; the 8-bit form is one less) and its /digit for the immediate
|
|
|
|
|
|
// forms (0x80/0x81/0x83).
|
|
|
|
|
|
var aluOp = map[string]struct {
|
|
|
|
|
|
rr byte
|
|
|
|
|
|
digit int
|
|
|
|
|
|
}{
|
|
|
|
|
|
"ADD": {0x01, 0},
|
|
|
|
|
|
"OR": {0x09, 1},
|
|
|
|
|
|
"AND": {0x21, 4},
|
|
|
|
|
|
"SUB": {0x29, 5},
|
|
|
|
|
|
"XOR": {0x31, 6},
|
|
|
|
|
|
"CMP": {0x39, 7},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// unaryOp maps INC/DEC/NEG/NOT to their /digit and base opcode. INC/DEC use
|
|
|
|
|
|
// the 0xFE/0xFF group (the short 0x40–0x4F forms are REX prefixes in 64-bit
|
|
|
|
|
|
// mode); NEG/NOT use the 0xF6/0xF7 group.
|
|
|
|
|
|
var unaryOp = map[string]struct {
|
|
|
|
|
|
digit int
|
|
|
|
|
|
op byte
|
|
|
|
|
|
}{
|
|
|
|
|
|
"INC": {0, 0xFF},
|
|
|
|
|
|
"DEC": {1, 0xFF},
|
|
|
|
|
|
"NOT": {2, 0xF7},
|
|
|
|
|
|
"NEG": {3, 0xF7},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// shiftOp maps SHL/SHR/SAR to their /digit in the 0xC0/0xC1/0xD0–0xD3 group.
|
|
|
|
|
|
var shiftOp = map[string]int{
|
|
|
|
|
|
"SHL": 4,
|
|
|
|
|
|
"SHR": 5,
|
|
|
|
|
|
"SAR": 7,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- MOV --------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
func (e *enc) encodeMov(ops []Operand, size int) error {
|
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
|
return fmt.Errorf("MOV expects 2 operands, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
src, dst := ops[0], ops[1]
|
|
|
|
|
|
|
|
|
|
|
|
dstReg, dstIsReg := dst.(Reg)
|
|
|
|
|
|
switch src := src.(type) {
|
|
|
|
|
|
case Reg:
|
|
|
|
|
|
if dstIsReg {
|
2026-07-08 12:51:35 +02:00
|
|
|
|
// MOV r/m, r: 0x88/0x89, reg=src, rm=dst — the form the Go
|
|
|
|
|
|
// assembler emits for register-to-register moves.
|
|
|
|
|
|
i := newInstr(size, []byte{movRM(size)})
|
|
|
|
|
|
if err := setRM(i, src, dst, size); err != nil {
|
2026-07-06 09:49:50 +02:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
// MOV r/m, r: 0x88/0x89, reg=src, rm=dst(mem).
|
|
|
|
|
|
i := newInstr(size, []byte{movRM(size)})
|
|
|
|
|
|
if err := setRM(i, src, dst, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
|
|
|
|
|
|
case Mem:
|
|
|
|
|
|
if !dstIsReg {
|
|
|
|
|
|
return fmt.Errorf("MOV: two memory operands")
|
|
|
|
|
|
}
|
|
|
|
|
|
// MOV r, r/m: reg=dst, rm=src(mem).
|
|
|
|
|
|
i := newInstr(size, []byte{movRR(size)})
|
|
|
|
|
|
if err := setRM(i, dstReg, src, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
|
2026-07-09 15:56:03 +02:00
|
|
|
|
case sbMem:
|
|
|
|
|
|
if !dstIsReg {
|
|
|
|
|
|
return fmt.Errorf("MOV: two memory operands")
|
|
|
|
|
|
}
|
|
|
|
|
|
// MOV r, r/m: reg=dst, rm=src(static symbol).
|
|
|
|
|
|
i := newInstr(size, []byte{movRR(size)})
|
|
|
|
|
|
if err := setRM(i, dstReg, src, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
|
2026-07-06 09:49:50 +02:00
|
|
|
|
case Imm:
|
|
|
|
|
|
if dstIsReg {
|
|
|
|
|
|
// MOV r, imm: 0xB0+reg (8-bit) / 0xB8+reg (16/32/64, imm64 for Q).
|
|
|
|
|
|
opBase := byte(0xB8)
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
opBase = 0xB0
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{opBase + byte(dstReg.idx&7)})
|
|
|
|
|
|
i.rexB = dstReg.idx >= 8
|
|
|
|
|
|
if dstReg.needsREX(size) {
|
|
|
|
|
|
i.rexForced = true
|
|
|
|
|
|
}
|
|
|
|
|
|
i.imm = immediate(int64(src), size, true)
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
// MOV r/m, imm: 0xC6 (8-bit) / 0xC7 /0.
|
|
|
|
|
|
op := byte(0xC7)
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
op = 0xC6
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{op})
|
|
|
|
|
|
if err := setRMDigit(i, 0, dst, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
i.imm = immediate(int64(src), size, false)
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmt.Errorf("MOV: invalid operands")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func movRR(size int) byte { // MOV r, r/m
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
return 0x8A
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0x8B
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func movRM(size int) byte { // MOV r/m, r
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
return 0x88
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0x89
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- ALU (ADD/OR/AND/SUB/XOR/CMP) -------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
func (e *enc) encodeALU(op struct {
|
|
|
|
|
|
rr byte
|
|
|
|
|
|
digit int
|
|
|
|
|
|
}, ops []Operand, size int) error {
|
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
|
return fmt.Errorf("ALU instruction expects 2 operands, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
src, dst := ops[0], ops[1]
|
|
|
|
|
|
|
|
|
|
|
|
if imm, ok := src.(Imm); ok {
|
|
|
|
|
|
return e.encodeALUImm(op.digit, dst, int64(imm), size)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-07 13:57:53 +02:00
|
|
|
|
// CMP accepts the immediate in the second position too — CMPL CX, $31 is
|
|
|
|
|
|
// the form the Go assembler itself accepts — and encodes it identically
|
|
|
|
|
|
// (CMP r/m, imm sets the flags as first − second). No other ALU op takes
|
|
|
|
|
|
// an immediate destination.
|
|
|
|
|
|
if imm, ok := dst.(Imm); ok {
|
|
|
|
|
|
if op.digit != 7 {
|
|
|
|
|
|
return fmt.Errorf("immediate must be the source operand")
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.encodeALUImm(op.digit, src, int64(imm), size)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 12:51:35 +02:00
|
|
|
|
// CMP records first − second without writing anywhere, so the first
|
|
|
|
|
|
// operand must land as the minuend; every other ALU op writes its second
|
|
|
|
|
|
// operand and follows the forms below.
|
|
|
|
|
|
cmp := op.rr == 0x39
|
2026-07-06 09:49:50 +02:00
|
|
|
|
dstReg, dstIsReg := dst.(Reg)
|
|
|
|
|
|
srcReg, srcIsReg := src.(Reg)
|
|
|
|
|
|
switch {
|
2026-07-08 12:51:35 +02:00
|
|
|
|
case cmp && dstIsReg:
|
|
|
|
|
|
// CMP x, reg: OP r/m, r (0x38/0x39) with rm = first operand, reg =
|
|
|
|
|
|
// second, matching the Go assembler.
|
|
|
|
|
|
opc := op.rr
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
opc = op.rr - 1
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{opc})
|
|
|
|
|
|
if err := setRM(i, dstReg, src, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
case cmp && srcIsReg:
|
|
|
|
|
|
// CMP reg, mem: OP r, r/m (0x3A/0x3B) with reg = first operand, rm =
|
|
|
|
|
|
// second.
|
|
|
|
|
|
opc := op.rr + 2
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
opc = op.rr + 1
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{opc})
|
|
|
|
|
|
if err := setRM(i, srcReg, dst, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
2026-07-06 09:49:50 +02:00
|
|
|
|
case srcIsReg:
|
|
|
|
|
|
// OP r/m, r: reg=src, rm=dst (dst is a register or memory). This is the
|
|
|
|
|
|
// form the Go assembler prefers when the source is a register.
|
|
|
|
|
|
opc := op.rr
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
opc = op.rr - 1
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{opc})
|
|
|
|
|
|
if err := setRM(i, srcReg, dst, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
case dstIsReg:
|
|
|
|
|
|
// OP r, r/m: reg=dst, rm=src(memory).
|
|
|
|
|
|
opc := op.rr + 2
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
opc = op.rr + 1
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{opc})
|
|
|
|
|
|
if err := setRM(i, dstReg, src, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmt.Errorf("two memory operands")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (e *enc) encodeALUImm(digit int, dst Operand, imm int64, size int) error {
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
i := newInstr(1, []byte{0x80})
|
|
|
|
|
|
if err := setRMDigit(i, digit, dst, 1); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
i.imm = []byte{byte(int8(imm))}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
if fits8(imm) {
|
|
|
|
|
|
// 0x83 /digit, sign-extended imm8.
|
|
|
|
|
|
i := newInstr(size, []byte{0x83})
|
|
|
|
|
|
if err := setRMDigit(i, digit, dst, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
i.imm = []byte{byte(int8(imm))}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 0x81 /digit, imm16/imm32.
|
|
|
|
|
|
i := newInstr(size, []byte{0x81})
|
|
|
|
|
|
if err := setRMDigit(i, digit, dst, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
i.imm = immediate(imm, size, false)
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- TEST -------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
func (e *enc) encodeTest(ops []Operand, size int) error {
|
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
|
return fmt.Errorf("TEST expects 2 operands, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
src, dst := ops[0], ops[1]
|
|
|
|
|
|
if imm, ok := src.(Imm); ok {
|
|
|
|
|
|
// TEST r/m, imm: 0xF6 (8-bit) / 0xF7 /0.
|
|
|
|
|
|
op := byte(0xF7)
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
op = 0xF6
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{op})
|
|
|
|
|
|
if err := setRMDigit(i, 0, dst, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
i.imm = immediate(int64(imm), size, false)
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
srcReg, ok := src.(Reg)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("TEST: source must be a register or immediate")
|
|
|
|
|
|
}
|
|
|
|
|
|
// TEST r/m, r: 0x84 (8-bit) / 0x85.
|
|
|
|
|
|
op := byte(0x85)
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
op = 0x84
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{op})
|
|
|
|
|
|
if err := setRM(i, srcReg, dst, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- LEA --------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
func (e *enc) encodeLea(ops []Operand, size int) error {
|
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
|
return fmt.Errorf("LEA expects 2 operands, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
src, dst := ops[0], ops[1] // LEAQ addr, reg
|
|
|
|
|
|
dstReg, ok := dst.(Reg)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("LEA: destination must be a register")
|
|
|
|
|
|
}
|
2026-07-09 15:56:03 +02:00
|
|
|
|
switch src.(type) {
|
|
|
|
|
|
case Mem, sbMem:
|
|
|
|
|
|
default:
|
2026-07-06 09:49:50 +02:00
|
|
|
|
return fmt.Errorf("LEA: source must be a memory operand")
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{0x8D})
|
2026-07-09 15:56:03 +02:00
|
|
|
|
if err := setRM(i, dstReg, src, size); err != nil {
|
2026-07-06 09:49:50 +02:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- INC/DEC/NEG/NOT --------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
func (e *enc) encodeUnary(op struct {
|
|
|
|
|
|
digit int
|
|
|
|
|
|
op byte
|
|
|
|
|
|
}, ops []Operand, size int) error {
|
|
|
|
|
|
if len(ops) != 1 {
|
|
|
|
|
|
return fmt.Errorf("unary instruction expects 1 operand, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
base := op.op
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
base-- // 0xFF→0xFE, 0xF7→0xF6
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{base})
|
|
|
|
|
|
if err := setRMDigit(i, op.digit, ops[0], size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- SHL/SHR/SAR ------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
func (e *enc) encodeShift(digit int, ops []Operand, size int) error {
|
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
|
return fmt.Errorf("shift expects 2 operands, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
count, dst := ops[0], ops[1]
|
|
|
|
|
|
// Count is $1, %CL, or an imm8.
|
|
|
|
|
|
if reg, ok := count.(Reg); ok && reg.idx == 1 && reg.size <= 1 {
|
|
|
|
|
|
// CL: 0xD2 (8-bit) / 0xD3.
|
|
|
|
|
|
op := byte(0xD3)
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
op = 0xD2
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{op})
|
|
|
|
|
|
if err := setRMDigit(i, digit, dst, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
imm, ok := count.(Imm)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("shift count must be $1, CL or an immediate")
|
|
|
|
|
|
}
|
|
|
|
|
|
if imm == 1 {
|
|
|
|
|
|
// 0xD0 (8-bit) / 0xD1.
|
|
|
|
|
|
op := byte(0xD1)
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
op = 0xD0
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{op})
|
|
|
|
|
|
if err := setRMDigit(i, digit, dst, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 0xC0 (8-bit) / 0xC1, imm8.
|
|
|
|
|
|
op := byte(0xC1)
|
|
|
|
|
|
if size == 1 {
|
|
|
|
|
|
op = 0xC0
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{op})
|
|
|
|
|
|
if err := setRMDigit(i, digit, dst, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
i.imm = []byte{byte(int8(imm))}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- IMUL -------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
func (e *enc) encodeImul(ops []Operand, size int) error {
|
|
|
|
|
|
switch len(ops) {
|
|
|
|
|
|
case 2:
|
|
|
|
|
|
// IMUL r, r/m: 0x0F 0xAF.
|
|
|
|
|
|
dstReg, ok := ops[1].(Reg)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("IMUL: destination must be a register")
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{0x0F, 0xAF})
|
|
|
|
|
|
if err := setRM(i, dstReg, ops[0], size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
case 3:
|
|
|
|
|
|
// IMUL r, r/m, imm: 0x6B (imm8) / 0x69 (imm16/32).
|
|
|
|
|
|
dstReg, ok := ops[2].(Reg)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("IMUL: destination must be a register")
|
|
|
|
|
|
}
|
|
|
|
|
|
imm, ok := ops[0].(Imm)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("IMUL: immediate operand expected first")
|
|
|
|
|
|
}
|
|
|
|
|
|
// Plan 9 order: IMUL $imm, src, dst.
|
|
|
|
|
|
if fits8(int64(imm)) {
|
|
|
|
|
|
i := newInstr(size, []byte{0x6B})
|
|
|
|
|
|
if err := setRM(i, dstReg, ops[1], size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
i.imm = []byte{byte(int8(imm))}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{0x69})
|
|
|
|
|
|
if err := setRM(i, dstReg, ops[1], size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
i.imm = immediate(int64(imm), size, false)
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmt.Errorf("IMUL expects 2 or 3 operands, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- PUSH / POP -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
func (e *enc) encodePushPop(ops []Operand, push bool) error {
|
|
|
|
|
|
if len(ops) != 1 {
|
|
|
|
|
|
return fmt.Errorf("PUSH/POP expects 1 operand, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
switch op := ops[0].(type) {
|
|
|
|
|
|
case Reg:
|
|
|
|
|
|
base := byte(0x50) // PUSH r; POP is 0x58
|
|
|
|
|
|
if !push {
|
|
|
|
|
|
base = 0x58
|
|
|
|
|
|
}
|
|
|
|
|
|
// PUSH/POP default to 64-bit in 64-bit mode; no REX.W needed.
|
|
|
|
|
|
i := &instr{opcode: []byte{base + byte(op.idx&7)}, modrm: -1, sib: -1}
|
|
|
|
|
|
i.rexB = op.idx >= 8
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
case Mem:
|
|
|
|
|
|
opc := byte(0xFF) // PUSH r/m: /6
|
|
|
|
|
|
digit := 6
|
|
|
|
|
|
if !push {
|
|
|
|
|
|
opc = 0x8F // POP r/m: /0
|
|
|
|
|
|
digit = 0
|
|
|
|
|
|
}
|
|
|
|
|
|
i := &instr{opcode: []byte{opc}, modrm: -1, sib: -1}
|
|
|
|
|
|
if err := setRMDigit(i, digit, ops[0], 8); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
case Imm:
|
|
|
|
|
|
if !push {
|
|
|
|
|
|
return fmt.Errorf("POP does not take an immediate")
|
|
|
|
|
|
}
|
|
|
|
|
|
if fits8(int64(op)) {
|
|
|
|
|
|
i := &instr{opcode: []byte{0x6A}, modrm: -1, sib: -1, imm: []byte{byte(int8(op))}}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
i := &instr{opSize16: false, opcode: []byte{0x68}, modrm: -1, sib: -1, imm: le32(int64(op))}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmt.Errorf("PUSH/POP: invalid operand")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- RET / JMP / CALL / Jcc -------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
func (e *enc) encodeRet() error {
|
|
|
|
|
|
return e.emit(&instr{opcode: []byte{0xC3}, modrm: -1, sib: -1})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// encodeJmpRel encodes JMP/CALL with a relative displacement (the operand is an
|
|
|
|
|
|
// Imm holding the already-computed rel32 offset).
|
|
|
|
|
|
func (e *enc) encodeJmpRel(ops []Operand, opcode []byte) error {
|
|
|
|
|
|
if len(ops) != 1 {
|
|
|
|
|
|
return fmt.Errorf("JMP/CALL expects 1 operand, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
imm, ok := ops[0].(Imm)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("JMP/CALL: relative offset must be an immediate (labels are resolved by the assembler)")
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(&instr{opcode: opcode, modrm: -1, sib: -1, imm: le32(int64(imm))})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// condCode maps a Plan 9 conditional-jump mnemonic to its x86 condition code.
|
|
|
|
|
|
func condCode(upper string) (int, bool) {
|
|
|
|
|
|
if len(upper) < 2 || upper[0] != 'J' || upper == "JMP" {
|
|
|
|
|
|
return 0, false
|
|
|
|
|
|
}
|
|
|
|
|
|
cc, ok := jccMap[upper[1:]]
|
|
|
|
|
|
return cc, ok
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var jccMap = map[string]int{
|
|
|
|
|
|
"O": 0x0, "NO": 0x1, "OS": 0x0, "OC": 0x1,
|
|
|
|
|
|
"B": 0x2, "C": 0x2, "NAE": 0x2, "CS": 0x2,
|
|
|
|
|
|
"NB": 0x3, "NC": 0x3, "AE": 0x3, "CC": 0x3,
|
|
|
|
|
|
"E": 0x4, "Z": 0x4, "EQ": 0x4,
|
|
|
|
|
|
"NE": 0x5, "NZ": 0x5,
|
|
|
|
|
|
"BE": 0x6, "NA": 0x6, "LS": 0x6,
|
|
|
|
|
|
"NBE": 0x7, "A": 0x7, "HI": 0x7,
|
|
|
|
|
|
"S": 0x8, "MI": 0x8,
|
|
|
|
|
|
"NS": 0x9, "PL": 0x9,
|
|
|
|
|
|
"P": 0xA, "PE": 0xA, "PS": 0xA,
|
|
|
|
|
|
"NP": 0xB, "PO": 0xB, "PC": 0xB,
|
|
|
|
|
|
"L": 0xC, "NGE": 0xC, "LT": 0xC,
|
|
|
|
|
|
"NL": 0xD, "GE": 0xD,
|
|
|
|
|
|
"LE": 0xE, "NG": 0xE,
|
|
|
|
|
|
"NLE": 0xF, "G": 0xF, "GT": 0xF,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (e *enc) encodeJcc(cc int, ops []Operand) error {
|
|
|
|
|
|
if len(ops) != 1 {
|
|
|
|
|
|
return fmt.Errorf("conditional jump expects 1 operand, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
imm, ok := ops[0].(Imm)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("conditional jump: relative offset must be an immediate")
|
|
|
|
|
|
}
|
|
|
|
|
|
if fits8(int64(imm)) {
|
|
|
|
|
|
// Short form: 0x70+cc, rel8.
|
|
|
|
|
|
return e.emit(&instr{opcode: []byte{0x70 + byte(cc)}, modrm: -1, sib: -1, imm: []byte{byte(int8(imm))}})
|
|
|
|
|
|
}
|
|
|
|
|
|
// Near form: 0x0F 0x80+cc, rel32.
|
|
|
|
|
|
return e.emit(&instr{opcode: []byte{0x0F, 0x80 + byte(cc)}, modrm: -1, sib: -1, imm: le32(int64(imm))})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// immediate encodes an immediate of the given operand size. full64 selects the
|
|
|
|
|
|
// 64-bit immediate form (only valid for MOV r64, imm64); otherwise a 32-bit
|
|
|
|
|
|
// sign-extended immediate is used for 64-bit operands.
|
|
|
|
|
|
func immediate(v int64, size int, full64 bool) []byte {
|
|
|
|
|
|
switch size {
|
|
|
|
|
|
case 1:
|
|
|
|
|
|
return []byte{byte(int8(v))}
|
|
|
|
|
|
case 2:
|
|
|
|
|
|
return le16(v)
|
|
|
|
|
|
case 4:
|
|
|
|
|
|
return le32(v)
|
|
|
|
|
|
default: // 8
|
|
|
|
|
|
if full64 {
|
|
|
|
|
|
return le64(v)
|
|
|
|
|
|
}
|
|
|
|
|
|
return le32(v) // sign-extended imm32
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-08 12:51:35 +02:00
|
|
|
|
|
|
|
|
|
|
// --- CMOVcc / SETcc ---------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// encodeCmov encodes a conditional move: CMOV + size (W/L/Q) + condition
|
|
|
|
|
|
// (CMOVLGT, CMOVQEQ, …). The condition reads exactly like the Jcc spellings;
|
|
|
|
|
|
// the instruction is 0F 40+cc with reg = dst, rm = src.
|
|
|
|
|
|
func (e *enc) encodeCmov(upper string, ops []Operand) error {
|
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
|
return fmt.Errorf("CMOVcc expects 2 operands, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
rest := upper[len("CMOV"):]
|
|
|
|
|
|
if len(rest) < 2 {
|
|
|
|
|
|
return fmt.Errorf("unsupported instruction %q", upper)
|
|
|
|
|
|
}
|
|
|
|
|
|
var size int
|
|
|
|
|
|
switch rest[0] {
|
|
|
|
|
|
case 'W':
|
|
|
|
|
|
size = 2
|
|
|
|
|
|
case 'L':
|
|
|
|
|
|
size = 4
|
|
|
|
|
|
case 'Q':
|
|
|
|
|
|
size = 8
|
|
|
|
|
|
default:
|
|
|
|
|
|
return fmt.Errorf("unsupported instruction %q", upper)
|
|
|
|
|
|
}
|
|
|
|
|
|
cc, ok := jccMap[rest[1:]]
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("unsupported instruction %q", upper)
|
|
|
|
|
|
}
|
|
|
|
|
|
src, dst := ops[0], ops[1]
|
|
|
|
|
|
dstReg, ok := dst.(Reg)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("CMOVcc destination must be a register")
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{0x0F, byte(0x40 + cc)})
|
|
|
|
|
|
if err := setRM(i, dstReg, src, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// encodeSet encodes a conditional byte set: SET + condition (SETNE, SETEQ, …),
|
|
|
|
|
|
// always a byte write — 0F 90+cc /0 into a register or memory operand.
|
|
|
|
|
|
func (e *enc) encodeSet(upper string, ops []Operand) error {
|
|
|
|
|
|
if len(ops) != 1 {
|
|
|
|
|
|
return fmt.Errorf("SETcc expects 1 operand, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
cond := upper[len("SET"):]
|
|
|
|
|
|
cc, ok := jccMap[cond]
|
|
|
|
|
|
if !ok || cond == "" {
|
|
|
|
|
|
return fmt.Errorf("unsupported instruction %q", upper)
|
|
|
|
|
|
}
|
|
|
|
|
|
i := &instr{opcode: []byte{0x0F, byte(0x90 + cc)}, modrm: -1, sib: -1}
|
|
|
|
|
|
if err := setRMDigit(i, 0, ops[0], 1); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- LZCNT / TZCNT ----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// encodeCount encodes LZCNT/TZCNT (leading / trailing zero count): F3 0F BD
|
|
|
|
|
|
// or F3 0F BC, with reg = dst and rm = src. The size suffix selects the
|
|
|
|
|
|
// operand width (LZCNTW/LZCNTL/LZCNTQ).
|
|
|
|
|
|
func (e *enc) encodeCount(base string, ops []Operand, size int) error {
|
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
|
return fmt.Errorf("%s expects 2 operands, got %d", base, len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
op := byte(0xBD)
|
|
|
|
|
|
if base == "TZCNT" {
|
|
|
|
|
|
op = 0xBC
|
|
|
|
|
|
}
|
|
|
|
|
|
dstReg, ok := ops[1].(Reg)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("%s destination must be a register", base)
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{0x0F, op})
|
|
|
|
|
|
i.prefix = 0xF3
|
|
|
|
|
|
if err := setRM(i, dstReg, ops[0], size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- mixed-width sign/zero-extending moves -----------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// movExtendOp maps Go's mixed-width move names to their opcode and destination
|
|
|
|
|
|
// width. The source is narrower than the destination, so the plain size-suffix
|
|
|
|
|
|
// convention does not apply to these names.
|
|
|
|
|
|
var movExtendOp = map[string]struct {
|
|
|
|
|
|
op []byte
|
|
|
|
|
|
dst64 bool
|
|
|
|
|
|
}{
|
|
|
|
|
|
"MOVBLZX": {[]byte{0x0F, 0xB6}, false}, // byte → long, zero-extend
|
|
|
|
|
|
"MOVBQZX": {[]byte{0x0F, 0xB6}, true}, // byte → quad, zero-extend
|
|
|
|
|
|
"MOVWLZX": {[]byte{0x0F, 0xB7}, false}, // word → long, zero-extend
|
|
|
|
|
|
"MOVWQZX": {[]byte{0x0F, 0xB7}, true}, // word → quad, zero-extend
|
|
|
|
|
|
"MOVWLSX": {[]byte{0x0F, 0xBF}, false}, // word → long, sign-extend
|
|
|
|
|
|
"MOVLQSX": {[]byte{0x63}, true}, // long → quad, sign-extend (MOVSXD)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// encodeMovExtend encodes a mixed-width extending move: reg = dst (the wider
|
|
|
|
|
|
// operand), rm = src.
|
|
|
|
|
|
func (e *enc) encodeMovExtend(base string, ops []Operand) error {
|
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
|
return fmt.Errorf("%s expects 2 operands, got %d", base, len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
spec := movExtendOp[base]
|
|
|
|
|
|
dstReg, ok := ops[1].(Reg)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return fmt.Errorf("%s destination must be a register", base)
|
|
|
|
|
|
}
|
|
|
|
|
|
size := 4
|
|
|
|
|
|
if spec.dst64 {
|
|
|
|
|
|
size = 8
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, spec.op)
|
|
|
|
|
|
if err := setRM(i, dstReg, ops[0], size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-11 17:36:52 +02:00
|
|
|
|
// --- legacy SSE moves --------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// sseMove describes a legacy (non-VEX) SSE move: a mandatory prefix plus a
|
|
|
|
|
|
// load opcode (reg = destination, rm = source) and a store opcode (the
|
|
|
|
|
|
// reverse). The Plan 9 names MOVOU/MOVO are the integer unaligned/aligned
|
|
|
|
|
|
// octa moves (MOVDQU/MOVDQA), not the packed-single ones.
|
|
|
|
|
|
type sseMove struct {
|
|
|
|
|
|
prefix byte // 0, 0x66, 0xF2 or 0xF3
|
|
|
|
|
|
load byte
|
|
|
|
|
|
store byte
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var sseMoveTable = map[string]sseMove{
|
|
|
|
|
|
"MOVOU": {0xF3, 0x6F, 0x7F}, // MOVDQU — unaligned octa
|
|
|
|
|
|
"MOVO": {0x66, 0x6F, 0x7F}, // MOVDQA — aligned octa
|
|
|
|
|
|
"MOVUPS": {0x00, 0x10, 0x11}, // unaligned packed single
|
|
|
|
|
|
"MOVAPS": {0x00, 0x28, 0x29}, // aligned packed single
|
|
|
|
|
|
"MOVUPD": {0x66, 0x10, 0x11}, // unaligned packed double
|
|
|
|
|
|
"MOVAPD": {0x66, 0x28, 0x29}, // aligned packed double
|
|
|
|
|
|
"MOVSD": {0xF2, 0x10, 0x11}, // scalar double
|
|
|
|
|
|
"MOVSS": {0xF3, 0x10, 0x11}, // scalar single
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// encodeSSEMove encodes a legacy SSE move: a vector-to-vector move uses the
|
|
|
|
|
|
// load form (reg = destination), matching the Go assembler.
|
|
|
|
|
|
func (e *enc) encodeSSEMove(m sseMove, ops []Operand) error {
|
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
|
return fmt.Errorf("SSE move expects 2 operands, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
src, dst := ops[0], ops[1]
|
|
|
|
|
|
srcReg, srcVec := vecReg(src)
|
|
|
|
|
|
dstReg, dstVec := vecReg(dst)
|
|
|
|
|
|
op := m.store
|
|
|
|
|
|
var reg Reg
|
|
|
|
|
|
var rm Operand
|
|
|
|
|
|
switch {
|
|
|
|
|
|
case srcVec && dstVec:
|
|
|
|
|
|
op = m.load
|
|
|
|
|
|
reg, rm = dstReg, src
|
|
|
|
|
|
case srcVec:
|
|
|
|
|
|
if _, ok := dst.(Mem); !ok {
|
|
|
|
|
|
return fmt.Errorf("SSE move: invalid destination operand")
|
|
|
|
|
|
}
|
|
|
|
|
|
reg, rm = srcReg, dst
|
|
|
|
|
|
case dstVec:
|
|
|
|
|
|
if _, ok := src.(Mem); !ok {
|
|
|
|
|
|
return fmt.Errorf("SSE move: invalid source operand")
|
|
|
|
|
|
}
|
|
|
|
|
|
op = m.load
|
|
|
|
|
|
reg, rm = dstReg, src
|
|
|
|
|
|
default:
|
|
|
|
|
|
return fmt.Errorf("SSE move needs a vector register operand")
|
|
|
|
|
|
}
|
|
|
|
|
|
i := &instr{prefix: m.prefix, opcode: []byte{0x0F, op}, modrm: -1, sib: -1}
|
|
|
|
|
|
if err := setRM(i, reg, rm, 8); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 12:51:35 +02:00
|
|
|
|
// --- CVTSL2SD / CVTSQ2SD -----------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// encodeCvtsi2sd encodes a signed integer to scalar double conversion
|
|
|
|
|
|
// (CVTSL2SD from a 32-bit, CVTSQ2SD from a 64-bit source): F2 0F 2A with
|
|
|
|
|
|
// reg = XMM dst, rm = GPR/memory src. The Go assembler emits the legacy SSE
|
|
|
|
|
|
// encoding here, not the VEX form, so we match it byte for byte.
|
|
|
|
|
|
func (e *enc) encodeCvtsi2sd(quad bool, ops []Operand) error {
|
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
|
return fmt.Errorf("CVTSx2SD expects 2 operands, got %d", len(ops))
|
|
|
|
|
|
}
|
|
|
|
|
|
src, dst := ops[0], ops[1]
|
|
|
|
|
|
dstReg, ok := dst.(Reg)
|
|
|
|
|
|
if !ok || !dstReg.isVec() {
|
|
|
|
|
|
return fmt.Errorf("CVTSx2SD destination must be a vector register")
|
|
|
|
|
|
}
|
|
|
|
|
|
size := 4
|
|
|
|
|
|
if quad {
|
|
|
|
|
|
size = 8
|
|
|
|
|
|
}
|
|
|
|
|
|
i := newInstr(size, []byte{0x0F, 0x2A})
|
|
|
|
|
|
i.prefix = 0xF2
|
|
|
|
|
|
if err := setRM(i, dstReg, src, size); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return e.emit(i)
|
|
|
|
|
|
}
|