500 lines
13 KiB
Go
500 lines
13 KiB
Go
// 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 {
|
||
// MOV r, r/m: 0x8A/0x8B, reg=dst, rm=src.
|
||
i := newInstr(size, []byte{movRR(size)})
|
||
if err := setRM(i, dstReg, src, size); err != nil {
|
||
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)
|
||
|
||
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)
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
dstReg, dstIsReg := dst.(Reg)
|
||
srcReg, srcIsReg := src.(Reg)
|
||
switch {
|
||
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")
|
||
}
|
||
mem, ok := src.(Mem)
|
||
if !ok {
|
||
return fmt.Errorf("LEA: source must be a memory operand")
|
||
}
|
||
i := newInstr(size, []byte{0x8D})
|
||
if err := setRM(i, dstReg, mem, size); err != nil {
|
||
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
|
||
}
|
||
}
|