Files
gasm-sdk/asm/operand.go
T

192 lines
6.7 KiB
Go
Raw Permalink 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"
// 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.<hex> or $f32.<hex>), 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
}