189 lines
5.4 KiB
Go
189 lines
5.4 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package asm is a standalone assembler: it encodes Plan 9 assembly
|
|
// instructions into machine code without the Go toolchain. Phase 2 begins
|
|
// with an amd64 (x86-64) scalar instruction encoder; the encoding is validated
|
|
// by round-tripping through golang.org/x/arch's decoder in the tests.
|
|
package asm
|
|
|
|
import "maps"
|
|
|
|
import "strings"
|
|
|
|
// Reg is an x86-64 register. In Plan 9 assembly the classic names (AX, BX, …)
|
|
// are size-agnostic, the instruction suffix (MOVQ vs MOVL) fixes the width
|
|
// so the encoder keys off the register's index and lets the mnemonic supply the
|
|
// size. The high flag marks the legacy high-byte registers AH/CH/DH/BH, which
|
|
// occupy indices 4-7 yet take no REX prefix, unlike SPL/BPL/SIL/DIL that share
|
|
// those indices but require one. The mask flag marks the AVX-512 opmask
|
|
// registers K0-K7, the fp flag the x87 stack registers F0-F7.
|
|
type Reg struct {
|
|
idx int
|
|
size int // informational width implied by the name; the mnemonic decides
|
|
high bool // AH/CH/DH/BH
|
|
mask bool // K0-K7 opmask register
|
|
fp bool // F0-F7 x87 stack register
|
|
}
|
|
|
|
// Index returns the register number (0-15 for GPRs, 0-31 for vectors).
|
|
func (r Reg) Index() int { return r.idx }
|
|
|
|
// Size returns the width in bytes implied by the register's name.
|
|
func (r Reg) Size() int { return r.size }
|
|
|
|
// IsMask reports whether r is an AVX-512 opmask register (K0-K7).
|
|
func (r Reg) IsMask() bool { return r.mask }
|
|
|
|
func (r Reg) isOperand() {}
|
|
|
|
// needsREX reports whether this register forces a REX prefix at the given
|
|
// operand size: the extended registers R8-R15 always do, and at byte size the
|
|
// low registers SPL/BPL/SIL/DIL (indices 4-7, not high) do as well.
|
|
func (r Reg) needsREX(opSize int) bool {
|
|
if r.idx >= 8 {
|
|
return true
|
|
}
|
|
return opSize == 1 && r.idx >= 4 && !r.high
|
|
}
|
|
|
|
// Register constants (the size is the width the name implies).
|
|
var (
|
|
AL = Reg{idx: 0, size: 1}
|
|
CL = Reg{idx: 1, size: 1}
|
|
DL = Reg{idx: 2, size: 1}
|
|
BL = Reg{idx: 3, size: 1}
|
|
AH = Reg{idx: 4, size: 1, high: true}
|
|
CH = Reg{idx: 5, size: 1, high: true}
|
|
DH = Reg{idx: 6, size: 1, high: true}
|
|
BH = Reg{idx: 7, size: 1, high: true}
|
|
SPL = Reg{idx: 4, size: 1}
|
|
BPL = Reg{idx: 5, size: 1}
|
|
SIL = Reg{idx: 6, size: 1}
|
|
DIL = Reg{idx: 7, size: 1}
|
|
|
|
AX = Reg{idx: 0, size: 2}
|
|
CX = Reg{idx: 1, size: 2}
|
|
DX = Reg{idx: 2, size: 2}
|
|
BX = Reg{idx: 3, size: 2}
|
|
_ = Reg{idx: 4, size: 2}
|
|
_ = Reg{idx: 5, size: 2}
|
|
SI = Reg{idx: 6, size: 2}
|
|
DI = Reg{idx: 7, size: 2}
|
|
|
|
_ = Reg{idx: 0, size: 4}
|
|
_ = Reg{idx: 1, size: 4}
|
|
_ = Reg{idx: 2, size: 4}
|
|
_ = Reg{idx: 3, size: 4}
|
|
_ = Reg{idx: 4, size: 4}
|
|
_ = Reg{idx: 5, size: 4}
|
|
_ = Reg{idx: 6, size: 4}
|
|
_ = Reg{idx: 7, size: 4}
|
|
|
|
_ = Reg{idx: 0, size: 8}
|
|
_ = Reg{idx: 1, size: 8}
|
|
_ = Reg{idx: 2, size: 8}
|
|
_ = Reg{idx: 3, size: 8}
|
|
_ = Reg{idx: 4, size: 8}
|
|
_ = Reg{idx: 5, size: 8}
|
|
_ = Reg{idx: 6, size: 8}
|
|
_ = Reg{idx: 7, size: 8}
|
|
)
|
|
|
|
// regByName maps an assembly register name (case-insensitive) to a Reg.
|
|
var regByName = buildRegByName()
|
|
|
|
func buildRegByName() map[string]Reg {
|
|
m := map[string]Reg{}
|
|
|
|
// 64-bit: RAX..RDI, R8..R15.
|
|
r64 := []string{"RAX", "RCX", "RDX", "RBX", "RSP", "RBP", "RSI", "RDI"}
|
|
for i, n := range r64 {
|
|
m[n] = Reg{idx: i, size: 8}
|
|
}
|
|
for i := 8; i <= 15; i++ {
|
|
m["R"+itoa(i)] = Reg{idx: i, size: 8}
|
|
}
|
|
|
|
// 32-bit: EAX..EDI, R8D..R15D.
|
|
e32 := []string{"EAX", "ECX", "EDX", "EBX", "ESP", "EBP", "ESI", "EDI"}
|
|
for i, n := range e32 {
|
|
m[n] = Reg{idx: i, size: 4}
|
|
}
|
|
for i := 8; i <= 15; i++ {
|
|
m["R"+itoa(i)+"D"] = Reg{idx: i, size: 4}
|
|
}
|
|
|
|
// 16-bit: AX..DI, R8W..R15W.
|
|
w16 := []string{"AX", "CX", "DX", "BX", "SP", "BP", "SI", "DI"}
|
|
for i, n := range w16 {
|
|
m[n] = Reg{idx: i, size: 2}
|
|
}
|
|
for i := 8; i <= 15; i++ {
|
|
m["R"+itoa(i)+"W"] = Reg{idx: i, size: 2}
|
|
}
|
|
|
|
// 8-bit: AL..BH, SPL..DIL, R8B..R15B.
|
|
maps.Copy(m, map[string]Reg{
|
|
"AL": AL, "CL": CL, "DL": DL, "BL": BL,
|
|
"AH": AH, "CH": CH, "DH": DH, "BH": BH,
|
|
"SPL": SPL, "BPL": BPL, "SIL": SIL, "DIL": DIL,
|
|
})
|
|
for i := 8; i <= 15; i++ {
|
|
m["R"+itoa(i)+"B"] = Reg{idx: i, size: 1}
|
|
}
|
|
|
|
// Vector: X0..X31 (128-bit, size 16), Y0..Y31 (256-bit, size 32),
|
|
// Z0..Z31 (512-bit, size 64). Indices 16-31 are only encodable in EVEX
|
|
// (AVX-512) instructions; the encoder validates that through its tables.
|
|
for i := 0; i <= 31; i++ {
|
|
m["X"+itoa(i)] = Reg{idx: i, size: 16}
|
|
m["Y"+itoa(i)] = Reg{idx: i, size: 32}
|
|
m["Z"+itoa(i)] = Reg{idx: i, size: 64}
|
|
}
|
|
// Opmask: K0..K7.
|
|
for i := 0; i <= 7; i++ {
|
|
m["K"+itoa(i)] = Reg{idx: i, size: 8, mask: true}
|
|
}
|
|
// x87 stack: F0..F7.
|
|
for i := 0; i <= 7; i++ {
|
|
m["F"+itoa(i)] = Reg{idx: i, size: 8, fp: true}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// isVec reports whether r is an XMM/YMM/ZMM vector register.
|
|
func (r Reg) isVec() bool { return r.size == 16 || r.size == 32 || r.size == 64 }
|
|
|
|
// vecLenBit returns the vector-length field for a vector register:
|
|
// 0 (128-bit, VEX.L / EVEX.L'L=00), 1 (256-bit) or 2 (512-bit, EVEX only).
|
|
func (r Reg) vecLenBit() int {
|
|
switch r.size {
|
|
case 64:
|
|
return 2
|
|
case 32:
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// ParseReg resolves an assembly register name to a Reg.
|
|
func ParseReg(name string) (Reg, bool) {
|
|
r, ok := regByName[strings.ToUpper(name)]
|
|
return r, ok
|
|
}
|
|
|
|
func itoa(n int) string {
|
|
if n == 0 {
|
|
return "0"
|
|
}
|
|
var buf [3]byte
|
|
i := len(buf)
|
|
for n > 0 {
|
|
i--
|
|
buf[i] = byte('0' + n%10)
|
|
n /= 10
|
|
}
|
|
return string(buf[i:])
|
|
}
|