Files
gasm-sdk/asm/instrs.go
T

678 lines
18 KiB
Go
Raw Normal View History

// 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/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 {
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)
}
// 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
dstReg, dstIsReg := dst.(Reg)
srcReg, srcIsReg := src.(Reg)
switch {
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)
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
}
}
// --- 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)
}
// --- 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)
}