2026-08-07 22:43:40 +02:00
|
|
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package asm
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
2026-08-20 13:32:52 +02:00
|
|
|
"strings"
|
2026-08-07 22:43:40 +02:00
|
|
|
|
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-20 13:32:52 +02:00
|
|
|
// assembleARM64 assembles an AArch64 (arm64) TEXT function body into machine
|
|
|
|
|
// code. Every instruction is 4 bytes; the MOV pseudo-instruction and the
|
2026-09-14 18:22:00 +02:00
|
|
|
// immediate-arithmetic forms expand to 2-4 instructions when the immediate
|
2026-08-20 13:32:52 +02:00
|
|
|
// does not fit, so the layout is computed in two passes (sizes, then encoding
|
|
|
|
|
// with resolved branch targets).
|
|
|
|
|
//
|
|
|
|
|
// The emitted bytes match the Go toolchain's arm64 assembler, which is the
|
|
|
|
|
// ground-truth oracle: prologue/epilogue, FP/SP frame mapping, branch
|
|
|
|
|
// encodings and the MOV immediate expansions all follow cmd/internal/obj/
|
2026-09-14 18:22:00 +02:00
|
|
|
// arm64's asmout cases. One deliberate difference: the stack-growth guard
|
|
|
|
|
// (the morestack check in the prologue and the call back into the runtime in
|
|
|
|
|
// the epilogue) is not emitted, so the bytes match only for NOSPLIT functions
|
|
|
|
|
// or zero-frame leaves, where the toolchain emits no guard either.
|
2026-08-20 13:32:52 +02:00
|
|
|
func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, error) {
|
|
|
|
|
fi := arm64ComputeFrame(t)
|
|
|
|
|
prologue := arm64Prologue(fi)
|
2026-09-14 20:49:03 +02:00
|
|
|
guardLen := arm64GuardLen(fi)
|
2026-08-20 13:32:52 +02:00
|
|
|
chain := arm64JumpChain(t)
|
|
|
|
|
resolve := func(name string) string {
|
|
|
|
|
if r, ok := chain[name]; ok {
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var relocs []Reloc
|
|
|
|
|
var spadj []SpadjStep
|
|
|
|
|
|
|
|
|
|
// The prologue (3 instructions when a small frame, 4 for large)
|
2026-09-14 20:49:03 +02:00
|
|
|
// raises the SP delta by autosize. The guard prefix shifts its PC.
|
2026-08-20 13:32:52 +02:00
|
|
|
if fi.autosize != 0 {
|
2026-09-14 20:49:03 +02:00
|
|
|
spadj = append(spadj, SpadjStep{PC: guardLen + arm64PrologueSpadjPC(fi), Value: fi.autosize})
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Pass 1: label offsets from the instruction sizes.
|
|
|
|
|
offsets := map[string]int{}
|
2026-09-14 20:49:03 +02:00
|
|
|
pos := guardLen + len(prologue)
|
2026-08-20 13:32:52 +02:00
|
|
|
for _, stmt := range t.Body {
|
|
|
|
|
switch s := stmt.(type) {
|
|
|
|
|
case *ast.Label:
|
|
|
|
|
offsets[s.Name.Text] = pos
|
|
|
|
|
case *ast.Instr:
|
|
|
|
|
pos += arm64InstrSize(s, fi)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 20:49:03 +02:00
|
|
|
// Pass 2: encode. The guard prefix precedes the prologue; its branches
|
|
|
|
|
// target the morestack block at the end of the function, whose position
|
|
|
|
|
// the first pass has settled.
|
|
|
|
|
bodyLen := 0
|
|
|
|
|
{
|
|
|
|
|
p := guardLen + len(prologue)
|
|
|
|
|
for _, stmt := range t.Body {
|
|
|
|
|
if in, ok := stmt.(*ast.Instr); ok {
|
|
|
|
|
p += arm64InstrSize(in, fi)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
bodyLen = p - (guardLen + len(prologue))
|
|
|
|
|
}
|
|
|
|
|
var out []byte
|
|
|
|
|
if fi.needSplit {
|
|
|
|
|
out = append(out, arm64GuardBytes(fi, guardLen+len(prologue)+bodyLen)...)
|
|
|
|
|
}
|
|
|
|
|
out = append(out, prologue...)
|
|
|
|
|
pc := guardLen + len(prologue)
|
2026-08-20 13:32:52 +02:00
|
|
|
preCount := len(relocs)
|
|
|
|
|
var lines []LineEntry
|
|
|
|
|
for _, stmt := range t.Body {
|
|
|
|
|
in, ok := stmt.(*ast.Instr)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
code, err := encodeARM64Instr(in, pc, offsets, fi, &relocs, resolve)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", in.Mnemonic.Text, err)
|
|
|
|
|
}
|
|
|
|
|
for j := preCount; j < len(relocs); j++ {
|
2026-09-14 18:22:00 +02:00
|
|
|
// Make the relocation offsets function-relative: each instruction
|
|
|
|
|
// records its reloc offset relative to its own start, and pc is
|
|
|
|
|
// that instruction's offset from the function start (prologue
|
|
|
|
|
// included). After shifts by the same amount.
|
|
|
|
|
relocs[j].Off += pc
|
|
|
|
|
relocs[j].After += pc
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
|
|
|
|
preCount = len(relocs)
|
|
|
|
|
lines = append(lines, LineEntry{Offset: pc, Line: in.Pos().Line})
|
|
|
|
|
// The RET's epilogue closes the frame: the SP delta returns to zero.
|
|
|
|
|
if strings.ToUpper(in.Mnemonic.Text) == "RET" && fi.autosize != 0 {
|
|
|
|
|
epi := arm64ReturnEpilogueLen(fi)
|
|
|
|
|
spadj = append(spadj, SpadjStep{PC: pc + epi, Value: 0})
|
|
|
|
|
}
|
|
|
|
|
out = append(out, code...)
|
|
|
|
|
pc += len(code)
|
|
|
|
|
}
|
2026-09-14 20:49:03 +02:00
|
|
|
if fi.needSplit {
|
|
|
|
|
block, blReloc := arm64MoreStackBlock(pc)
|
|
|
|
|
out = append(out, block...)
|
|
|
|
|
relocs = append(relocs, blReloc)
|
|
|
|
|
pc += len(block)
|
|
|
|
|
}
|
2026-08-20 13:32:52 +02:00
|
|
|
return out, offsets, relocs, lines, spadj, nil
|
2026-08-07 22:43:40 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-20 13:32:52 +02:00
|
|
|
// arm64JumpChain precomputes jump-to-jump folding: a label whose first
|
|
|
|
|
// instruction is an unconditional local jump redirects its own jumpers to
|
|
|
|
|
// the ultimate target. The Go toolchain chases these chains before it
|
|
|
|
|
// encodes branches, so matching its bytes requires the same redirection.
|
|
|
|
|
func arm64JumpChain(t *ast.Text) map[string]string {
|
|
|
|
|
leadsTo := map[string]string{}
|
|
|
|
|
for i, stmt := range t.Body {
|
|
|
|
|
l, ok := stmt.(*ast.Label)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
j := i + 1
|
|
|
|
|
for j < len(t.Body) {
|
|
|
|
|
if _, isLabel := t.Body[j].(*ast.Label); !isLabel {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
j++
|
|
|
|
|
}
|
|
|
|
|
if j >= len(t.Body) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
in, ok := t.Body[j].(*ast.Instr)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
mnem := strings.ToUpper(in.Mnemonic.Text)
|
|
|
|
|
if (mnem != "JMP" && mnem != "B") || len(in.Operands) != 1 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if name, ok := arm64LabelOK(in.Operands[0]); ok {
|
|
|
|
|
leadsTo[l.Name.Text] = name
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
chain := map[string]string{}
|
|
|
|
|
for name := range leadsTo {
|
|
|
|
|
visited := map[string]bool{name: true}
|
|
|
|
|
cur := name
|
|
|
|
|
for {
|
|
|
|
|
next, ok := leadsTo[cur]
|
|
|
|
|
if !ok || visited[next] {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
visited[next] = true
|
|
|
|
|
cur = next
|
|
|
|
|
}
|
|
|
|
|
if cur != name {
|
|
|
|
|
chain[name] = cur
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return chain
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// arm64LabelOK returns the local label name of a jump operand.
|
|
|
|
|
func arm64LabelOK(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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// arm64InstrSize returns the encoded size of an instruction: 4 bytes for
|
|
|
|
|
// most, more for the multi-instruction expansions.
|
|
|
|
|
func arm64InstrSize(instr *ast.Instr, fi arm64FrameInfo) int {
|
|
|
|
|
mnem := strings.ToUpper(instr.Mnemonic.Text)
|
|
|
|
|
ops := instr.Operands
|
|
|
|
|
|
|
|
|
|
if mnem == "RET" {
|
|
|
|
|
return len(arm64Return(fi))
|
|
|
|
|
}
|
|
|
|
|
switch mnem {
|
|
|
|
|
case "MOV", "MOVD", "MOVW", "MOVWU", "MOVH", "MOVHU", "MOVB", "MOVBU",
|
|
|
|
|
"FMOVS", "FMOVD":
|
|
|
|
|
return arm64MovSize(mnem, ops, fi)
|
|
|
|
|
case "ADD", "ADDW", "SUB", "SUBW", "AND", "ANDW", "ORR", "ORRW", "EOR", "EORW":
|
|
|
|
|
if len(ops) >= 2 && isImmOperand(ops[0]) {
|
2026-09-19 23:49:07 +02:00
|
|
|
v := arm64Imm64(ops[0])
|
2026-08-20 13:32:52 +02:00
|
|
|
// Small immediate (0..4095 or -2048..-1) fits in one instruction.
|
|
|
|
|
if v >= 0 && v <= 0xFFF {
|
|
|
|
|
return 4
|
|
|
|
|
}
|
|
|
|
|
if v >= -2048 && v < 0 {
|
|
|
|
|
return 4
|
|
|
|
|
}
|
|
|
|
|
// Larger immediates need MOV materialisation + op.
|
|
|
|
|
return 8
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 4
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64Instr encodes a single AArch64 instruction.
|
|
|
|
|
func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64FrameInfo, relocs *[]Reloc, resolve func(string) string) ([]byte, error) {
|
|
|
|
|
mnem := strings.ToUpper(instr.Mnemonic.Text)
|
|
|
|
|
ops := instr.Operands
|
|
|
|
|
|
|
|
|
|
// Pseudo-instructions and special cases first.
|
|
|
|
|
switch mnem {
|
|
|
|
|
case "RET":
|
|
|
|
|
return arm64Return(fi), nil
|
|
|
|
|
case "NOP", "NOOP":
|
|
|
|
|
return a64wordLE(a64NOP), nil
|
|
|
|
|
case "UNDEF":
|
|
|
|
|
return a64wordLE(a64BRK(0)), nil
|
|
|
|
|
case "WORD":
|
|
|
|
|
if len(ops) != 1 {
|
|
|
|
|
return nil, fmt.Errorf("WORD expects 1 operand, got %d", len(ops))
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
w := arm64Imm64(ops[0])
|
|
|
|
|
if w < 0 || w > 0xFFFFFFFF {
|
|
|
|
|
return nil, fmt.Errorf("WORD: immediate %d does not fit a 32-bit word", w)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(uint32(w)), nil
|
2026-09-14 23:25:14 +02:00
|
|
|
case "B", "JMP":
|
2026-08-20 23:22:34 +02:00
|
|
|
return encodeARM64Branch(mnem, ops, pc, offsets, false, relocs, resolve)
|
2026-08-20 13:32:52 +02:00
|
|
|
case "BL", "CALL":
|
2026-08-20 23:22:34 +02:00
|
|
|
return encodeARM64Branch(mnem, ops, pc, offsets, true, relocs, resolve)
|
2026-08-20 13:32:52 +02:00
|
|
|
case "MOV", "MOVD", "MOVW", "MOVWU", "MOVH", "MOVHU", "MOVB", "MOVBU",
|
|
|
|
|
"FMOVS", "FMOVD":
|
|
|
|
|
return encodeARM64Mov(instr, mnem, fi, relocs)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Conditional branches (BEQ, BNE, BGE, BLT, BGT, BLE, etc.).
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FBranchCond {
|
|
|
|
|
return encodeARM64BranchCond(mnem, enc.op, ops, pc, offsets, resolve)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 19:17:07 +02:00
|
|
|
// Unconditional register branches (BR, BLR).
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FUncondBranch {
|
|
|
|
|
return encodeARM64RegBranch(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 13:32:52 +02:00
|
|
|
// ADD/SUB immediate.
|
|
|
|
|
if mnem == "ADD" || mnem == "ADDW" || mnem == "SUB" || mnem == "SUBW" ||
|
|
|
|
|
mnem == "CMP" || mnem == "CMPW" || mnem == "CMN" || mnem == "CMNW" {
|
|
|
|
|
if len(ops) >= 2 && isImmOperand(ops[0]) {
|
|
|
|
|
return encodeARM64AddSubImm(mnem, ops)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:07 +02:00
|
|
|
// Shifts: immediate forms alias SBFM/UBFM/EXTR, register forms are the
|
|
|
|
|
// two-source LSLV/LSRV/ASRV/RORV.
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FShift {
|
|
|
|
|
return encodeARM64Shift(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Multiply-accumulate: MADD/MSUB Rm, Ra, Rn, Rd.
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FDPR4 {
|
|
|
|
|
return encodeARM64MAddSub(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 13:32:52 +02:00
|
|
|
// Register-register data processing.
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FDPSR {
|
|
|
|
|
return encodeARM64DPSR(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 14:07:12 +02:00
|
|
|
// FP 3-operand (Rm, Rn, Rd).
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FFP3 {
|
|
|
|
|
return encodeARM64FP3(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FP unary (Rn, Rd).
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FFPUnary {
|
|
|
|
|
return encodeARM64FPUnary(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FP 4-operand FMA (Ra, Rm, Rn, Rd).
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FFP4 {
|
|
|
|
|
return encodeARM64FP4(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FP compare (Rm, Rn or #0, Rn).
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FFPCmp {
|
|
|
|
|
return encodeARM64FPCmp(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FP conditional compare (Rm, Rn, #nzcv, cond).
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FFPCCmp {
|
|
|
|
|
return encodeARM64FPCCmp(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FP conditional select (Rm, Rn, Rd, cond).
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FFPSel {
|
|
|
|
|
return encodeARM64FPSel(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FP ↔ integer conversion.
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FFPCvt {
|
|
|
|
|
return encodeARM64FPCvt(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Conditional select (CSEL, CSINC, CSINV, CSNEG, CSET, CSETM, CINC, CINV, CNEG).
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FCSEL {
|
|
|
|
|
return encodeARM64CSEL(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CRC32.
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FCRC32 {
|
|
|
|
|
return encodeARM64CRC32(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 00:38:24 +02:00
|
|
|
// Exclusive load/store (LDXR, STXR, LDAXR, STLXR and the register-pair
|
|
|
|
|
// forms LDXP, STXP).
|
2026-08-20 14:31:15 +02:00
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FExcl {
|
|
|
|
|
return encodeARM64Excl(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LSE atomics (LDADD, CAS, SWP).
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FLSE {
|
|
|
|
|
return encodeARM64LSEAtom(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bitfield/shift (ASR, LSL, LSR, ROR, BFI, BFXIL, SBFM, UBFM).
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FBitfield {
|
|
|
|
|
return encodeARM64Bitfield(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// EXTR.
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FEXTR {
|
|
|
|
|
return encodeARM64Extr(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SIMD 3-operand (VADD, VSUB, VMUL).
|
|
|
|
|
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FSIMD3 {
|
|
|
|
|
return encodeARM64SIMD3(mnem, enc.op, ops)
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 13:32:52 +02:00
|
|
|
return nil, fmt.Errorf("unsupported arm64 instruction %q", mnem)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- branch encoding ----
|
|
|
|
|
|
|
|
|
|
// encodeARM64Branch encodes an unconditional branch (B/BL) to a label.
|
2026-08-20 23:22:34 +02:00
|
|
|
func encodeARM64Branch(mnem string, ops []*ast.Operand, pc int, offsets map[string]int, link bool, relocs *[]Reloc, resolve func(string) string) ([]byte, error) {
|
2026-08-20 13:32:52 +02:00
|
|
|
if len(ops) != 1 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
|
|
|
|
|
}
|
2026-08-20 14:31:15 +02:00
|
|
|
op := ops[0]
|
|
|
|
|
|
2026-09-19 19:17:07 +02:00
|
|
|
// Register-indirect: JMP (R0) is BR R0, CALL (R0) is BLR R0. The
|
|
|
|
|
// toolchain's spelling carries no offset and no index; anything else
|
|
|
|
|
// is reported rather than silently dropped.
|
|
|
|
|
if op.Addr.Sym == nil && op.Addr.Base != "" {
|
|
|
|
|
if op.Addr.Offset != 0 || op.Addr.Index != "" {
|
|
|
|
|
return nil, fmt.Errorf("%s: invalid indirect branch operand %q", mnem, op.Raw)
|
|
|
|
|
}
|
|
|
|
|
rn := arm64RegNum(op.Addr.Base)
|
|
|
|
|
if rn < 0 {
|
|
|
|
|
return nil, fmt.Errorf("%s: unknown branch register %q", mnem, op.Addr.Base)
|
|
|
|
|
}
|
|
|
|
|
opc := uint32(0) // BR
|
|
|
|
|
if link {
|
|
|
|
|
opc = 1 // BLR
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(a64UncondBranch(opc, uint32(rn), 0)), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 23:25:14 +02:00
|
|
|
// Symbol reference: BL sym(SB), or B sym(SB) for a tail call, against a
|
|
|
|
|
// relocation (R_CALLARM64 either way).
|
|
|
|
|
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo == "SB" {
|
2026-08-20 23:22:34 +02:00
|
|
|
if relocs != nil {
|
|
|
|
|
*relocs = append(*relocs, Reloc{
|
|
|
|
|
Off: 0,
|
|
|
|
|
After: 4,
|
|
|
|
|
Name: op.Addr.Sym.Name,
|
|
|
|
|
Addend: op.Addr.Sym.Offset,
|
|
|
|
|
Kind: RelArm64Branch,
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-09-14 23:25:14 +02:00
|
|
|
// Emit B/BL with zero offset; the linker fills in the target.
|
|
|
|
|
bop := uint32(0) // B
|
|
|
|
|
if link {
|
|
|
|
|
bop = 1 // BL
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(a64Branch(bop, 0)), nil
|
2026-08-20 14:31:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
target := resolve(arm64Label(op))
|
2026-08-20 13:32:52 +02:00
|
|
|
targetOff, ok := offsets[target]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("undefined label %q", target)
|
|
|
|
|
}
|
|
|
|
|
rel := (targetOff - pc) >> 2
|
|
|
|
|
if rel < -(1<<25) || rel >= (1<<25) {
|
|
|
|
|
return nil, fmt.Errorf("branch to %q too far (26-bit range)", target)
|
|
|
|
|
}
|
2026-08-20 14:31:15 +02:00
|
|
|
bop := uint32(0) // B
|
2026-08-20 13:32:52 +02:00
|
|
|
if link {
|
2026-08-20 14:31:15 +02:00
|
|
|
bop = 1 // BL
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
2026-08-20 14:31:15 +02:00
|
|
|
return a64wordLE(a64Branch(bop, int32(rel))), nil
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-19 19:17:07 +02:00
|
|
|
// encodeARM64RegBranch encodes BR/BLR through a register operand:
|
|
|
|
|
// BR Xn = 0xd61f0000 | Rn<<5, BLR Xn = 0xd63f0000 | Rn<<5.
|
|
|
|
|
func encodeARM64RegBranch(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 1 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
if rn < 0 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects a register operand", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(uint32(baseOp) | 31<<16 | uint32(rn)<<5), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 13:32:52 +02:00
|
|
|
// encodeARM64BranchCond encodes a conditional branch (B.cond) to a label.
|
|
|
|
|
func encodeARM64BranchCond(mnem string, baseOp uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) {
|
|
|
|
|
if len(ops) != 1 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
target := resolve(arm64Label(ops[0]))
|
|
|
|
|
targetOff, ok := offsets[target]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("undefined label %q", target)
|
|
|
|
|
}
|
|
|
|
|
rel := (targetOff - pc) >> 2
|
|
|
|
|
if rel < -(1<<18) || rel >= (1<<18) {
|
|
|
|
|
return nil, fmt.Errorf("branch to %q too far (19-bit range)", target)
|
|
|
|
|
}
|
|
|
|
|
// The condition code is in the low 4 bits of baseOp.
|
|
|
|
|
cond := baseOp & 0xF
|
|
|
|
|
return a64wordLE(a64BranchCond(int32(rel), cond)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- data-processing (shifted register) ----
|
|
|
|
|
|
|
|
|
|
// encodeARM64DPSR encodes a data-processing (shifted register) instruction.
|
|
|
|
|
// For most instructions: OP Rm, Rn, Rd (3 operands) or OP Rm, Rd (2 operands, Rn=Rd).
|
|
|
|
|
// For CMP/CMN/TST: CMP Rm, Rn (Rd=ZR).
|
|
|
|
|
// For NEG: NEG Rm, Rd (Rn=ZR).
|
|
|
|
|
func encodeARM64DPSR(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
isCmp := mnem == "CMP" || mnem == "CMPW" || mnem == "CMN" || mnem == "CMNW" || mnem == "TST" || mnem == "TSTW"
|
|
|
|
|
isNeg := mnem == "NEG" || mnem == "NEGW" || mnem == "MVN" || mnem == "MVNW"
|
|
|
|
|
|
|
|
|
|
switch len(ops) {
|
|
|
|
|
case 3:
|
|
|
|
|
// OP Rm, Rn, Rd
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
if rm < 0 || rn < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
case 2:
|
|
|
|
|
if isCmp {
|
|
|
|
|
// CMP Rm, Rn → SUBS XZR, Rn, Rm
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
if rm < 0 || rn < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rn)<<5 | 31), nil
|
|
|
|
|
}
|
|
|
|
|
if isNeg {
|
|
|
|
|
// NEG Rm, Rd → SUB Rd, ZR, Rm
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
if rm < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | 31<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
// OP Rm, Rd → OP Rm, Rd, Rd
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
if rm < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rd)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
return nil, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:07 +02:00
|
|
|
// encodeARM64Shift encodes LSL/LSR/ASR/ROR in both widths. The operand order
|
|
|
|
|
// is source first, destination last: OP $sh|Rm, Rn, Rd or OP $sh|Rm, Rd.
|
|
|
|
|
// With an immediate the shift is the SBFM/UBFM (ROR: EXTR) alias, with a
|
|
|
|
|
// register it is the data-processing (2 source) LSLV/LSRV/ASRV/RORV; the
|
|
|
|
|
// two-source opcode rides the same 0xd6<<21 field as SDIV/UDIV, with
|
|
|
|
|
// LSLV=0b001000, LSRV=0b001001, ASRV=0b001010, RORV=0b001011 at bits 15:10.
|
|
|
|
|
func encodeARM64Shift(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 2 && len(ops) != 3 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[len(ops)-1]))
|
|
|
|
|
if rn < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if isImmOperand(ops[0]) {
|
|
|
|
|
width := uint32(64)
|
|
|
|
|
if strings.HasSuffix(mnem, "W") {
|
|
|
|
|
width = 32
|
|
|
|
|
}
|
|
|
|
|
sh := arm64Imm64(ops[0])
|
|
|
|
|
if sh < 0 || uint32(sh) >= width {
|
|
|
|
|
return nil, fmt.Errorf("%s: shift amount %d out of range for %d-bit form", mnem, sh, width)
|
|
|
|
|
}
|
|
|
|
|
switch mnem {
|
|
|
|
|
case "LSL", "LSLW":
|
|
|
|
|
// UBFM Rd, Rn, #(-sh) mod W, #(W-1)-sh
|
|
|
|
|
immr := (width - uint32(sh)) % width
|
|
|
|
|
return a64wordLE(baseOp | immr<<16 | (width-1-uint32(sh))<<10 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
case "LSR", "LSRW":
|
|
|
|
|
// UBFM Rd, Rn, #sh, #(W-1)
|
|
|
|
|
return a64wordLE(baseOp | uint32(sh)<<16 | (width-1)<<10 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
case "ASR", "ASRW":
|
|
|
|
|
// SBFM Rd, Rn, #sh, #(W-1)
|
|
|
|
|
return a64wordLE(baseOp | uint32(sh)<<16 | (width-1)<<10 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
default:
|
|
|
|
|
// ROR, RORW: EXTR Rd, Rn, Rn, #sh (Rm = Rn, imms = sh).
|
|
|
|
|
return a64wordLE(baseOp | uint32(rn)<<16 | uint32(sh)<<10 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
if rm < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
op2 := uint32(8) // LSLV
|
|
|
|
|
switch mnem {
|
|
|
|
|
case "LSR", "LSRW":
|
|
|
|
|
op2 = 9 // LSRV
|
|
|
|
|
case "ASR", "ASRW":
|
|
|
|
|
op2 = 10 // ASRV
|
|
|
|
|
case "ROR", "RORW":
|
|
|
|
|
op2 = 11 // RORV
|
|
|
|
|
}
|
|
|
|
|
sf := uint32(1)
|
|
|
|
|
if strings.HasSuffix(mnem, "W") {
|
|
|
|
|
sf = 0
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(sf<<31 | 0xd6<<21 | op2<<10 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64MAddSub encodes MADD/MSUB/MADDW/MSUBW. The toolchain's operand
|
|
|
|
|
// order is Rm, Ra, Rn, Rd (its optab case 15 comment says exactly that), so
|
|
|
|
|
// the accumulate register is the SECOND operand: base | Rm<<16 | Ra<<10 |
|
|
|
|
|
// Rn<<5 | Rd. The optab has no shorter row for these mnemonics, so all four
|
|
|
|
|
// operands are mandatory; MUL's two-operand spelling (Ra = ZR) belongs to the
|
|
|
|
|
// MUL mnemonic, not to these.
|
|
|
|
|
func encodeARM64MAddSub(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 4 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 4 operands (Rm, Ra, Rn, Rd), got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
ra := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[3]))
|
|
|
|
|
if rm < 0 || rn < 0 || ra < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(ra)<<10 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 13:32:52 +02:00
|
|
|
// ---- ADD/SUB immediate ----
|
|
|
|
|
|
|
|
|
|
// encodeARM64AddSubImm encodes an ADD/SUB immediate instruction.
|
|
|
|
|
func encodeARM64AddSubImm(mnem string, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 2 && len(ops) != 3 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
v := arm64Imm64(ops[0])
|
2026-08-20 13:32:52 +02:00
|
|
|
rd := arm64RegNum(operandRegName(ops[len(ops)-1]))
|
|
|
|
|
rn := rd
|
|
|
|
|
if len(ops) == 3 {
|
|
|
|
|
rn = arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
}
|
|
|
|
|
if rn < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isSub := mnem == "SUB" || mnem == "SUBW" || mnem == "CMP" || mnem == "CMPW"
|
|
|
|
|
isS := mnem == "CMP" || mnem == "CMPW" || mnem == "CMN" || mnem == "CMNW"
|
|
|
|
|
sf := uint32(1) // 64-bit
|
|
|
|
|
if mnem == "ADDW" || mnem == "SUBW" || mnem == "CMPW" || mnem == "CMNW" {
|
|
|
|
|
sf = 0 // 32-bit
|
|
|
|
|
}
|
|
|
|
|
if mnem == "CMP" || mnem == "CMPW" {
|
|
|
|
|
rd = 31 // ZR
|
|
|
|
|
}
|
|
|
|
|
if mnem == "CMN" || mnem == "CMNW" {
|
|
|
|
|
rd = 31 // ZR
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
op := uint32(0) // ADD
|
|
|
|
|
S := uint32(0)
|
|
|
|
|
if isSub {
|
|
|
|
|
op = 1
|
|
|
|
|
}
|
|
|
|
|
if isS {
|
|
|
|
|
S = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if v >= 0 && v <= 0xFFF {
|
|
|
|
|
return a64wordLE(a64AddSub(sf, op, S, 0, uint32(v), uint32(rn), uint32(rd))), nil
|
|
|
|
|
}
|
|
|
|
|
if v >= -2048 && v < 0 {
|
|
|
|
|
// Encode as the opposite operation with positive immediate.
|
|
|
|
|
opp := op ^ 1
|
|
|
|
|
return a64wordLE(a64AddSub(sf, opp, S, 0, uint32(-v), uint32(rn), uint32(rd))), nil
|
|
|
|
|
}
|
|
|
|
|
// Try with shift by 12.
|
|
|
|
|
if v >= 0 && v <= 0xFFF000 && v&0xFFF == 0 {
|
|
|
|
|
return a64wordLE(a64AddSub(sf, op, S, 1, uint32(v>>12), uint32(rn), uint32(rd))), nil
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
// The imm12 field cannot carry the value; rejecting (rather than
|
|
|
|
|
// truncating) matches the toolchain, which reports the same shape.
|
2026-08-20 13:32:52 +02:00
|
|
|
return nil, fmt.Errorf("%s: immediate %d out of range for single instruction", mnem, v)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- MOV pseudo-instruction ----
|
|
|
|
|
|
2026-09-14 18:22:00 +02:00
|
|
|
// encodeARM64Mov encodes the MOV family, the load/store/immediate workhorse
|
2026-08-20 13:32:52 +02:00
|
|
|
// of Go's arm64 assembly. MOV is an alias of MOVD (the width mnemonics
|
|
|
|
|
// select the access width). The forms, mirroring the toolchain:
|
|
|
|
|
//
|
|
|
|
|
// MOVx $imm, rd load immediate (MOVZ/MOVN/MOVK)
|
|
|
|
|
// MOVx mem, rd load from memory
|
|
|
|
|
// MOVx rd, mem store to memory
|
|
|
|
|
// MOVx rs, rd register move (ORR Rd, ZR, Rs)
|
|
|
|
|
// MOVx $sym(SB), rd address of a static symbol (ADRP+ADD)
|
|
|
|
|
// MOVx sym(SB), rd load from a static symbol (ADRP+LDR)
|
|
|
|
|
// MOVx rd, sym(SB) store to a static symbol (ADRP+STR)
|
|
|
|
|
func encodeARM64Mov(instr *ast.Instr, mnem string, fi arm64FrameInfo, relocs *[]Reloc) ([]byte, error) {
|
|
|
|
|
ops := instr.Operands
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
src, dst := ops[0], ops[1]
|
|
|
|
|
|
|
|
|
|
// Immediate → register (including $sym(SB)).
|
|
|
|
|
if isImmOperand(src) && !isMemOperand(src) {
|
|
|
|
|
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo == "SB" {
|
|
|
|
|
rd := arm64RegNum(operandRegName(dst))
|
|
|
|
|
if rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("%s $sym(SB): invalid destination register", mnem)
|
|
|
|
|
}
|
|
|
|
|
return encodeARM64SBAddr(src.Imm.Sym, rd, relocs), nil
|
|
|
|
|
}
|
|
|
|
|
rd := arm64RegNum(operandRegName(dst))
|
|
|
|
|
if rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("%s $imm: invalid destination register", mnem)
|
|
|
|
|
}
|
2026-08-20 14:31:15 +02:00
|
|
|
return encodeARM64LoadImm(rd, arm64Imm64(src), mnem)
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Static symbol load/store via ADRP.
|
|
|
|
|
if src.Addr.Sym != nil && src.Addr.Sym.Pseudo == "SB" && isMemOperand(src) {
|
|
|
|
|
rd := arm64RegNum(operandRegName(dst))
|
|
|
|
|
if rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("%s sym(SB): invalid destination register", mnem)
|
|
|
|
|
}
|
|
|
|
|
return encodeARM64SBLoad(src.Addr.Sym, rd, mnem, relocs)
|
|
|
|
|
}
|
|
|
|
|
if dst.Addr.Sym != nil && dst.Addr.Sym.Pseudo == "SB" && isMemOperand(dst) {
|
|
|
|
|
rs := arm64RegNum(operandRegName(src))
|
|
|
|
|
if rs < 0 {
|
|
|
|
|
return nil, fmt.Errorf("%s rd, sym(SB): invalid source register", mnem)
|
|
|
|
|
}
|
|
|
|
|
return encodeARM64SBStore(dst.Addr.Sym, rs, mnem, relocs)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Memory load/store with offset.
|
|
|
|
|
if isMemOperand(src) && !isMemOperand(dst) {
|
|
|
|
|
rd := arm64RegNum(operandRegName(dst))
|
|
|
|
|
if rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("%s: invalid destination register", mnem)
|
|
|
|
|
}
|
|
|
|
|
return encodeARM64MemOp(mnem, src, rd, true, fi)
|
|
|
|
|
}
|
|
|
|
|
if !isMemOperand(src) && isMemOperand(dst) {
|
|
|
|
|
rs := arm64RegNum(operandRegName(src))
|
|
|
|
|
if rs < 0 {
|
|
|
|
|
return nil, fmt.Errorf("%s: invalid source register", mnem)
|
|
|
|
|
}
|
|
|
|
|
return encodeARM64MemOp(mnem, dst, rs, false, fi)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Register → register.
|
|
|
|
|
return encodeARM64RegMove(mnem, src, dst)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// arm64MovSize returns the encoded size of a MOV instruction.
|
|
|
|
|
func arm64MovSize(mnem string, ops []*ast.Operand, fi arm64FrameInfo) int {
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
return 4
|
|
|
|
|
}
|
|
|
|
|
src, dst := ops[0], ops[1]
|
|
|
|
|
switch {
|
|
|
|
|
case isImmOperand(src):
|
|
|
|
|
if src.Imm.Sym != nil && src.Imm.Sym.Pseudo == "SB" {
|
|
|
|
|
return 8 // ADRP + ADD
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
// Size the immediate exactly as the encoder will emit it: multi-chunk
|
|
|
|
|
// values expand to up to four words and the W forms truncate first.
|
|
|
|
|
// Anything else would desynchronise the label offsets of pass 1 from
|
|
|
|
|
// the bytes pass 2 lays down, corrupting every later branch.
|
|
|
|
|
b, err := encodeARM64LoadImm(31, arm64Imm64(src), mnem)
|
|
|
|
|
if err != nil {
|
2026-08-20 13:32:52 +02:00
|
|
|
return 4
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
return len(b)
|
2026-08-20 13:32:52 +02:00
|
|
|
case src.Addr.Sym != nil && src.Addr.Sym.Pseudo == "SB":
|
|
|
|
|
return 8 // ADRP + LDR
|
|
|
|
|
case dst.Addr.Sym != nil && dst.Addr.Sym.Pseudo == "SB":
|
|
|
|
|
return 8 // ADRP + STR
|
|
|
|
|
case isMemOperand(src) || isMemOperand(dst):
|
|
|
|
|
mem := src
|
|
|
|
|
if !isMemOperand(src) {
|
|
|
|
|
mem = dst
|
|
|
|
|
}
|
|
|
|
|
_, off := arm64MemWithFrame(mem, fi)
|
|
|
|
|
// Scaled unsigned offset fits if aligned and in range.
|
2026-09-14 23:25:14 +02:00
|
|
|
lt, ok := a64LoadTable[mnem]
|
|
|
|
|
if !ok {
|
|
|
|
|
lt = a64LoadTable["MOVD"] // the MOV pseudo is a 64-bit access
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
scale := int64(1) << uint(lt.size)
|
2026-08-20 13:32:52 +02:00
|
|
|
if off >= 0 && off%scale == 0 && off/scale < 4096 {
|
|
|
|
|
return 4
|
|
|
|
|
}
|
|
|
|
|
if off >= -256 && off <= 255 {
|
|
|
|
|
return 4 // unscaled
|
|
|
|
|
}
|
2026-09-14 23:25:14 +02:00
|
|
|
if _, _, _, ok := arm64SplitOffset(off, scale); ok {
|
|
|
|
|
return 8 // ADD base, REGTMP + access
|
|
|
|
|
}
|
|
|
|
|
return 12 // literal pool range: encoding reports it as unsupported
|
2026-08-20 13:32:52 +02:00
|
|
|
default:
|
|
|
|
|
return 4 // register move
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64LoadImm loads an immediate into a register, matching the
|
2026-09-19 23:49:07 +02:00
|
|
|
// toolchain's MOVZ/MOVN/MOVK sequence. W forms truncate to 32 bits first and
|
|
|
|
|
// every classification (movcon, complement, chunk count) runs on the truncated
|
|
|
|
|
// value, so a 32-bit immediate never reaches the 64-bit halves: MOVW $-1
|
|
|
|
|
// truncates to 0xFFFFFFFF, whose complement is a single zero chunk, and encodes
|
|
|
|
|
// as MOVN W, #0.
|
2026-08-20 14:31:15 +02:00
|
|
|
func encodeARM64LoadImm(rd int, v int64, mnem string) ([]byte, error) {
|
|
|
|
|
d := v
|
2026-09-19 23:49:07 +02:00
|
|
|
sf := uint32(1) // 64-bit
|
2026-08-20 13:32:52 +02:00
|
|
|
if mnem == "MOVW" || mnem == "MOVWU" {
|
|
|
|
|
d = int64(uint32(v))
|
2026-09-19 23:49:07 +02:00
|
|
|
sf = 0
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if d == 0 {
|
|
|
|
|
// ORR Rd, ZR, ZR (MOV $0, Rd)
|
|
|
|
|
op := uint32(1<<31 | 1<<29 | 0x0a<<24) // ORR 64-bit
|
2026-09-19 23:49:07 +02:00
|
|
|
if sf == 0 {
|
2026-08-20 13:32:52 +02:00
|
|
|
op = 0<<31 | 1<<29 | 0x0a<<24 // ORR 32-bit
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(op | 31<<16 | 31<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 00:38:24 +02:00
|
|
|
// The Go toolchain classifies immediates (asm7.go conclass):
|
|
|
|
|
// - inside the imm12/shifted-imm12 "addcon" band (C_ABCON0/C_ABCON,
|
|
|
|
|
// 0 < v ≤ 4095 or a 4096 multiple up to 0xFFF000): bitmask first, so
|
|
|
|
|
// `MOVD $4096, R27` is ORR $4096, not MOVZ $(1<<12)
|
|
|
|
|
// - outside that band: MOVZ/MOVN first (C_MOVCON before C_BITCON), and
|
|
|
|
|
// negative values reach MOVN before the bitmask test
|
|
|
|
|
tryBitmaskFirst := d > 0 && (d <= 0xFFF || (d&0xFFF == 0 && d <= 0xFFF000))
|
2026-08-20 14:31:15 +02:00
|
|
|
|
|
|
|
|
if tryBitmaskFirst {
|
2026-09-20 00:38:24 +02:00
|
|
|
// Addcon-band immediate: try bitmask first (Go uses ORR for values
|
|
|
|
|
// like $1, $256 and $65536).
|
2026-08-20 14:31:15 +02:00
|
|
|
N, immr, imms, ok := arm64Bitmask(uint64(d), int(sf))
|
|
|
|
|
if ok {
|
|
|
|
|
return a64wordLE(sf<<31 | 1<<29 | 0x24<<23 | N<<22 | immr<<16 | imms<<10 | 31<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:07 +02:00
|
|
|
// Try MOVZ (single non-zero 16-bit chunk) and MOVN (single non-0xFFFF
|
|
|
|
|
// chunk of the complement). The W forms must look inside the 32-bit
|
|
|
|
|
// window only, so the complement is masked to the operand width; d is
|
|
|
|
|
// already truncated and needs no mask.
|
|
|
|
|
width := uint64(0xFFFFFFFF)
|
|
|
|
|
if sf == 1 {
|
|
|
|
|
width = 0xFFFFFFFFFFFFFFFF
|
|
|
|
|
}
|
2026-08-20 13:32:52 +02:00
|
|
|
s := arm64Movcon(d)
|
|
|
|
|
if s >= 0 {
|
|
|
|
|
return a64wordLE(a64MoveWide(sf, 2, uint32(s>>4), uint32((d>>uint(s))&0xFFFF), uint32(rd))), nil
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
sn := arm64Movcon(^d & int64(width))
|
2026-08-20 13:32:52 +02:00
|
|
|
if sn >= 0 {
|
2026-09-19 23:49:07 +02:00
|
|
|
return a64wordLE(a64MoveWide(sf, 0, uint32(sn>>4), uint32(((^d)>>uint(sn))&0xFFFF), uint32(rd))), nil
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-20 14:31:15 +02:00
|
|
|
// For values outside the bitmask-first range that are not movcon: try bitmask.
|
|
|
|
|
if !tryBitmaskFirst {
|
|
|
|
|
N, immr, imms, ok := arm64Bitmask(uint64(d), int(sf))
|
|
|
|
|
if ok {
|
|
|
|
|
return a64wordLE(sf<<31 | 1<<29 | 0x24<<23 | N<<22 | immr<<16 | imms<<10 | 31<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:07 +02:00
|
|
|
// Multi-instruction: MOVZ + MOVK for each non-zero 16-bit chunk.
|
2026-08-20 13:32:52 +02:00
|
|
|
var ws []uint32
|
|
|
|
|
first := true
|
2026-08-29 15:40:31 +02:00
|
|
|
for i := range 4 {
|
2026-08-20 13:32:52 +02:00
|
|
|
chunk := (d >> uint(i*16)) & 0xFFFF
|
|
|
|
|
if chunk == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if first {
|
|
|
|
|
ws = append(ws, a64MoveWide(sf, 2, uint32(i), uint32(chunk), uint32(rd))) // MOVZ
|
|
|
|
|
first = false
|
|
|
|
|
} else {
|
|
|
|
|
ws = append(ws, a64MoveWide(sf, 3, uint32(i), uint32(chunk), uint32(rd))) // MOVK
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(ws) == 0 {
|
|
|
|
|
op := uint32(1<<31 | 1<<29 | 0x0a<<24)
|
|
|
|
|
return a64wordLE(op | 31<<16 | 31<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
return a64WordsLE(ws...), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// arm64Bitmask checks whether a value can be encoded as an AArch64 logical
|
|
|
|
|
// immediate (bitmask). Returns the N, immr, imms fields and true if
|
|
|
|
|
// representable. sf is 0 for 32-bit or 1 for 64-bit.
|
|
|
|
|
func arm64Bitmask(v uint64, sf int) (N, immr, imms uint32, ok bool) {
|
|
|
|
|
if v == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
maxElem := uint(6) // 2^6 = 64
|
|
|
|
|
if sf == 0 {
|
|
|
|
|
maxElem = 5 // 2^5 = 32
|
|
|
|
|
v &= 0xFFFFFFFF
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for e := uint(0); e < maxElem; e++ {
|
|
|
|
|
esize := uint(1) << (e + 1) // 2, 4, 8, 16, 32, 64
|
|
|
|
|
emask := uint64(1<<esize) - 1
|
|
|
|
|
pattern := v & emask
|
|
|
|
|
if pattern == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check each rotation: is the rotated pattern a contiguous block of 1s at the LSB?
|
2026-08-29 15:40:31 +02:00
|
|
|
for r := range esize {
|
2026-08-20 13:32:52 +02:00
|
|
|
rotated := (pattern >> r) | ((pattern << (esize - r)) & emask)
|
|
|
|
|
if rotated == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// Count trailing 1s (contiguous block of 1s from bit 0).
|
|
|
|
|
tz := uint(0)
|
|
|
|
|
tmp := ^rotated
|
|
|
|
|
for tmp&1 == 0 && tz < esize {
|
|
|
|
|
tz++
|
|
|
|
|
tmp >>= 1
|
|
|
|
|
}
|
|
|
|
|
if tz == 0 || tz >= esize {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
mask := uint64(1<<tz) - 1
|
|
|
|
|
if rotated != mask {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
ones := tz
|
|
|
|
|
|
|
|
|
|
// Verify the pattern repeats to fill the register.
|
|
|
|
|
full := uint64(0)
|
|
|
|
|
for i := uint(0); i < 64/esize; i++ {
|
|
|
|
|
full |= pattern << (i * esize)
|
|
|
|
|
}
|
|
|
|
|
if sf == 0 {
|
|
|
|
|
full &= 0xFFFFFFFF
|
|
|
|
|
}
|
|
|
|
|
if full != v {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Encode N, immr, imms.
|
|
|
|
|
if esize == 64 && sf == 1 {
|
|
|
|
|
N = 1
|
|
|
|
|
} else {
|
|
|
|
|
N = 0
|
|
|
|
|
}
|
|
|
|
|
imms = uint32((^(esize - 1))&0x3F) | uint32(ones-1)
|
2026-08-20 14:31:15 +02:00
|
|
|
immr = uint32((esize - r) % esize)
|
2026-08-20 13:32:52 +02:00
|
|
|
return N, immr, imms, true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 14:07:12 +02:00
|
|
|
// encodeARM64RegMove encodes a register-to-register move.
|
|
|
|
|
// Integer → integer: ORR Rd, ZR, Rs.
|
|
|
|
|
// FP → FP: FMOV Fd, Fn (FP data processing).
|
|
|
|
|
// FP ↔ GP: FMOV general (FPCVTI encoding).
|
2026-09-19 23:49:07 +02:00
|
|
|
// Go Plan 9 syntax is source first, destination last: MOV src, dst.
|
2026-08-20 13:32:52 +02:00
|
|
|
func encodeARM64RegMove(mnem string, src, dst *ast.Operand) ([]byte, error) {
|
|
|
|
|
rs := arm64RegNum(operandRegName(src))
|
|
|
|
|
rd := arm64RegNum(operandRegName(dst))
|
|
|
|
|
if rs < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
sc := arm64RegClassOf(operandRegName(src))
|
|
|
|
|
dc := arm64RegClassOf(operandRegName(dst))
|
|
|
|
|
|
2026-08-20 14:07:12 +02:00
|
|
|
// FP → FP: FMOV Fd, Fn (FP data processing unary form).
|
2026-08-20 13:32:52 +02:00
|
|
|
if sc == arm64ClsFP && dc == arm64ClsFP {
|
|
|
|
|
typ := uint32(1) // 64-bit double
|
|
|
|
|
if mnem == "FMOVS" {
|
|
|
|
|
typ = 0 // 32-bit float
|
|
|
|
|
}
|
2026-08-20 14:07:12 +02:00
|
|
|
// FPOP1S encoding: 0x1E204000 | type<<22 | Rn<<5 | Rd
|
|
|
|
|
return a64wordLE(0x1E<<24 | typ<<22 | 1<<21 | 0x10<<10 | uint32(rs)<<5 | uint32(rd)), nil
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-20 14:07:12 +02:00
|
|
|
// GP ↔ FP: FMOV general (FPCVTI encoding).
|
2026-09-19 23:49:07 +02:00
|
|
|
// Go syntax: FMOV GPsrc, FPdst or FMOV FPsrc, GPdst, source first.
|
2026-08-20 14:07:12 +02:00
|
|
|
if sc == arm64ClsFP && dc == arm64ClsGR {
|
|
|
|
|
// FP → GP: FMOV Wd/Xd, Sn/Dn. opcode bits[20:16]=6.
|
|
|
|
|
sf, typ := uint32(0), uint32(0)
|
|
|
|
|
if mnem == "FMOVD" {
|
|
|
|
|
sf, typ = 1, 1
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(sf<<31 | 0x1E<<24 | typ<<22 | 1<<21 | 6<<16 | uint32(rs)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
if sc == arm64ClsGR && dc == arm64ClsFP {
|
|
|
|
|
// GP → FP: FMOV Vd, Wn/Xn. opcode bits[20:16]=7.
|
|
|
|
|
sf, typ := uint32(0), uint32(0)
|
|
|
|
|
if mnem == "FMOVD" {
|
|
|
|
|
sf, typ = 1, 1
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(sf<<31 | 0x1E<<24 | typ<<22 | 1<<21 | 7<<16 | uint32(rs)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Integer → integer: ORR Rd, ZR, Rs.
|
2026-08-20 13:32:52 +02:00
|
|
|
sf := uint32(1) // 64-bit
|
|
|
|
|
if mnem == "MOVW" || mnem == "MOVWU" || mnem == "MOVB" || mnem == "MOVBU" ||
|
|
|
|
|
mnem == "MOVH" || mnem == "MOVHU" {
|
|
|
|
|
sf = 0
|
|
|
|
|
}
|
|
|
|
|
op := uint32(1<<29 | 0x0a<<24) // ORR
|
|
|
|
|
return a64wordLE(sf<<31 | op | uint32(rs)<<16 | 31<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64MemOp encodes a memory load or store with offset.
|
|
|
|
|
func encodeARM64MemOp(mnem string, mem *ast.Operand, reg int, load bool, fi arm64FrameInfo) ([]byte, error) {
|
|
|
|
|
rn, off := arm64MemWithFrame(mem, fi)
|
|
|
|
|
if rn < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid memory operand")
|
|
|
|
|
}
|
|
|
|
|
lt, ok := a64LoadTable[mnem]
|
|
|
|
|
if !ok {
|
|
|
|
|
// MOV defaults to MOVD (64-bit load/store).
|
|
|
|
|
lt = a64LoadTable["MOVD"]
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:07 +02:00
|
|
|
scale := int64(1) << uint(lt.size)
|
2026-08-20 13:32:52 +02:00
|
|
|
storeOpc := a64StoreOpc(lt)
|
2026-09-14 23:25:14 +02:00
|
|
|
var opc int
|
|
|
|
|
if load {
|
|
|
|
|
opc = lt.opc
|
|
|
|
|
} else {
|
|
|
|
|
opc = storeOpc
|
|
|
|
|
}
|
|
|
|
|
// Scaled unsigned offset first, then the unscaled ±255 form.
|
|
|
|
|
if off >= 0 && off%scale == 0 && off/scale < 4096 {
|
|
|
|
|
return a64wordLE(a64LSU(uint32(lt.size), uint32(lt.V), uint32(opc), uint32(off/scale), uint32(rn), uint32(reg))), nil
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
|
|
|
|
if off >= -256 && off <= 255 {
|
2026-09-19 23:49:07 +02:00
|
|
|
return a64wordLE(a64LSUnscaled(lt.size, lt.V, opc, int32(off), rn, reg)), nil
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
2026-09-14 23:25:14 +02:00
|
|
|
// Large offset: materialise the base in REGTMP (R27) the way the
|
2026-09-19 23:49:07 +02:00
|
|
|
// toolchain does and access what remains. The ADD offsets from the
|
|
|
|
|
// operand's own base register, [SP] and [Rn] alike.
|
2026-09-14 23:25:14 +02:00
|
|
|
addImm, addShift, access, ok := arm64SplitOffset(off, scale)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("%s: offset %d out of range (literal pool not supported)", mnem, off)
|
|
|
|
|
}
|
|
|
|
|
return a64WordsLE(
|
2026-09-19 23:49:07 +02:00
|
|
|
a64AddSub(1, 0, 0, addShift, addImm, uint32(rn), 27), // ADD $addImm<<shift, Rn, R27
|
2026-09-14 23:25:14 +02:00
|
|
|
a64LSU(uint32(lt.size), uint32(lt.V), uint32(opc), uint32(access/scale), 27, uint32(reg)),
|
|
|
|
|
), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:07 +02:00
|
|
|
// arm64SplitOffset decomposes an out-of-range offset for a REGTMP base: an
|
|
|
|
|
// ADD (plain, or shifted left by 12) brings the base near the target and the
|
|
|
|
|
// access covers what remains. ok is false when no decomposition exists
|
|
|
|
|
// (negative offsets, or beyond 16 MiB, where the toolchain falls back to a
|
|
|
|
|
// literal pool).
|
|
|
|
|
func arm64SplitOffset(off int64, scale int64) (addImm, addShift uint32, access int64, ok bool) {
|
2026-09-14 23:25:14 +02:00
|
|
|
if off < 0 {
|
|
|
|
|
return 0, 0, 0, false
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
// Plain ADD: bring the base to within the largest scaled access.
|
2026-09-14 23:25:14 +02:00
|
|
|
l := min(off, 4095*scale)
|
|
|
|
|
l -= l % scale
|
|
|
|
|
if a := off - l; a <= 4095 {
|
|
|
|
|
return uint32(a), 0, l, true
|
|
|
|
|
}
|
|
|
|
|
// Shifted ADD: cover everything but the bits the access imm12 carries.
|
|
|
|
|
rest := off &^ (0xFFF * scale)
|
|
|
|
|
if rest >= 0 && rest>>12 <= 4095 {
|
|
|
|
|
return uint32(rest >> 12), 1, off - rest, true
|
|
|
|
|
}
|
|
|
|
|
return 0, 0, 0, false
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- static symbol references (ADRP + offset) ----
|
|
|
|
|
|
|
|
|
|
// encodeARM64SBAddr emits ADRP Rd, 0; ADD Rd, Rd, 0 with the
|
|
|
|
|
// R_ADDRARM64 relocation pair, loading a symbol's address.
|
|
|
|
|
func encodeARM64SBAddr(sym *ast.Symbol, rd int, relocs *[]Reloc) []byte {
|
|
|
|
|
if relocs != nil {
|
|
|
|
|
*relocs = append(*relocs,
|
|
|
|
|
Reloc{Off: 0, After: 0, Name: sym.Name, Kind: RelArm64Addr, Addend: sym.Offset},
|
|
|
|
|
Reloc{Off: 4, After: 4, Name: sym.Name, Kind: RelArm64Addr, Addend: sym.Offset},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return a64WordsLE(
|
|
|
|
|
a64ADR(1, 0, 0, uint32(rd)), // ADRP Rd, 0
|
|
|
|
|
a64AddSub(1, 0, 0, 0, 0, uint32(rd), uint32(rd)), // ADD $0, Rd, Rd
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 18:22:00 +02:00
|
|
|
// encodeARM64SBLoad emits ADRP R27, 0; LDR Rd, [R27, 0] with relocations,
|
|
|
|
|
// matching the toolchain: the scratch register is REGTMP (R27) and the pair
|
|
|
|
|
// carries R_ARM64_PCREL_LDST64.
|
2026-08-20 13:32:52 +02:00
|
|
|
func encodeARM64SBLoad(sym *ast.Symbol, rd int, mnem string, relocs *[]Reloc) ([]byte, error) {
|
|
|
|
|
lt, ok := a64LoadTable[mnem]
|
|
|
|
|
if !ok {
|
|
|
|
|
lt = a64LoadTable["MOVD"]
|
|
|
|
|
}
|
|
|
|
|
if relocs != nil {
|
|
|
|
|
*relocs = append(*relocs,
|
2026-09-14 18:22:00 +02:00
|
|
|
Reloc{Off: 0, After: 8, Name: sym.Name, Kind: RelArm64LDST64, Addend: sym.Offset},
|
2026-08-20 13:32:52 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return a64WordsLE(
|
2026-09-14 18:22:00 +02:00
|
|
|
a64ADR(1, 0, 0, 27), // ADRP R27, 0
|
|
|
|
|
a64LSU(uint32(lt.size), uint32(lt.V), uint32(lt.opc), 0, 27, uint32(rd)), // LDR Rd, [R27, #0]
|
2026-08-20 13:32:52 +02:00
|
|
|
), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 18:22:00 +02:00
|
|
|
// encodeARM64SBStore emits ADRP R27, 0; STR Rs, [R27, 0] with relocations,
|
|
|
|
|
// matching the toolchain's R27 scratch and R_ARM64_PCREL_LDST64 pair.
|
2026-08-20 13:32:52 +02:00
|
|
|
func encodeARM64SBStore(sym *ast.Symbol, rs int, mnem string, relocs *[]Reloc) ([]byte, error) {
|
|
|
|
|
lt, ok := a64LoadTable[mnem]
|
|
|
|
|
if !ok {
|
|
|
|
|
lt = a64LoadTable["MOVD"]
|
|
|
|
|
}
|
|
|
|
|
storeOpc := a64StoreOpc(lt)
|
|
|
|
|
if relocs != nil {
|
|
|
|
|
*relocs = append(*relocs,
|
2026-09-14 18:22:00 +02:00
|
|
|
Reloc{Off: 0, After: 8, Name: sym.Name, Kind: RelArm64LDST64, Addend: sym.Offset},
|
2026-08-20 13:32:52 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return a64WordsLE(
|
2026-09-14 18:22:00 +02:00
|
|
|
a64ADR(1, 0, 0, 27), // ADRP R27, 0
|
|
|
|
|
a64LSU(uint32(lt.size), uint32(lt.V), uint32(storeOpc), 0, 27, uint32(rs)), // STR Rs, [R27, #0]
|
2026-08-20 13:32:52 +02:00
|
|
|
), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- operand helpers ----
|
|
|
|
|
|
2026-08-20 14:31:15 +02:00
|
|
|
// arm64Imm64 returns the full 64-bit immediate value of an operand.
|
|
|
|
|
func arm64Imm64(op *ast.Operand) int64 {
|
|
|
|
|
if op.Imm.HasVal {
|
|
|
|
|
v := op.Imm.Val
|
|
|
|
|
if op.Imm.Neg {
|
|
|
|
|
v = -v
|
|
|
|
|
}
|
|
|
|
|
return v
|
|
|
|
|
}
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 13:32:52 +02:00
|
|
|
// arm64MemWithFrame resolves a memory operand, translating FP/SP pseudo-
|
2026-09-19 23:49:07 +02:00
|
|
|
// registers via the frame mapping. The offset stays 64-bit: the AST carries
|
|
|
|
|
// int64 displacements and truncating here would wrap offsets beyond 2^31
|
|
|
|
|
// silently.
|
|
|
|
|
func arm64MemWithFrame(op *ast.Operand, fi arm64FrameInfo) (rn int, off int64) {
|
2026-08-20 13:32:52 +02:00
|
|
|
if op.Addr.Sym != nil && op.Addr.Sym.Pseudo != "" {
|
2026-09-19 23:49:07 +02:00
|
|
|
base, pseudo := arm64ResolvePseudo(op.Addr.Sym, fi)
|
|
|
|
|
return base, int64(pseudo)
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
return arm64RegNum(op.Addr.Base), op.Addr.Offset
|
2026-08-20 13:32:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// arm64Label returns the label name of an operand.
|
|
|
|
|
func arm64Label(op *ast.Operand) string {
|
|
|
|
|
if op.Addr.Sym != nil {
|
|
|
|
|
return op.Addr.Sym.Name
|
|
|
|
|
}
|
|
|
|
|
return op.Raw
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 14:07:12 +02:00
|
|
|
// ---- FP instruction encoding ----
|
|
|
|
|
|
|
|
|
|
// encodeARM64FP3 encodes a FP 3-operand instruction (Rm, Rn, Rd).
|
|
|
|
|
// FADD, FSUB, FMUL, FDIV, FMAX, FMIN, FNMUL.
|
|
|
|
|
func encodeARM64FP3(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 3 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
if rm < 0 || rn < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64FPUnary encodes a FP unary instruction (Rn, Rd).
|
|
|
|
|
// FMOV reg-reg, FABS, FNEG, FSQRT, FCVT cross-precision, FRINT*.
|
|
|
|
|
func encodeARM64FPUnary(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
if rn < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64FP4 encodes a FP 4-operand FMA instruction (Ra, Rm, Rn, Rd).
|
|
|
|
|
// FMADD, FMSUB, FNMADD, FNMSUB.
|
|
|
|
|
func encodeARM64FP4(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
var ra, rm, rn, rd int
|
|
|
|
|
switch len(ops) {
|
|
|
|
|
case 4:
|
|
|
|
|
ra = arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
rm = arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
rn = arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
rd = arm64RegNum(operandRegName(ops[3]))
|
|
|
|
|
case 3:
|
|
|
|
|
// 3-operand form: Fa, Fm, Fd → Fd = Fa ± Fd*Fm (Rn = Rd)
|
|
|
|
|
ra = arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
rm = arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
rd = arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
rn = rd
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("%s expects 3 or 4 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
if ra < 0 || rm < 0 || rn < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(ra)<<16 | uint32(rm)<<10 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64FPCmp encodes a FP compare instruction.
|
|
|
|
|
// Go assembler syntax: FCMP Fn, Fm (register) or FCMP $0.0, Fn (compare with zero).
|
|
|
|
|
// ARM64 encoding: Rm in bits[20:16], Rn in bits[9:5].
|
|
|
|
|
// Go puts first operand → Rm, second → Rn.
|
|
|
|
|
func encodeARM64FPCmp(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
// Check if first operand is #0 (compare with zero): FCMP $0.0, Fn.
|
2026-09-19 23:49:07 +02:00
|
|
|
if isImmOperand(ops[0]) && arm64Imm64(ops[0]) == 0 {
|
2026-08-20 14:07:12 +02:00
|
|
|
rn := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
if rn < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
// For compare with zero: Rm=0, op2 bit 3 set (|= 8).
|
|
|
|
|
return a64wordLE((baseOp | 8) | 0<<16 | uint32(rn)<<5), nil
|
|
|
|
|
}
|
|
|
|
|
// Register compare: FCMP Fn, Fm.
|
|
|
|
|
// Go puts first operand in Rm field, second in Rn field.
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
if rm < 0 || rn < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rn)<<5), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64FPCCmp encodes a FP conditional compare.
|
|
|
|
|
// Go assembler syntax: FCCMP cond, Fn, Fm, $nzcv
|
|
|
|
|
// ARM64 encoding: Rm in bits[20:16], Rn in bits[9:5].
|
|
|
|
|
// Go puts ops[1] in Rm field, ops[2] in Rn field.
|
|
|
|
|
func encodeARM64FPCCmp(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 4 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 4 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
condName := operandRegName(ops[0])
|
|
|
|
|
cond, ok := arm64CondMap[condName]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("invalid condition code %q in %s", condName, mnem)
|
|
|
|
|
}
|
|
|
|
|
// Go puts ops[1] in Rm (bits 20:16), ops[2] in Rn (bits 9:5).
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
if rm < 0 || rn < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
nzcv := arm64Imm64(ops[3])
|
|
|
|
|
if nzcv < 0 || nzcv > 0xF {
|
|
|
|
|
return nil, fmt.Errorf("%s: nzcv %d out of range (0..15)", mnem, nzcv)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | cond<<12 | uint32(rn)<<5 | uint32(nzcv)&0xF), nil
|
2026-08-20 14:07:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64FPSel encodes a FP conditional select.
|
|
|
|
|
// Go assembler syntax: FCSEL cond, Fn, Fm, Fd
|
|
|
|
|
func encodeARM64FPSel(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 4 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 4 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
// Operand order: cond, Fn, Fm, Fd
|
|
|
|
|
condName := operandRegName(ops[0])
|
|
|
|
|
cond, ok := arm64CondMap[condName]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("invalid condition code %q in %s", condName, mnem)
|
|
|
|
|
}
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[3]))
|
|
|
|
|
if rn < 0 || rm < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | cond<<12 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64FPCvt encodes a FP ↔ integer conversion instruction.
|
|
|
|
|
// The operand order depends on direction: FCVTZS Fd, Rn (FP→int) or SCVTF Rd, Fn (int→FP).
|
|
|
|
|
func encodeARM64FPCvt(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
src := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
dst := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
if src < 0 || dst < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(src)<<5 | uint32(dst)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64CSEL encodes a conditional select instruction.
|
|
|
|
|
// CSEL Rm, Rn, Rd, cond (4 operands) or CSET Rd, cond (2 operands).
|
|
|
|
|
func encodeARM64CSEL(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
isAlias := mnem == "CSET" || mnem == "CSETW" || mnem == "CSETM" || mnem == "CSETMW" ||
|
|
|
|
|
mnem == "CINC" || mnem == "CINCW" || mnem == "CINV" || mnem == "CINVW" ||
|
|
|
|
|
mnem == "CNEG" || mnem == "CNEGW"
|
|
|
|
|
|
|
|
|
|
if isAlias {
|
|
|
|
|
is2op := mnem == "CSET" || mnem == "CSETW" || mnem == "CSETM" || mnem == "CSETMW"
|
|
|
|
|
if is2op {
|
|
|
|
|
// CSET cond, Rd → CSEL XZR, XZR, Rd, inverted_cond
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
condName := operandRegName(ops[0])
|
|
|
|
|
cond, ok := arm64CondMap[condName]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("invalid condition code %q in %s", condName, mnem)
|
|
|
|
|
}
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
if rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
invCond := cond ^ 1
|
|
|
|
|
return a64wordLE(baseOp | 31<<16 | invCond<<12 | 31<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
// CINC cond, Rn, Rd → CSINC Rn, Rn, Rd, inverted_cond
|
|
|
|
|
if len(ops) != 3 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
condName := operandRegName(ops[0])
|
|
|
|
|
cond, ok := arm64CondMap[condName]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("invalid condition code %q in %s", condName, mnem)
|
|
|
|
|
}
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
if rn < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
invCond := cond ^ 1
|
|
|
|
|
return a64wordLE(baseOp | uint32(rn)<<16 | invCond<<12 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-14 18:22:00 +02:00
|
|
|
// CSEL cond, Rn, Rm, Rd (4 operands), condition first.
|
2026-08-20 14:07:12 +02:00
|
|
|
// Go assembler syntax: CSEL cond, Rn, Rm, Rd
|
|
|
|
|
// ARM64 encoding: Rm in bits[20:16], Rn in bits[9:5], Rd in bits[4:0].
|
|
|
|
|
if len(ops) != 4 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 4 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
condName := operandRegName(ops[0])
|
|
|
|
|
cond, ok := arm64CondMap[condName]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("invalid condition code %q in %s", condName, mnem)
|
|
|
|
|
}
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[3]))
|
|
|
|
|
if rn < 0 || rm < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | cond<<12 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64CRC32 encodes a CRC32 instruction.
|
|
|
|
|
// Go assembler syntax: CRC32B Rm, Rd (2 operands, Rn=Rd).
|
|
|
|
|
func encodeARM64CRC32(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) == 3 {
|
|
|
|
|
// 3-operand form: CRC32B Rm, Rn, Rd → use Rm and Rd, Rn=Rd.
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
if rm < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rd)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
if len(ops) != 2 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 2 or 3 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
if rm < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rd)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 14:31:15 +02:00
|
|
|
// ---- Atomics encoding ----
|
|
|
|
|
|
2026-09-19 23:49:07 +02:00
|
|
|
// arm64ExclMem resolves the memory operand of an exclusive or atomic
|
|
|
|
|
// instruction. These encodings have no immediate field: the toolchain
|
|
|
|
|
// rejects `LDXR 8(R1), R2` as an illegal combination, so a non-zero offset is
|
|
|
|
|
// reported rather than silently dropped (which would read the wrong address).
|
|
|
|
|
func arm64ExclMem(mnem string, op *ast.Operand) (int, error) {
|
|
|
|
|
rn, off := arm64MemWithFrame(op, arm64FrameInfo{})
|
|
|
|
|
if rn < 0 {
|
|
|
|
|
return 0, fmt.Errorf("invalid memory operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
if off != 0 {
|
|
|
|
|
return 0, fmt.Errorf("%s: offset %d not supported, exclusive and atomic accesses take a plain (Rn) operand", mnem, off)
|
|
|
|
|
}
|
|
|
|
|
return rn, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 00:38:24 +02:00
|
|
|
// arm64PairOf parses a register-pair operand `(R1, R2)`, reporting false
|
|
|
|
|
// when the operand is not a pair. The toolchain takes the second register of
|
|
|
|
|
// the pair from the operand's Offset (its C_PAIR class,
|
|
|
|
|
// cmd/internal/obj/arm64/asm7.go cases 58/59).
|
|
|
|
|
func arm64PairOf(op *ast.Operand) (int, int, bool) {
|
|
|
|
|
raw := strings.TrimSpace(op.Raw)
|
|
|
|
|
if !strings.HasPrefix(raw, "(") || !strings.HasSuffix(raw, ")") {
|
|
|
|
|
return -1, -1, false
|
|
|
|
|
}
|
|
|
|
|
parts := strings.Split(raw[1:len(raw)-1], ",")
|
|
|
|
|
if len(parts) != 2 {
|
|
|
|
|
return -1, -1, false
|
|
|
|
|
}
|
|
|
|
|
r1 := arm64RegNum(strings.TrimSpace(parts[0]))
|
|
|
|
|
r2 := arm64RegNum(strings.TrimSpace(parts[1]))
|
|
|
|
|
if r1 < 0 || r2 < 0 {
|
|
|
|
|
return -1, -1, false
|
|
|
|
|
}
|
|
|
|
|
return r1, r2, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64Excl encodes the exclusive load/store family with the operand
|
|
|
|
|
// order the toolchain parses (cmd/internal/obj/arm64/asm7.go cases 58 and 59,
|
|
|
|
|
// and its own spellings in arm64enc.s):
|
|
|
|
|
//
|
|
|
|
|
// STXR Rt, (Rn), Rs store, single register
|
|
|
|
|
// STXP (Rt1, Rt2), (Rn), Rs store, register pair
|
|
|
|
|
// LDXR (Rn), Rt load, single register
|
|
|
|
|
// LDXP (Rn), (Rt1, Rt2) load, register pair
|
|
|
|
|
//
|
|
|
|
|
// Decoded toolchain evidence: `STXR R1, (R2), R3` assembles to 0xc8037c41,
|
|
|
|
|
// whose fields are Rs=3, Rn=2, Rt=1: the FIRST register operand is the data
|
|
|
|
|
// register and the LAST the status register.
|
2026-08-20 14:31:15 +02:00
|
|
|
func encodeARM64Excl(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
isLoad := strings.HasPrefix(mnem, "LD")
|
|
|
|
|
if isLoad {
|
2026-09-20 00:38:24 +02:00
|
|
|
// LDXR (Rn), Rt / LDXP (Rn), (Rt1, Rt2): 2 operands.
|
2026-08-20 14:31:15 +02:00
|
|
|
if len(ops) != 2 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
rn, err := arm64ExclMem(mnem, ops[0])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-09-20 00:38:24 +02:00
|
|
|
if rt1, rt2, ok := arm64PairOf(ops[1]); ok {
|
|
|
|
|
// The single-register opcodes pre-set the unused Rs (bits 20:16)
|
|
|
|
|
// and Rt2 (bits 14:10) fields to 31; the pair forms carry a real
|
|
|
|
|
// Rt2 and keep Rs at 31.
|
|
|
|
|
return a64wordLE(baseOp | 0x1F<<16 | uint32(rt2)<<10 | uint32(rn)<<5 | uint32(rt1)), nil
|
|
|
|
|
}
|
2026-08-20 14:31:15 +02:00
|
|
|
rt := arm64RegNum(operandRegName(ops[1]))
|
2026-09-19 23:49:07 +02:00
|
|
|
if rt < 0 {
|
2026-08-20 14:31:15 +02:00
|
|
|
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rn)<<5 | uint32(rt)), nil
|
|
|
|
|
}
|
2026-09-20 00:38:24 +02:00
|
|
|
// STXR Rt, (Rn), Rs / STXP (Rt1, Rt2), (Rn), Rs: 3 operands.
|
2026-08-20 14:31:15 +02:00
|
|
|
if len(ops) != 3 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
rn, err := arm64ExclMem(mnem, ops[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-09-20 00:38:24 +02:00
|
|
|
rs := arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
if rs < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
if rt1, rt2, ok := arm64PairOf(ops[0]); ok {
|
|
|
|
|
return a64wordLE(baseOp | uint32(rs)<<16 | uint32(rt2)<<10 | uint32(rn)<<5 | uint32(rt1)), nil
|
|
|
|
|
}
|
|
|
|
|
rt := arm64RegNum(operandRegName(ops[0]))
|
2026-09-19 23:49:07 +02:00
|
|
|
if rt < 0 {
|
2026-08-20 14:31:15 +02:00
|
|
|
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rs)<<16 | uint32(rn)<<5 | uint32(rt)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64LSEAtom encodes an LSE atomic instruction (LDADD, CAS, SWP).
|
|
|
|
|
// LDADD Rs, (Rn), Rt → 3 operands: Rs, mem, Rt
|
|
|
|
|
// CAS Rs, (Rn), Rt → 3 operands: Rs, mem, Rt
|
|
|
|
|
func encodeARM64LSEAtom(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 3 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
rs := arm64RegNum(operandRegName(ops[0]))
|
2026-09-19 23:49:07 +02:00
|
|
|
if rs < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
rn, err := arm64ExclMem(mnem, ops[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-08-20 14:31:15 +02:00
|
|
|
rt := arm64RegNum(operandRegName(ops[2]))
|
2026-09-19 23:49:07 +02:00
|
|
|
if rt < 0 {
|
2026-08-20 14:31:15 +02:00
|
|
|
return nil, fmt.Errorf("invalid operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rs)<<16 | uint32(rn)<<5 | uint32(rt)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Bitfield/EXTR encoding ----
|
|
|
|
|
|
|
|
|
|
// encodeARM64Bitfield encodes a bitfield instruction.
|
|
|
|
|
// BFI/BFXIL/SBFM/UBFM $immr, Rn, $imms, Rd → 4 operands
|
|
|
|
|
func encodeARM64Bitfield(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
// BFI/BFXIL/SBFM/UBFM: 4 operands ($immr, Rn, $imms, Rd)
|
|
|
|
|
if len(ops) != 4 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 4 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
immr := arm64Imm64(ops[0])
|
2026-08-20 14:31:15 +02:00
|
|
|
rn := arm64RegNum(operandRegName(ops[1]))
|
2026-09-19 23:49:07 +02:00
|
|
|
imms := arm64Imm64(ops[2])
|
2026-08-20 14:31:15 +02:00
|
|
|
rd := arm64RegNum(operandRegName(ops[3]))
|
|
|
|
|
if rn < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
// The toolchain rejects bit numbers at or above the operand width, which
|
|
|
|
|
// sf (bit 31 of the base) selects: 64 when set, 32 otherwise.
|
|
|
|
|
width := uint32(32) << (baseOp >> 31 & 1)
|
|
|
|
|
if immr < 0 || uint32(immr) >= width || imms < 0 || uint32(imms) >= width {
|
|
|
|
|
return nil, fmt.Errorf("%s: bit number out of range (immr=%d imms=%d, width=%d)", mnem, immr, imms, width)
|
|
|
|
|
}
|
2026-08-20 14:31:15 +02:00
|
|
|
return a64wordLE(baseOp | uint32(immr)<<16 | uint32(imms)<<10 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// encodeARM64Extr encodes an EXTR instruction.
|
|
|
|
|
// EXTR $lsb, Rm, Rn, Rd → 4 operands
|
|
|
|
|
func encodeARM64Extr(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 4 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 4 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
lsb := arm64Imm64(ops[0])
|
2026-08-20 14:31:15 +02:00
|
|
|
rm := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[3]))
|
|
|
|
|
if rm < 0 || rn < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
2026-09-19 23:49:07 +02:00
|
|
|
// The imms field is 6 bits and must stay below the operand width, which
|
|
|
|
|
// sf (bit 31 of the base) selects: 64 when set, 32 otherwise.
|
|
|
|
|
width := int64(32) << (baseOp >> 31 & 1)
|
|
|
|
|
if lsb < 0 || lsb >= width {
|
|
|
|
|
return nil, fmt.Errorf("%s: bit number %d out of range (width=%d)", mnem, lsb, width)
|
|
|
|
|
}
|
2026-08-20 14:31:15 +02:00
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(lsb)<<10 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- SIMD/NEON encoding ----
|
|
|
|
|
|
|
|
|
|
// encodeARM64SIMD3 encodes a SIMD 3-operand instruction.
|
|
|
|
|
// VADD Vm, Vn, Vd → base | Rm<<16 | Rn<<5 | Rd (Q and size bits in base)
|
|
|
|
|
func encodeARM64SIMD3(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) {
|
|
|
|
|
if len(ops) != 3 {
|
|
|
|
|
return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops))
|
|
|
|
|
}
|
|
|
|
|
rm := arm64RegNum(operandRegName(ops[0]))
|
|
|
|
|
rn := arm64RegNum(operandRegName(ops[1]))
|
|
|
|
|
rd := arm64RegNum(operandRegName(ops[2]))
|
|
|
|
|
if rm < 0 || rn < 0 || rd < 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid register operand in %s", mnem)
|
|
|
|
|
}
|
|
|
|
|
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd)), nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 13:32:52 +02:00
|
|
|
// AssembleFileARM64 assembles every TEXT function of a parsed arm64 file
|
|
|
|
|
// and lays out its static symbols (GLOBL/DATA) in a data section behind the
|
|
|
|
|
// code. SB references in the code are encoded as ADRP pairs with zero
|
|
|
|
|
// immediates; the object-file emitters record R_ADDRARM64 relocations for
|
|
|
|
|
// the linker.
|
2026-08-07 22:43:40 +02:00
|
|
|
func AssembleFileARM64(f *ast.File) (*Image, error) {
|
2026-08-20 13:32:52 +02:00
|
|
|
dataSyms, err := collectData(f)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 23:49:07 +02:00
|
|
|
img := &Image{Symbols: map[string]int{}, SourcePath: f.Path}
|
2026-08-20 13:32:52 +02:00
|
|
|
for _, d := range f.Decls {
|
|
|
|
|
t, ok := d.(*ast.Text)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
code, labels, relocs, lines, spadj, err := assembleARM64(t)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("%s: %w", t.Name.Name, err)
|
|
|
|
|
}
|
|
|
|
|
fl := FuncLayout{
|
|
|
|
|
Name: t.Name.Name,
|
|
|
|
|
Pkg: t.Name.Pkg,
|
|
|
|
|
Static: t.Name.Static,
|
|
|
|
|
Offset: len(img.Code),
|
|
|
|
|
Size: len(code),
|
|
|
|
|
Frame: frameSize(t),
|
|
|
|
|
Args: argsSize(t),
|
|
|
|
|
Line: t.Pos().Line,
|
|
|
|
|
Labels: labels,
|
|
|
|
|
Lines: lines,
|
|
|
|
|
Spadj: spadj,
|
|
|
|
|
Relocs: relocs,
|
|
|
|
|
}
|
|
|
|
|
for _, f := range t.Flags {
|
|
|
|
|
switch f {
|
|
|
|
|
case "NOSPLIT":
|
|
|
|
|
fl.NoSplit = true
|
|
|
|
|
case "SPWRITE":
|
|
|
|
|
fl.SPWrite = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
img.Funcs = append(img.Funcs, fl)
|
|
|
|
|
img.Code = append(img.Code, code...)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Lay out the data section behind the code, 16-aligned.
|
|
|
|
|
dataStart := len(img.Code)
|
|
|
|
|
for _, d := range dataSyms {
|
|
|
|
|
pos := dataStart + len(img.Data)
|
|
|
|
|
for pos%16 != 0 {
|
|
|
|
|
img.Data = append(img.Data, 0)
|
|
|
|
|
pos++
|
|
|
|
|
}
|
|
|
|
|
img.Symbols[d.name] = pos
|
|
|
|
|
img.Data = append(img.Data, d.buf...)
|
|
|
|
|
img.DataSyms = append(img.DataSyms, DataSymbol{
|
|
|
|
|
Name: d.name,
|
|
|
|
|
Pkg: d.pkg,
|
|
|
|
|
Offset: len(img.Data) - len(d.buf),
|
|
|
|
|
Size: d.size,
|
|
|
|
|
Static: d.static,
|
|
|
|
|
Rodata: d.rodata,
|
|
|
|
|
Dupok: d.dupok,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-20 23:22:34 +02:00
|
|
|
markExternals(img, dataSyms)
|
2026-08-20 13:32:52 +02:00
|
|
|
return img, nil
|
2026-08-07 22:43:40 +02:00
|
|
|
}
|