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:
+292
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user