// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import "fmt" // Operand is an instruction operand: a Reg, a Mem reference or an Imm value. type Operand interface { isOperand() } // Imm is an immediate value. Its encoded width is chosen by the instruction // (sign-extended imm8 where possible, otherwise imm32, imm64 for MOV). type Imm int64 func (Imm) isOperand() {} // RegList is a bracketed register range, [Z0-Z3]: the four-register source // of the 4FMAPS and 4VNNIW families. The EVEX emit path carries the list's // low register through the inverted 5-bit V'VVVV field; the three higher // registers are implied by the instruction, so only the pair travels here. type RegList struct { Lo Reg Hi Reg // implied by the encoding; Lo.idx+3 by construction } func (RegList) isOperand() {} // FloatImm is a floating-point immediate ($-1.0). The SSE mnemonics whose // encoding takes an XMM/memory source at that position rewrite it as a read // from a read-only pool constant ($f64. or $f32.), the toolchain's // own behaviour; every other instruction rejects it. type FloatImm struct { Text string // the numeric text as written, sign excluded Neg bool // a leading minus } func (FloatImm) isOperand() {} // TLSMem is a thread-local access, the source form off(base)(TLS*1) with the // base dropped: the toolchain's one-instruction TLS rewrite assembles it as // the segment-prefixed absolute whose disp32 carries an R_TLS_LE patch site // (the linker fills the TLS slot offset). type TLSMem struct { Disp int64 Size int Seg byte // the segment override: FS (0x64) or GS (0x65) on windows } func (TLSMem) isOperand() {} // SegAbs is a segment-absolute access, 0x30(GS): the segment override // prefixes a disp32 absolute reference with no relocation. The base // register spellings GS and FS produce it. type SegAbs struct { Disp int64 Size int Seg byte // 0x64 FS, 0x65 GS } func (SegAbs) isOperand() {} // Mem is a memory operand of the form disp(base)(index*scale). type Mem struct { Base Reg Index Reg Scale int // 1, 2, 4 or 8; 0 means no index Disp int64 Size int // operand width in bytes HasBase bool HasIndex bool Seg byte // segment override prefix (0x64 FS, 0x65 GS); 0 = none } func (Mem) isOperand() {} // Ptr builds a plain displaced memory operand (base)+disp of the given size. func Ptr(base Reg, disp int64, size int) Mem { return Mem{Base: base, Disp: disp, Size: size, HasBase: true} } // Idx builds an indexed memory operand disp(base)(index*scale). func Idx(base, index Reg, scale int, disp int64, size int) Mem { return Mem{Base: base, Index: index, Scale: scale, Disp: disp, Size: size, HasBase: true, HasIndex: true} } // sbMem is a memory operand that references a static (SB) symbol. It encodes // as a RIP-relative reference with a placeholder displacement; the encoder // records a patch site so the file-level layout can fill in the true rel32 // once the symbol's address is known. type sbMem struct { size int name string // static symbol name (the GLOBL identifier) addend int64 // byte offset within the symbol } func (sbMem) isOperand() {} // isX86Mem reports whether the operand is an amd64 memory reference: a base // or indexed Mem, or an SB-relative sbMem. Encoders that gate on "memory in // this position" must accept both; the r/m emitters distinguish the two // themselves. func isX86Mem(o Operand) bool { switch o.(type) { case Mem, sbMem: return true default: return false } } // byteReg reports whether r is a general-purpose register whose name spells // a byte width (AL, CL, DL, BL, AH-DH, SPL-DIL, R8B-R15B): the name itself // fixes an 8-bit access, unlike the size-agnostic spellings (AX, EAX, RAX, // R11) whose width the mnemonic supplies. The vector, opmask, x87, MMX, // control and segment registers never name a byte access. func byteReg(r Reg) bool { return r.size == 1 && !r.isVec() && !r.mask && !r.fp && !r.mmx && r.ctl == 0 && r.seg == 0 } // byteFormBase lists the scalar families that own an 8-bit encoding beside // the 16/32/64-bit ones: the arithmetic and logic group, TEST, the moves, // the unary group, the shifts, the exchanges and atomics, the one-operand // IMUL and CRC32. Families without a byte form (LEA, BT, the bit scans, // PUSH/POP, ADCX/ADOX, BSWAP) stay out, so their width comes from the // mnemonic alone and a byte register in them is the handler's own error to // make. var byteFormBase = map[string]bool{ "MOV": true, "ADD": true, "OR": true, "ADC": true, "SBB": true, "AND": true, "SUB": true, "XOR": true, "CMP": true, "TEST": true, "INC": true, "DEC": true, "NEG": true, "NOT": true, "MUL": true, "DIV": true, "IDIV": true, "SHL": true, "SAL": true, "SHR": true, "SAR": true, "ROL": true, "ROR": true, "RCL": true, "RCR": true, "XCHG": true, "CMPXCHG": true, "XADD": true, "CRC32": true, "IMUL": true, } // operandWidth settles the operand width of a suffixed scalar instruction // against the widths its register operands spell. A byte-spelled register // operand (AL, DL, R8B, ...) forces the 8-bit form whatever the mnemonic's // L suffix or lack of one says: that is the text the toolchain's own // disassembly prints for the byte encodings (the rendered suffix rides the // operand-size attribute, so an L or no suffix at all can name a byte // form), and every register operand joins the form at its low byte, the way // go tool asm reads the classic names there (TESTL R11, DL encodes TESTB // R11B, DL). A W or Q suffix never rides a byte form, so that mix is a // conflict rejected the way the toolchain rejects it (MOVQ AL, AX). With // no byte operand the mnemonic decides alone and 0 returns, keeping the // caller's width: the classic names are size-agnostic at every width. func operandWidth(mnem string, suffix int, ops []Operand) (int, error) { for _, o := range ops { r, ok := o.(Reg) if !ok || !byteReg(r) { continue } if suffix == 2 || suffix == 8 { kind := "quad" if suffix == 2 { kind = "word" } return 0, fmt.Errorf("%s: byte register cannot take the %s form", mnem, kind) } return 1, nil } return 0, nil } // widthOperands selects the operands that carry the instruction's data // width for base: every operand of the scalar families but a shift's count, // which names the CL register without narrowing the shifted value (RCLW CL, // 0(R11) stays 16-bit), and nothing of IMUL's two- and three-operand forms, // which have no byte encoding at all. func widthOperands(base string, ops []Operand) []Operand { switch base { case "SHL", "SAL", "SHR", "SAR", "ROL", "ROR", "RCL", "RCR": if len(ops) > 1 { return ops[1:] } return nil case "IMUL": if len(ops) == 1 { return ops } return nil } return ops }