The suffixed scalar families derived the operand width from the mnemonic alone, so a byte-spelled register under the L spelling or no suffix at all encoded the widened form: XADDL DL, DL emitted 0F C1 where the byte form is 0F C0, CMPL AL, $7 emitted the 32-bit immediate form where the AL form is 3C 07, and CRC32 DL, R11 widened past the F0 byte opcode. operandWidth now reconciles the suffix with the operands: a byte register (AL, DL, R8B, ...) forces the 8-bit form, which is the text the toolchain's own disassembly prints for those encodings, while the W and Q spellings never ride a byte register and are refused as go tool asm refuses them (MOVQ AL, AX). The shift count and the two- and three-operand IMUL forms stay out of the reconciliation, and the byte accumulator short forms now belong to the AL spelling alone, matching the toolchain's division (ADDB $3, AX is 80 c0 03, TESTB $7, AX is f6 c0 07). Assisted-by: GLM 5.3
193 lines
6.7 KiB
Go
193 lines
6.7 KiB
Go
// 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
|
|
}
|