feat: gasm-devkit 0.1.0 — GAsm lexer, parser, linter, formatter, LSP and amd64 assembler

Assisted-by: Qwen 3.8 Max Preview
This commit is contained in:
2026-07-06 09:49:50 +02:00
commit d5a4a6de45
53 changed files with 13166 additions and 0 deletions
+292
View File
@@ -0,0 +1,292 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"fmt"
"strings"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
)
// Assemble encodes the body of a TEXT function into x86-64 machine code,
// resolving local labels to relative jump offsets and translating the FP/SP
// pseudo-registers onto the hardware stack pointer (matching the Go
// assembler's default frame-pointer behaviour). Jumps always use the 32-bit
// relative form so instruction sizes are fixed and offsets resolve in a single
// layout pass.
//
// Supported operands: registers, memory (real base register), immediates,
// FP/SP frame-relative operands, and local-label jumps. SB (global symbol)
// operands require relocations and are not yet supported; SIMD (VEX/EVEX)
// instructions are pending.
func Assemble(t *ast.Text) ([]byte, map[string]int, error) {
fi := computeFrame(t)
// Pass 1: lay out instructions (including prologue/epilogue) to fix label
// offsets.
offsets := map[string]int{}
sizes := make([]int, len(t.Body))
pos := len(fi.prologue)
for i, stmt := range t.Body {
switch s := stmt.(type) {
case *ast.Label:
offsets[s.Name.Text] = pos
case *ast.Instr:
sz, err := instrSize(s, fi)
if err != nil {
return nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
}
sizes[i] = sz
pos += sz
}
}
// Pass 2: emit.
out := append([]byte(nil), fi.prologue...)
pos = len(fi.prologue)
for i, stmt := range t.Body {
s, ok := stmt.(*ast.Instr)
if !ok {
continue
}
code, err := encodeInstr(s, pos, offsets, fi)
if err != nil {
return nil, nil, fmt.Errorf("%s: %w", s.Mnemonic.Text, err)
}
if len(code) != sizes[i] {
return nil, nil, fmt.Errorf("%s: size mismatch (%d vs %d)", s.Mnemonic.Text, len(code), sizes[i])
}
out = append(out, code...)
pos += len(code)
}
return out, offsets, nil
}
// frameInfo carries the frame layout derived from the TEXT directive.
type frameInfo struct {
size int // local frame size ($framesize)
useFP bool // a frame pointer (BP) is set up
fpAdjust int64 // added to x+N(FP) to reach the hardware SP-relative offset
spAdjust int64 // x-N(SP) becomes (spAdjust - N)(SP)
prologue []byte
epilogue []byte
}
// computeFrame derives the frame layout, matching the Go assembler's default
// (a frame pointer is used whenever the function has a non-zero frame).
func computeFrame(t *ast.Text) frameInfo {
fi := frameInfo{}
if t.Frame != nil && t.Frame.Imm.HasVal {
fi.size = int(t.Frame.Imm.Val)
}
if fi.size > 0 {
fi.useFP = true
fi.fpAdjust = int64(fi.size) + 16 // frame + saved BP + return address
fi.spAdjust = int64(fi.size)
fi.prologue = prologueBytes(fi.size)
fi.epilogue = epilogueBytes(fi.size)
} else {
fi.fpAdjust = 8 // return address only
}
return fi
}
// prologueBytes emits: PUSHQ BP; MOVQ SP, BP; SUBQ $size, SP.
func prologueBytes(size int) []byte {
out := []byte{0x55, 0x48, 0x89, 0xE5} // PUSHQ BP; MOVQ SP, BP
return append(out, subSP(size)...)
}
// epilogueBytes emits: ADDQ $size, SP; POPQ BP.
func epilogueBytes(size int) []byte {
out := addSP(size)
return append(out, 0x5D) // POPQ BP
}
func subSP(size int) []byte { // SUBQ $size, SP
if size >= -128 && size <= 127 {
return []byte{0x48, 0x83, 0xEC, byte(int8(size))}
}
return append([]byte{0x48, 0x81, 0xEC}, le32(int64(size))...)
}
func addSP(size int) []byte { // ADDQ $size, SP
if size >= -128 && size <= 127 {
return []byte{0x48, 0x83, 0xC4, byte(int8(size))}
}
return append([]byte{0x48, 0x81, 0xC4}, le32(int64(size))...)
}
// instrSize returns the encoded length of an instruction (pass 1). encodeInstr
// already includes the epilogue for a RET in a frame-pointer function; jumps use
// a fixed rel32 size (no epilogue).
func instrSize(s *ast.Instr, fi frameInfo) (int, error) {
mnem := strings.ToUpper(s.Mnemonic.Text)
if isJumpMnemonic(mnem) {
return jumpSize(mnem), nil
}
code, err := encodeInstr(s, 0, nil, fi)
if err != nil {
return 0, err
}
return len(code), nil
}
func isJumpMnemonic(mnem string) bool {
if mnem == "JMP" || mnem == "CALL" {
return true
}
_, ok := condCode(mnem)
return ok
}
// jumpSize returns the fixed length of a rel32 jump instruction.
func jumpSize(mnem string) int {
if mnem == "JMP" || mnem == "CALL" {
return 5 // opcode + rel32
}
return 6 // 0x0F 0x8x + rel32
}
// encodeInstr encodes one instruction, resolving jump targets against offsets
// (relative to pc, the instruction's own offset). A RET in a frame-pointer
// function is prefixed with the epilogue.
func encodeInstr(s *ast.Instr, pc int, offsets map[string]int, fi frameInfo) ([]byte, error) {
mnem := strings.ToUpper(s.Mnemonic.Text)
var prefix []byte
if mnem == "RET" && fi.useFP {
prefix = fi.epilogue
}
var code []byte
var err error
if isJumpMnemonic(mnem) {
code, err = encodeJump(s, mnem, pc+len(prefix), offsets)
} else {
code, err = encodeNormal(s, fi)
}
if err != nil {
return nil, err
}
return append(prefix, code...), nil
}
func encodeNormal(s *ast.Instr, fi frameInfo) ([]byte, error) {
_, size := splitSize(strings.ToUpper(s.Mnemonic.Text))
if size == 0 {
size = 8
}
ops := make([]Operand, len(s.Operands))
for i, op := range s.Operands {
o, err := operandFromAST(op, size, fi)
if err != nil {
return nil, err
}
ops[i] = o
}
return Encode(s.Mnemonic.Text, ops...)
}
// encodeJump encodes a JMP/CALL/Jcc with a rel32 offset resolved from the
// target label.
func encodeJump(s *ast.Instr, mnem string, pc int, offsets map[string]int) ([]byte, error) {
if len(s.Operands) != 1 {
return nil, fmt.Errorf("jump expects 1 operand, got %d", len(s.Operands))
}
name, ok := labelName(s.Operands[0])
if !ok {
return nil, fmt.Errorf("jump target must be a local label")
}
target, ok := offsets[name]
if !ok {
return nil, fmt.Errorf("undefined label %q", name)
}
rel := int64(target - (pc + jumpSize(mnem)))
switch mnem {
case "JMP":
return append([]byte{0xE9}, le32(rel)...), nil
case "CALL":
return append([]byte{0xE8}, le32(rel)...), nil
default:
cc, _ := condCode(mnem)
return append([]byte{0x0F, 0x80 + byte(cc)}, le32(rel)...), nil
}
}
// labelName extracts a local-label name from a jump operand.
func labelName(op *ast.Operand) (string, bool) {
if op.Kind == ast.OpAddr && op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "" &&
op.Addr.Base == "" && op.Addr.Sym.Name != "" {
return op.Addr.Sym.Name, true
}
return "", false
}
// spReg is the hardware stack pointer used to realise FP/SP pseudo-operands.
var spReg = Reg{idx: 4, size: 8}
// operandFromAST converts a parsed operand into an encoder Operand, applying
// the frame translation to FP/SP pseudo-register operands.
func operandFromAST(op *ast.Operand, size int, fi frameInfo) (Operand, error) {
switch op.Kind {
case ast.OpImmediate:
if op.Imm.HasVal {
v := op.Imm.Val
if op.Imm.Neg {
v = -v
}
return Imm(v), nil
}
return nil, fmt.Errorf("non-integer immediate not supported")
case ast.OpAddr:
a := op.Addr
// FP-relative: x+N(FP) → (N + fpAdjust)(SP). The offset N lives in the
// symbol, not the address displacement.
if a.Sym != nil && a.Sym.Pseudo == "FP" {
off := a.Sym.Offset + fi.fpAdjust
return Mem{Base: spReg, Disp: off, HasBase: true, Size: size}, nil
}
// SP-relative local: x-N(SP) → (spAdjust + offset)(SP).
if a.Sym != nil && a.Sym.Pseudo == "SP" && a.Base == "" {
off := fi.spAdjust + a.Sym.Offset
return Mem{Base: spReg, Disp: off, HasBase: true, Size: size}, nil
}
// SB (global symbol) needs a relocation — not yet supported.
if a.Sym != nil && a.Sym.Pseudo == "SB" {
return nil, fmt.Errorf("SB (global symbol) operands need relocation support (pending)")
}
// Memory with a real base register: (base), off(base), (base)(index*scale).
if a.Base != "" {
base, ok := ParseReg(a.Base)
if !ok {
return nil, fmt.Errorf("unknown base register %q", a.Base)
}
m := Mem{Base: base, Disp: a.Offset, HasBase: true, Size: size}
if a.Index != "" {
idx, ok := ParseReg(a.Index)
if !ok {
return nil, fmt.Errorf("unknown index register %q", a.Index)
}
m.Index = idx
m.Scale = a.Scale
m.HasIndex = true
}
return m, nil
}
// Bare register.
if a.Sym != nil && a.Sym.Pseudo == "" && a.Sym.Name != "" {
if r, ok := ParseReg(a.Sym.Name); ok {
return r, nil
}
}
return nil, fmt.Errorf("operand form not yet supported")
}
return nil, fmt.Errorf("unsupported operand")
}
+201
View File
@@ -0,0 +1,201 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"strings"
"testing"
"golang.org/x/arch/x86/x86asm"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
)
// firstText parses src and returns its first TEXT function.
func firstText(t *testing.T, src string) *ast.Text {
t.Helper()
f, errs := parser.Parse("f_amd64.s", src)
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
for _, d := range f.Decls {
if txt, ok := d.(*ast.Text); ok {
return txt
}
}
t.Fatal("no TEXT function found")
return nil
}
// disasm decodes a machine-code blob into Intel-syntax instruction strings.
func disasm(t *testing.T, code []byte) []string {
t.Helper()
var out []string
for len(code) > 0 {
inst, err := x86asm.Decode(code, 64)
if err != nil {
t.Fatalf("decode %x: %v", code, err)
}
out = append(out, x86asm.IntelSyntax(inst, 0, nil))
code = code[inst.Len:]
}
return out
}
func hexBytes(b []byte) string {
var sb strings.Builder
for _, x := range b {
sb.WriteString(" ")
const hexdig = "0123456789abcdef"
sb.WriteByte(hexdig[x>>4])
sb.WriteByte(hexdig[x&0xf])
}
return strings.TrimSpace(sb.String())
}
func TestAssembleLoop(t *testing.T) {
fn := firstText(t, `
#include "textflag.h"
TEXT ·f(SB), NOSPLIT, $0
XORQ AX, AX
loop:
ADDQ $1, AX
CMPQ $10, AX
JLT loop
RET
`)
code, labels, err := Assemble(fn)
if err != nil {
t.Fatalf("Assemble: %v", err)
}
if _, ok := labels["loop"]; !ok {
t.Fatalf("label 'loop' not recorded: %v", labels)
}
got := strings.Join(disasm(t, code), "\n")
want := strings.Join([]string{
"xor rax, rax",
"add rax, 0x1",
"cmp rax, 0xa",
"jl 0x0",
"ret",
}, "\n")
gotLines := strings.Split(got, "\n")
wantLines := strings.Split(want, "\n")
if len(gotLines) != len(wantLines) {
t.Fatalf("instruction count mismatch:\n got:\n%s\n want:\n%s", got, want)
}
for i := range wantLines {
if strings.HasPrefix(wantLines[i], "jl") {
if !strings.HasPrefix(gotLines[i], "jl") {
t.Errorf("line %d: got %q, want a jl", i, gotLines[i])
}
continue
}
if gotLines[i] != wantLines[i] {
t.Errorf("line %d: got %q, want %q", i, gotLines[i], wantLines[i])
}
}
}
func TestAssembleMemory(t *testing.T) {
fn := firstText(t, `
#include "textflag.h"
TEXT ·g(SB), NOSPLIT, $0
MOVQ (AX), BX
MOVQ 8(AX), CX
LEAQ (AX)(BX*4), DX
RET
`)
code, _, err := Assemble(fn)
if err != nil {
t.Fatalf("Assemble: %v", err)
}
got := strings.Join(disasm(t, code), "\n")
want := strings.Join([]string{
"mov rbx, qword ptr [rax]",
"mov rcx, qword ptr [rax+0x8]",
"lea rdx, ptr [rax+4*rbx]",
"ret",
}, "\n")
if got != want {
t.Errorf("assemble memory:\n got:\n%s\n want:\n%s", got, want)
}
}
// TestAssembleFP verifies the FP pseudo-register translation for a NOSPLIT $0
// function against the exact bytes the Go assembler produces (verified via
// `go tool objdump`): x+N(FP) maps to (N+8)(SP).
func TestAssembleFP(t *testing.T) {
fn := firstText(t, `
#include "textflag.h"
TEXT ·loadarg(SB), NOSPLIT, $0-24
MOVQ p+0(FP), AX
MOVQ n+8(FP), CX
ADDQ CX, AX
MOVQ AX, ret+16(FP)
RET
`)
code, _, err := Assemble(fn)
if err != nil {
t.Fatalf("Assemble: %v", err)
}
// From `go tool objdump` of the Go-assembled function:
// MOVQ 0x8(SP), AX 488b442408
// MOVQ 0x10(SP), CX 488b4c2410
// ADDQ CX, AX 4801c8
// MOVQ AX, 0x18(SP) 4889442418
// RET c3
want := []byte{
0x48, 0x8b, 0x44, 0x24, 0x08,
0x48, 0x8b, 0x4c, 0x24, 0x10,
0x48, 0x01, 0xc8,
0x48, 0x89, 0x44, 0x24, 0x18,
0xc3,
}
if hexBytes(code) != hexBytes(want) {
t.Errorf("FP translation mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want))
}
}
// TestAssembleFrame verifies a function with a non-zero frame: the Go-style
// prologue/epilogue and the x+N(FP) → (N+frame+16)(SP) translation, against
// the bytes the Go assembler produces.
func TestAssembleFrame(t *testing.T) {
fn := firstText(t, `
#include "textflag.h"
TEXT ·withframe(SB), NOSPLIT, $16-16
MOVQ a+0(FP), AX
MOVQ b+8(FP), CX
ADDQ CX, AX
MOVQ AX, ret+16(FP)
RET
`)
code, _, err := Assemble(fn)
if err != nil {
t.Fatalf("Assemble: %v", err)
}
// From `go tool objdump`:
// PUSHQ BP 55
// MOVQ SP, BP 4889e5
// SUBQ $0x10, SP 4883ec10
// MOVQ 0x20(SP), AX 488b442420 (0 + 16 + 16)
// MOVQ 0x28(SP), CX 488b4c2428 (8 + 16 + 16)
// ADDQ CX, AX 4801c8
// MOVQ AX, 0x30(SP) 4889442430 (16 + 16 + 16)
// ADDQ $0x10, SP 4883c410
// POPQ BP 5d
// RET c3
want := []byte{
0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x10,
0x48, 0x8b, 0x44, 0x24, 0x20,
0x48, 0x8b, 0x4c, 0x24, 0x28,
0x48, 0x01, 0xc8,
0x48, 0x89, 0x44, 0x24, 0x30,
0x48, 0x83, 0xc4, 0x10, 0x5d, 0xc3,
}
if hexBytes(code) != hexBytes(want) {
t.Errorf("frame translation mismatch:\n got: %s\n want: %s", hexBytes(code), hexBytes(want))
}
}
+288
View File
@@ -0,0 +1,288 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"fmt"
"strings"
)
// Encode encodes one Plan 9 instruction (mnemonic plus operands, in source
// order) into x86-64 machine code.
func Encode(mnemonic string, ops ...Operand) ([]byte, error) {
e := &enc{}
if err := e.encode(mnemonic, ops); err != nil {
return nil, err
}
return e.out, nil
}
type enc struct {
out []byte
}
func (e *enc) encode(mnem string, ops []Operand) error {
upper := strings.ToUpper(mnem)
// Fixed-name instructions (no size suffix).
switch {
case upper == "RET":
return e.encodeRet()
case upper == "NOP":
return e.emit(&instr{opcode: []byte{0x90}, modrm: -1, sib: -1})
case upper == "CALL":
return e.encodeJmpRel(ops, []byte{0xE8})
case upper == "JMP":
return e.encodeJmpRel(ops, []byte{0xE9})
}
if cc, ok := condCode(upper); ok {
return e.encodeJcc(cc, ops)
}
// VEX (AVX/AVX2) instructions: the trailing B/W/L/Q/D is part of the
// mnemonic, not a size suffix, so dispatch before splitSize.
if isVex(upper) {
return e.encodeVex(upper, ops)
}
base, size := splitSize(upper)
if size == 0 {
size = 8 // default operand size in 64-bit mode (e.g. PUSHQ)
}
switch base {
case "MOV":
return e.encodeMov(ops, size)
case "ADD", "SUB", "AND", "OR", "XOR", "CMP":
return e.encodeALU(aluOp[base], ops, size)
case "TEST":
return e.encodeTest(ops, size)
case "LEA":
return e.encodeLea(ops, size)
case "INC", "DEC", "NEG", "NOT":
return e.encodeUnary(unaryOp[base], ops, size)
case "SHL", "SHR", "SAR":
return e.encodeShift(shiftOp[base], ops, size)
case "IMUL":
return e.encodeImul(ops, size)
case "PUSH":
return e.encodePushPop(ops, true)
case "POP":
return e.encodePushPop(ops, false)
}
return fmt.Errorf("unsupported instruction %q", mnem)
}
// splitSize separates a trailing B/W/L/Q size suffix from the mnemonic.
func splitSize(upper string) (base string, size int) {
if upper == "" {
return upper, 0
}
switch upper[len(upper)-1] {
case 'B':
return upper[:len(upper)-1], 1
case 'W':
return upper[:len(upper)-1], 2
case 'L':
return upper[:len(upper)-1], 4
case 'Q':
return upper[:len(upper)-1], 8
}
return upper, 0
}
// --- instruction components -------------------------------------------------
type instr struct {
opSize16 bool
rexW bool
rexR bool
rexX bool
rexB bool
rexForced bool // REX needed even with all bits zero (8-bit low registers)
opcode []byte
modrm int // -1 if absent
sib int // -1 if absent
disp []byte
imm []byte
}
func (e *enc) emit(i *instr) error {
if i.opSize16 {
e.out = append(e.out, 0x66)
}
rex := byte(0)
if i.rexW {
rex |= 0x08
}
if i.rexR {
rex |= 0x04
}
if i.rexX {
rex |= 0x02
}
if i.rexB {
rex |= 0x01
}
if rex != 0 || i.rexForced {
e.out = append(e.out, 0x40|rex)
}
e.out = append(e.out, i.opcode...)
if i.modrm >= 0 {
e.out = append(e.out, byte(i.modrm))
}
if i.sib >= 0 {
e.out = append(e.out, byte(i.sib))
}
e.out = append(e.out, i.disp...)
e.out = append(e.out, i.imm...)
return nil
}
// newInstr starts an instruction with a size-derived REX.W and 0x66 prefix.
func newInstr(opSize int, opcode []byte) *instr {
return &instr{
opSize16: opSize == 2,
rexW: opSize == 8,
opcode: opcode,
modrm: -1,
sib: -1,
}
}
// --- ModR/M, SIB, displacement ----------------------------------------------
// setRM fills in the ModR/M (and SIB, displacement, REX bits) for an
// instruction whose reg field holds a real register `reg` and whose r/m field
// holds `rm`.
func setRM(i *instr, reg Reg, rm Operand, opSize int) error {
return setRMReg(i, reg.idx&7, reg.idx >= 8, reg.needsREX(opSize), rm, opSize)
}
// setRMDigit fills in the ModR/M for an instruction whose reg field is an
// opcode /digit extension (0–7), which carries none of the register REX rules.
func setRMDigit(i *instr, digit int, rm Operand, opSize int) error {
return setRMReg(i, digit, false, false, rm, opSize)
}
func setRMReg(i *instr, regField int, rexR, regForced bool, rm Operand, opSize int) error {
i.rexR = rexR
if regForced {
i.rexForced = true
}
switch r := rm.(type) {
case Reg:
i.rexB = r.idx >= 8
if r.needsREX(opSize) {
i.rexForced = true
}
i.modrm = 0xC0 | regField<<3 | (r.idx & 7)
return nil
case Mem:
return setMem(i, regField, r)
default:
return fmt.Errorf("invalid r/m operand %T", rm)
}
}
func setMem(i *instr, regField int, m Mem) error {
modrm, sib, disp, xBit, bBit, err := memComponents(regField, m)
if err != nil {
return err
}
i.modrm = modrm
i.sib = sib
i.disp = disp
i.rexX = xBit == 1
i.rexB = bBit == 1
return nil
}
// memComponents computes the ModR/M byte (with the given reg field), the SIB
// byte (-1 if none), the displacement bytes, and the high index/base bits, for
// a memory operand. It is shared by the REX (scalar) and VEX (vector) paths.
func memComponents(regField int, m Mem) (modrm, sib int, disp []byte, xBit, bBit int, err error) {
sib = -1
// RIP-relative: neither base nor index.
if !m.HasBase && !m.HasIndex {
return regField<<3 | 0x05, -1, le32(m.Disp), 0, 0, nil // mod=00, rm=101
}
needSIB := m.HasIndex || (m.HasBase && m.Base.idx&7 == 4)
var mod int
switch {
case !m.HasBase:
mod = 0
disp = le32(m.Disp)
case m.Base.idx&7 == 5 && m.Disp == 0:
mod = 1
disp = []byte{0}
case m.Disp == 0:
mod = 0
case fits8(m.Disp):
mod = 1
disp = []byte{byte(int8(m.Disp))}
default:
mod = 2
disp = le32(m.Disp)
}
if needSIB {
idxField := 4 // 100 = no index
if m.HasIndex {
idxField = m.Index.idx & 7
if m.Index.idx >= 8 {
xBit = 1
}
}
baseField := 5 // 101 = no base (with mod=00 → disp32)
if m.HasBase {
baseField = m.Base.idx & 7
if m.Base.idx >= 8 {
bBit = 1
}
}
return mod<<6 | regField<<3 | 0x04, scaleBits(m.Scale)<<6 | idxField<<3 | baseField, disp, xBit, bBit, nil
}
if m.Base.idx >= 8 {
bBit = 1
}
return mod<<6 | regField<<3 | (m.Base.idx & 7), -1, disp, 0, bBit, nil
}
func scaleBits(scale int) int {
switch scale {
case 2:
return 1
case 4:
return 2
case 8:
return 3
default:
return 0 // scale 1 (or unset)
}
}
func fits8(v int64) bool { return v >= -128 && v <= 127 }
func le32(v int64) []byte {
u := uint32(v)
return []byte{byte(u), byte(u >> 8), byte(u >> 16), byte(u >> 24)}
}
func le16(v int64) []byte {
u := uint16(v)
return []byte{byte(u), byte(u >> 8)}
}
func le64(v int64) []byte {
u := uint64(v)
b := make([]byte, 8)
for i := 0; i < 8; i++ {
b[i] = byte(u >> (8 * i))
}
return b
}
+132
View File
@@ -0,0 +1,132 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"testing"
"golang.org/x/arch/x86/x86asm"
)
// decode encodes an instruction and decodes it back, returning the decoded
// instruction and its Intel-syntax rendering.
func decode(t *testing.T, mnemonic string, ops ...Operand) (x86asm.Inst, string) {
t.Helper()
code, err := Encode(mnemonic, ops...)
if err != nil {
t.Fatalf("Encode(%s): %v", mnemonic, err)
}
inst, err := x86asm.Decode(code, 64)
if err != nil {
t.Fatalf("Decode(%x) of %s: %v", code, mnemonic, err)
}
if inst.Len != len(code) {
t.Fatalf("Decode consumed %d of %d bytes for %s (%x)", inst.Len, len(code), mnemonic, code)
}
return inst, x86asm.IntelSyntax(inst, 0, nil)
}
// checkSyntax asserts an instruction encodes and decodes to the expected
// Intel-syntax string.
func checkSyntax(t *testing.T, want, mnemonic string, ops ...Operand) {
t.Helper()
_, got := decode(t, mnemonic, ops...)
if got != want {
t.Errorf("%s: got %q, want %q", mnemonic, got, want)
}
}
// checkOp asserts the decoded opcode (used for relative jumps, whose rendered
// target depends on the program counter).
func checkOp(t *testing.T, want x86asm.Op, mnemonic string, ops ...Operand) {
t.Helper()
inst, _ := decode(t, mnemonic, ops...)
if inst.Op != want {
t.Errorf("%s: got op %v, want %v", mnemonic, inst.Op, want)
}
}
func TestMov(t *testing.T) {
checkSyntax(t, "mov rbx, rax", "MOVQ", AX, BX)
checkSyntax(t, "mov ebx, eax", "MOVL", AX, BX)
checkSyntax(t, "mov bl, al", "MOVB", AL, BL)
checkSyntax(t, "mov rax, rbx", "MOVQ", BX, AX)
checkSyntax(t, "mov rbx, qword ptr [rax]", "MOVQ", Ptr(AX, 0, 8), BX)
checkSyntax(t, "mov qword ptr [rbx], rax", "MOVQ", AX, Ptr(BX, 0, 8))
checkSyntax(t, "mov rbx, qword ptr [rax+0x10]", "MOVQ", Ptr(AX, 0x10, 8), BX)
checkSyntax(t, "mov rbx, qword ptr [rsi+4*rbx]", "MOVQ", Idx(SI, BX, 4, 0, 8), BX)
checkSyntax(t, "mov rax, 0x5", "MOVQ", Imm(5), AX)
checkSyntax(t, "mov r8, 0x5", "MOVQ", Imm(5), Reg{idx: 8, size: 8})
checkSyntax(t, "mov qword ptr [rax], 0x5", "MOVQ", Imm(5), Ptr(AX, 0, 8))
checkSyntax(t, "mov r12, r13", "MOVQ", Reg{idx: 13, size: 8}, Reg{idx: 12, size: 8})
}
func TestALU(t *testing.T) {
checkSyntax(t, "add rbx, rax", "ADDQ", AX, BX)
checkSyntax(t, "add rax, 0x1", "ADDQ", Imm(1), AX)
checkSyntax(t, "add rax, 0x12c", "ADDQ", Imm(300), AX)
checkSyntax(t, "sub rdx, rcx", "SUBQ", CX, DX)
checkSyntax(t, "and rbx, 0x7", "ANDQ", Imm(7), BX)
checkSyntax(t, "or rcx, rbx", "ORQ", BX, CX)
checkSyntax(t, "xor rax, rax", "XORQ", AX, AX)
checkSyntax(t, "cmp r10, rsi", "CMPQ", SI, Reg{idx: 10, size: 8})
checkSyntax(t, "add rbx, qword ptr [rax]", "ADDQ", Ptr(AX, 0, 8), BX)
checkSyntax(t, "add qword ptr [rax], rbx", "ADDQ", BX, Ptr(AX, 0, 8))
checkSyntax(t, "cmp rbx, -0x20", "CMPQ", Imm(-32), BX)
}
func TestLea(t *testing.T) {
checkSyntax(t, "lea r9, ptr [rsi+4*rbx]", "LEAQ", Idx(SI, BX, 4, 0, 8), Reg{idx: 9, size: 8})
checkSyntax(t, "lea rax, ptr [rbx+0x8]", "LEAQ", Ptr(BX, 0x8, 8), AX)
}
func TestTest(t *testing.T) {
checkSyntax(t, "test rax, rax", "TESTQ", AX, AX)
checkSyntax(t, "test rbx, 0x7", "TESTQ", Imm(7), BX)
}
func TestPushPop(t *testing.T) {
checkSyntax(t, "push rbx", "PUSHQ", BX)
checkSyntax(t, "pop r12", "POPQ", Reg{idx: 12, size: 8})
checkSyntax(t, "push 0x5", "PUSHQ", Imm(5))
}
func TestUnary(t *testing.T) {
checkSyntax(t, "inc rax", "INCQ", AX)
checkSyntax(t, "dec rbx", "DECQ", BX)
checkSyntax(t, "neg rcx", "NEGQ", CX)
checkSyntax(t, "not rdx", "NOTQ", DX)
}
func TestShift(t *testing.T) {
checkSyntax(t, "shl rdx, 0x2", "SHLQ", Imm(2), DX)
checkSyntax(t, "shl rdx, cl", "SHLQ", CL, DX)
checkSyntax(t, "shl rdx, 0x1", "SHLQ", Imm(1), DX)
checkSyntax(t, "sar rcx, 0x1f", "SARQ", Imm(31), CX)
}
func TestImul(t *testing.T) {
checkSyntax(t, "imul rdx, rcx", "IMULQ", CX, DX)
checkSyntax(t, "imul edx, edx, 0x3", "IMULL", Imm(3), DX, DX)
checkSyntax(t, "imul rdx, rcx, 0x100", "IMULQ", Imm(256), CX, DX)
}
func TestControl(t *testing.T) {
checkSyntax(t, "ret", "RET")
checkSyntax(t, "nop", "NOP")
checkOp(t, x86asm.JMP, "JMP", Imm(0))
checkOp(t, x86asm.CALL, "CALL", Imm(0))
checkOp(t, x86asm.JGE, "JGE", Imm(0))
checkOp(t, x86asm.JNE, "JNE", Imm(0))
checkOp(t, x86asm.JBE, "JLS", Imm(0))
}
// TestGoFlacScalarTail encodes the scalar tail of an analyze kernel to confirm
// the encoder handles a realistic instruction sequence.
func TestGoFlacScalarTail(t *testing.T) {
// MOVQ swin_base+0(FP), SI — modelled as MOVQ disp(reg), reg.
checkSyntax(t, "mov rsi, qword ptr [rax+0x10]", "MOVQ", Ptr(AX, 0x10, 8), SI)
checkSyntax(t, "lea r9, ptr [rsi+4*rbx]", "LEAQ", Idx(SI, BX, 4, 0, 8), Reg{idx: 9, size: 8})
checkSyntax(t, "and r10, -0x8", "ANDQ", Imm(-8), Reg{idx: 10, size: 8})
}
+488
View File
@@ -0,0 +1,488 @@
// 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, r/m: 0x8A/0x8B, reg=dst, rm=src.
i := newInstr(size, []byte{movRR(size)})
if err := setRM(i, dstReg, src, 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)
}
dstReg, dstIsReg := dst.(Reg)
srcReg, srcIsReg := src.(Reg)
switch {
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
}
}
+43
View File
@@ -0,0 +1,43 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
// 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() {}
// 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
}
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}
}
// Rip builds a RIP-relative memory operand (RIP)+disp.
func Rip(disp int64, size int) Mem {
return Mem{Disp: disp, Size: size}
}
+169
View File
@@ -0,0 +1,169 @@
// 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 "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.
type Reg struct {
idx int
size int // informational width implied by the name; the mnemonic decides
high bool // AH/CH/DH/BH
}
// Index returns the register number (0–15).
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 }
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{0, 1, false}
CL = Reg{1, 1, false}
DL = Reg{2, 1, false}
BL = Reg{3, 1, false}
AH = Reg{4, 1, true}
CH = Reg{5, 1, true}
DH = Reg{6, 1, true}
BH = Reg{7, 1, true}
SPL = Reg{4, 1, false}
BPL = Reg{5, 1, false}
SIL = Reg{6, 1, false}
DIL = Reg{7, 1, false}
AX = Reg{0, 2, false}
CX = Reg{1, 2, false}
DX = Reg{2, 2, false}
BX = Reg{3, 2, false}
SP = Reg{4, 2, false}
BP = Reg{5, 2, false}
SI = Reg{6, 2, false}
DI = Reg{7, 2, false}
EAX = Reg{0, 4, false}
ECX = Reg{1, 4, false}
EDX = Reg{2, 4, false}
EBX = Reg{3, 4, false}
ESP = Reg{4, 4, false}
EBP = Reg{5, 4, false}
ESI = Reg{6, 4, false}
EDI = Reg{7, 4, false}
RAX = Reg{0, 8, false}
RCX = Reg{1, 8, false}
RDX = Reg{2, 8, false}
RBX = Reg{3, 8, false}
RSP = Reg{4, 8, false}
RBP = Reg{5, 8, false}
RSI = Reg{6, 8, false}
RDI = Reg{7, 8, false}
)
// 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{i, 8, false}
}
for i := 8; i <= 15; i++ {
m["R"+itoa(i)] = Reg{i, 8, false}
}
// 32-bit: EAX..EDI, R8D..R15D.
e32 := []string{"EAX", "ECX", "EDX", "EBX", "ESP", "EBP", "ESI", "EDI"}
for i, n := range e32 {
m[n] = Reg{i, 4, false}
}
for i := 8; i <= 15; i++ {
m["R"+itoa(i)+"D"] = Reg{i, 4, false}
}
// 16-bit: AX..DI, R8W..R15W.
w16 := []string{"AX", "CX", "DX", "BX", "SP", "BP", "SI", "DI"}
for i, n := range w16 {
m[n] = Reg{i, 2, false}
}
for i := 8; i <= 15; i++ {
m["R"+itoa(i)+"W"] = Reg{i, 2, false}
}
// 8-bit: AL..BH, SPL..DIL, R8B..R15B.
for n, r := range 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,
} {
m[n] = r
}
for i := 8; i <= 15; i++ {
m["R"+itoa(i)+"B"] = Reg{i, 1, false}
}
// Vector: X0..X15 (128-bit, encoded size 16), Y0..Y15 (256-bit, size 32).
// Z (512-bit) and K (mask) registers arrive with EVEX/AVX-512 support.
for i := 0; i <= 15; i++ {
m["X"+itoa(i)] = Reg{i, 16, false}
m["Y"+itoa(i)] = Reg{i, 32, false}
}
return m
}
// isVec reports whether r is an XMM/YMM vector register.
func (r Reg) isVec() bool { return r.size == 16 || r.size == 32 }
// vecLenBit returns the VEX.L bit for a vector register (X=0/128-bit,
// Y=1/256-bit).
func (r Reg) vecLenBit() int {
if r.size == 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:])
}
+234
View File
@@ -0,0 +1,234 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import "fmt"
// This file implements VEX (AVX/AVX2) instruction encoding. EVEX (AVX-512)
// support is a later increment.
// vexForm selects how an instruction's operands map onto the VEX.vvvv,
// ModRM.reg and ModRM.rm fields.
type vexForm int
const (
// vexNDS3 is the three-operand form `OP src2, src1, dst` (Plan 9 order):
// ModRM.reg = dst (op2), VEX.vvvv = src1 (op1), ModRM.rm = src2 (op0).
vexNDS3 vexForm = iota
// vexRM is the two-operand form `OP src, dst` with no vvvv source:
// ModRM.reg = dst (op1), ModRM.rm = src (op0), VEX.vvvv = 1111 (unused).
vexRM
// vexShiftImm is the immediate-shift form `OP $imm, src, dst`: ModRM.reg =
// /digit, ModRM.rm = src (op1), VEX.vvvv = dst (op2), imm8 = op0.
vexShiftImm
)
// vexSpec describes one VEX instruction's encoding parameters.
type vexSpec struct {
mapSel int // 1 = 0F, 2 = 0F38, 3 = 0F3A
opcode byte
w int // VEX.W (0 for WIG)
pp int // 0 = none, 1 = 66, 2 = F3, 3 = F2
opdigit int // ModRM.reg /digit, or -1 when reg is a register
form vexForm
}
// vexTable maps an upper-case mnemonic to its VEX encoding. It covers the
// AVX2 instructions used by the go-flac kernels in the three-operand NDS form;
// it is extended incrementally.
var vexTable = map[string]vexSpec{
// VEX.128/256.66.0F.WIG — integer arithmetic / logic / compare.
"VPADDD": {1, 0xFE, 0, 1, -1, vexNDS3},
"VPADDQ": {1, 0xD4, 0, 1, -1, vexNDS3},
"VPSUBD": {1, 0xFA, 0, 1, -1, vexNDS3},
"VPSUBQ": {1, 0xFB, 0, 1, -1, vexNDS3},
"VPXOR": {1, 0xEF, 0, 1, -1, vexNDS3},
"VPOR": {1, 0xEB, 0, 1, -1, vexNDS3},
"VPAND": {1, 0xDB, 0, 1, -1, vexNDS3},
"VPANDN": {1, 0xDF, 0, 1, -1, vexNDS3},
"VPCMPEQD": {1, 0x76, 0, 1, -1, vexNDS3},
"VPUNPCKLDQ": {1, 0x62, 0, 1, -1, vexNDS3},
"VPUNPCKHDQ": {1, 0x6A, 0, 1, -1, vexNDS3},
"VPUNPCKLQDQ": {1, 0x6C, 0, 1, -1, vexNDS3},
"VPACKSSDW": {1, 0x6B, 0, 1, -1, vexNDS3},
// VEX.128/256.66.0F38.WIG.
"VPMULLD": {2, 0x40, 0, 1, -1, vexNDS3},
"VPMULDQ": {2, 0x28, 0, 1, -1, vexNDS3},
"VPSHUFB": {2, 0x00, 0, 1, -1, vexNDS3},
"VPCMPGTQ": {2, 0x37, 0, 1, -1, vexNDS3},
// VEX.128/256.66.0F38.WIG — sign/zero extend and broadcast (reg=dst, rm=src,
// no vvvv).
"VPMOVSXWD": {2, 0x23, 0, 1, -1, vexRM},
"VPMOVSXDQ": {2, 0x25, 0, 1, -1, vexRM},
"VPMOVZXDQ": {2, 0x35, 0, 1, -1, vexRM},
"VPBROADCASTD": {2, 0x58, 0, 1, -1, vexRM},
"VPBROADCASTQ": {2, 0x59, 0, 1, -1, vexRM},
// VEX.128/256.66.0F.WIG — move mask to a GPR (reg=gpr dst, rm=vec src).
"VPMOVMSKB": {1, 0xD7, 0, 1, -1, vexRM},
"VMOVMSKPS": {1, 0x50, 0, 0, -1, vexRM}, // no 66 prefix (that would be VMOVMSKPD)
// VEX.128/256.66.0F.WIG — immediate shifts (opdigit selects the shift).
"VPSLLD": {1, 0x72, 0, 1, 6, vexShiftImm},
"VPSRAD": {1, 0x72, 0, 1, 4, vexShiftImm},
"VPSRLD": {1, 0x72, 0, 1, 2, vexShiftImm},
"VPSRLQ": {1, 0x73, 0, 1, 2, vexShiftImm},
"VPSLLQ": {1, 0x73, 0, 1, 6, vexShiftImm},
}
// isVex reports whether the mnemonic is a VEX-encoded instruction we handle.
func isVex(mnemUpper string) bool {
_, ok := vexTable[mnemUpper]
return ok
}
// encodeVex encodes a VEX instruction with operands in Plan 9 order.
func (e *enc) encodeVex(mnemUpper string, ops []Operand) error {
spec := vexTable[mnemUpper]
switch spec.form {
case vexNDS3:
return e.encodeVexNDS3(spec, ops)
case vexRM:
return e.encodeVexRM(spec, ops)
case vexShiftImm:
return e.encodeVexShiftImm(spec, ops)
}
return fmt.Errorf("unhandled VEX form for %s", mnemUpper)
}
// encodeVexNDS3 encodes the three-operand NDS form: OP src2, src1, dst.
func (e *enc) encodeVexNDS3(spec vexSpec, ops []Operand) error {
if len(ops) != 3 {
return fmt.Errorf("VEX NDS instruction expects 3 operands, got %d", len(ops))
}
src2, src1, dst := ops[0], ops[1], ops[2]
dstReg, ok := dst.(Reg)
if !ok || !dstReg.isVec() {
return fmt.Errorf("VEX destination must be a vector register")
}
vvvvReg, ok := src1.(Reg)
if !ok || !vvvvReg.isVec() {
return fmt.Errorf("VEX vvvv operand must be a vector register")
}
regField := dstReg.idx & 7
rBit := 0
if dstReg.idx >= 8 {
rBit = 1
}
vvvvBar := 15 - (vvvvReg.idx & 15)
return e.emitVexFields(spec, dstReg.vecLenBit(), regField, rBit, vvvvBar, src2)
}
// encodeVexRM encodes the two-operand form: OP src, dst (no vvvv source).
// ModRM.reg = dst, ModRM.rm = src; the vector length comes from whichever
// operand is a vector register (the destination for extends/broadcasts, the
// source for the move-mask instructions whose destination is a GPR).
func (e *enc) encodeVexRM(spec vexSpec, ops []Operand) error {
if len(ops) != 2 {
return fmt.Errorf("VEX two-operand instruction expects 2 operands, got %d", len(ops))
}
src, dst := ops[0], ops[1]
dstReg, ok := dst.(Reg)
if !ok {
return fmt.Errorf("VEX destination must be a register")
}
regField := dstReg.idx & 7
rBit := 0
if dstReg.idx >= 8 {
rBit = 1
}
// Vector length: from the destination if it is a vector, otherwise from the
// source (move-mask instructions have a GPR destination and a vector source).
l := 0
if dstReg.isVec() {
l = dstReg.vecLenBit()
} else if srcReg, ok := src.(Reg); ok && srcReg.isVec() {
l = srcReg.vecLenBit()
}
return e.emitVexFields(spec, l, regField, rBit, 0, src) // vvvv unused → vvvvBar=0
}
// encodeVexShiftImm encodes an immediate-shift instruction: OP $imm, src, dst.
// The destination is carried in VEX.vvvv, the source in ModRM.rm, and the
// shift kind in the ModRM.reg /digit.
func (e *enc) encodeVexShiftImm(spec vexSpec, ops []Operand) error {
if len(ops) != 3 {
return fmt.Errorf("VEX shift expects 3 operands ($imm, src, dst), got %d", len(ops))
}
imm, src, dst := ops[0], ops[1], ops[2]
immVal, ok := imm.(Imm)
if !ok {
return fmt.Errorf("shift count must be an immediate")
}
srcReg, ok := src.(Reg)
if !ok || !srcReg.isVec() {
return fmt.Errorf("shift source must be a vector register")
}
dstReg, ok := dst.(Reg)
if !ok || !dstReg.isVec() {
return fmt.Errorf("shift destination must be a vector register")
}
vvvvBar := 15 - (dstReg.idx & 15)
l := dstReg.vecLenBit()
rmField := srcReg.idx & 7
bBit := 0
if srcReg.idx >= 8 {
bBit = 1
}
modrm := 0xC0 | spec.opdigit<<3 | rmField
if spec.mapSel == 1 && bBit == 0 && spec.w == 0 {
e.out = append(e.out, 0xC5, byte(1<<7|vvvvBar<<3|l<<2|spec.pp))
} else {
e.out = append(e.out, 0xC4,
byte(1<<7|1<<6|(1-bBit)<<5|spec.mapSel),
byte(spec.w<<7|vvvvBar<<3|l<<2|spec.pp))
}
e.out = append(e.out, spec.opcode, byte(modrm), byte(int8(immVal)))
return nil
}
// emitVexFields emits the VEX prefix, opcode, ModR/M, SIB and displacement for
// the given precomputed fields. It is shared by the NDS and RM forms.
func (e *enc) emitVexFields(spec vexSpec, l, regField, rBit, vvvvBar int, rm Operand) error {
var modrm, sib int
var disp []byte
var xBit, bBit int
switch r := rm.(type) {
case Reg:
modrm = 0xC0 | regField<<3 | (r.idx & 7)
sib = -1
if r.idx >= 8 {
bBit = 1
}
case Mem:
var err error
modrm, sib, disp, xBit, bBit, err = memComponents(regField, r)
if err != nil {
return err
}
default:
return fmt.Errorf("invalid VEX r/m operand")
}
if spec.mapSel == 1 && xBit == 0 && bBit == 0 && spec.w == 0 {
e.out = append(e.out, 0xC5, byte((1-rBit)<<7|vvvvBar<<3|l<<2|spec.pp))
} else {
e.out = append(e.out, 0xC4,
byte((1-rBit)<<7|(1-xBit)<<6|(1-bBit)<<5|spec.mapSel),
byte(spec.w<<7|vvvvBar<<3|l<<2|spec.pp))
}
e.out = append(e.out, spec.opcode, byte(modrm))
if sib >= 0 {
e.out = append(e.out, byte(sib))
}
e.out = append(e.out, disp...)
return nil
}
+142
View File
@@ -0,0 +1,142 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
package asm
import (
"testing"
"golang.org/x/arch/x86/x86asm"
)
func vreg(t *testing.T, name string) Reg {
t.Helper()
r, ok := ParseReg(name)
if !ok {
t.Fatalf("unknown register %s", name)
}
return r
}
// TestVexNDS3 encodes `mnem Y0, Y1, Y2` for every three-operand NDS
// instruction and verifies it round-trips through the x86 decoder to the same
// mnemonic. A wrong opcode/map/pp surfaces as a different decoded instruction.
func TestVexNDS3(t *testing.T) {
for mnem, spec := range vexTable {
if spec.form != vexNDS3 {
continue
}
code, err := Encode(mnem, vreg(t, "Y0"), vreg(t, "Y1"), vreg(t, "Y2"))
if err != nil {
t.Errorf("%s: Encode: %v", mnem, err)
continue
}
inst, err := x86asm.Decode(code, 64)
if err != nil {
t.Errorf("%s: Decode(% x): %v", mnem, code, err)
continue
}
if inst.Op.String() != mnem {
t.Errorf("%s: decoded as %s (% x)", mnem, inst.Op.String(), code)
}
}
}
// TestVexGoFlac checks a representative go-flac instruction sequence encodes
// and decodes as expected.
func TestVexGoFlac(t *testing.T) {
// VPADDD Y5, Y8, Y8 → vpaddd ymm8, ymm8, ymm5.
code, err := Encode("VPADDD", vreg(t, "Y5"), vreg(t, "Y8"), vreg(t, "Y8"))
if err != nil {
t.Fatalf("Encode: %v", err)
}
inst, err := x86asm.Decode(code, 64)
if err != nil {
t.Fatalf("Decode(% x): %v", code, err)
}
if inst.Op != x86asm.VPADDD {
t.Fatalf("decoded %s, want VPADDD", inst.Op)
}
}
// TestVexXMM checks the 128-bit (XMM) form selects VEX.L=0.
func TestVexXMM(t *testing.T) {
code, err := Encode("VPXOR", vreg(t, "X7"), vreg(t, "X7"), vreg(t, "X7"))
if err != nil {
t.Fatalf("Encode: %v", err)
}
inst, err := x86asm.Decode(code, 64)
if err != nil {
t.Fatalf("Decode(% x): %v", code, err)
}
if inst.Op != x86asm.VPXOR {
t.Fatalf("decoded %s, want VPXOR", inst.Op)
}
// vpxor xmm7, xmm7, xmm7 → C5 C9 EF FF (2-byte VEX, L=0).
if code[0] != 0xC5 {
t.Errorf("expected 2-byte VEX (C5), got % x", code)
}
}
// TestVexRM validates the two-operand (reg=dst, rm=src, no vvvv) forms by
// round-tripping through the decoder.
func TestVexRM(t *testing.T) {
cases := []struct {
mnem string
ops []Operand
want x86asm.Op
}{
{"VPMOVSXWD", []Operand{Ptr(SI, 0, 16), vreg(t, "Y0")}, x86asm.VPMOVSXWD},
{"VPMOVSXDQ", []Operand{vreg(t, "X0"), vreg(t, "Y4")}, x86asm.VPMOVSXDQ},
{"VPMOVZXDQ", []Operand{vreg(t, "X4"), vreg(t, "Y4")}, x86asm.VPMOVZXDQ},
{"VPBROADCASTD", []Operand{vreg(t, "X0"), vreg(t, "Y15")}, x86asm.VPBROADCASTD},
{"VPMOVMSKB", []Operand{vreg(t, "X11"), AX}, x86asm.VPMOVMSKB},
{"VMOVMSKPS", []Operand{vreg(t, "Y7"), AX}, x86asm.VMOVMSKPS},
}
for _, c := range cases {
code, err := Encode(c.mnem, c.ops...)
if err != nil {
t.Errorf("%s: Encode: %v", c.mnem, err)
continue
}
inst, err := x86asm.Decode(code, 64)
if err != nil {
t.Errorf("%s: Decode(% x): %v", c.mnem, code, err)
continue
}
if inst.Op != c.want {
t.Errorf("%s: decoded as %s (% x)", c.mnem, inst.Op, code)
}
}
}
// TestVexShiftImm validates the immediate-shift form, checking the destination
// (VEX.vvvv) and source (ModRM.rm) land in the right places.
func TestVexShiftImm(t *testing.T) {
// VPSLLD $1, Y3, Y4 → vpslld ymm4, ymm3, 1.
code, err := Encode("VPSLLD", Imm(1), vreg(t, "Y3"), vreg(t, "Y4"))
if err != nil {
t.Fatalf("Encode: %v", err)
}
inst, err := x86asm.Decode(code, 64)
if err != nil {
t.Fatalf("Decode(% x): %v", code, err)
}
if inst.Op != x86asm.VPSLLD {
t.Fatalf("decoded %s, want VPSLLD (% x)", inst.Op, code)
}
// Intel order: dst, src, imm → "vpslld ymm4, ymm3, 0x1".
if got := x86asm.IntelSyntax(inst, 0, nil); got != "vpslld ymm4, ymm3, 0x1" {
t.Errorf("VPSLLD syntax = %q, want \"vpslld ymm4, ymm3, 0x1\" (% x)", got, code)
}
// VPSRAD $31, Y3, Y3 → vpsrad ymm3, ymm3, 31.
code, err = Encode("VPSRAD", Imm(31), vreg(t, "Y3"), vreg(t, "Y3"))
if err != nil {
t.Fatalf("Encode VPSRAD: %v", err)
}
inst, err = x86asm.Decode(code, 64)
if err != nil || inst.Op != x86asm.VPSRAD {
t.Fatalf("VPSRAD decoded %v (err %v), want VPSRAD", inst.Op, err)
}
}