// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package asm import ( "fmt" "strconv" "strings" "sourcedock.dev/petrbalvin/gasm-devkit/ast" ) // assembleARM64 assembles an AArch64 (arm64) TEXT function body into machine // code. Every instruction is 4 bytes; the MOV pseudo-instruction and the // immediate-arithmetic forms expand to 2-4 instructions when the immediate // does not fit, so the layout is computed in two passes (sizes, then encoding // with resolved branch targets). The returned literals carry the read-only // constants any VMOVS/VMOVD/VMOVQ load refers to; the file assembler lays // them out in the data section. // // 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/ // 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. func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, []SpadjStep, []Arm64Literal, error) { fi := arm64ComputeFrame(t) prologue := arm64Prologue(fi) guardLen := arm64GuardLen(fi) chain := arm64JumpChain(t) resolve := func(name string) string { if r, ok := chain[name]; ok { return r } return name } var relocs []Reloc var spadj []SpadjStep lits := &arm64Literals{} // The prologue (3 instructions when a small frame, 4 for large) // raises the SP delta by autosize. The guard prefix shifts its PC. if fi.autosize != 0 { spadj = append(spadj, SpadjStep{PC: guardLen + arm64PrologueSpadjPC(fi), Value: fi.autosize}) } // Pass 1: label offsets from the instruction sizes. offsets := map[string]int{} pos := guardLen + len(prologue) for _, stmt := range t.Body { switch s := stmt.(type) { case *ast.Label: offsets[s.Name.Text] = pos case *ast.Instr: pos += arm64InstrSize(s, fi) } } // 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) 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, lits) if err != nil { return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", in.Mnemonic.Text, err) } for j := preCount; j < len(relocs); j++ { // 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 } 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) } if fi.needSplit { block, blReloc := arm64MoreStackBlock(pc) out = append(out, block...) relocs = append(relocs, blReloc) pc += len(block) } return out, offsets, relocs, lines, spadj, lits.list(), nil } // 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 "VMOVS", "VMOVD", "VMOVQ": // ADRP + ADD + wide load against a pooled literal. return 12 } 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]) { v := arm64Imm64(ops[0]) // 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. lits collects the // read-only literals a VMOVS/VMOVD/VMOVQ constant load needs; the file // assembler lays them out once every function is encoded. func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64FrameInfo, relocs *[]Reloc, resolve func(string) string, lits *arm64Literals) ([]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)) } 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 case "B", "JMP": return encodeARM64Branch(mnem, ops, pc, offsets, false, relocs, resolve) case "BL", "CALL": return encodeARM64Branch(mnem, ops, pc, offsets, true, relocs, resolve) 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) } // Unconditional register branches (BR, BLR). if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FUncondBranch { return encodeARM64RegBranch(mnem, enc.op, ops) } // 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) } } // 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) } // Register-register data processing. if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FDPSR { return encodeARM64DPSR(mnem, enc.op, ops) } // 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) } // Exclusive load/store (LDXR, STXR, LDAXR, STLXR and the register-pair // forms LDXP, STXP). 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) } // Acquire/release loads and stores (LDAR family, STLR family). if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FAcqRel { return encodeARM64AcqRel(mnem, enc.op, ops) } // Load/store pairs (LDP, STP, LDPW, STPW, FLDPD, FSTPD). if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FPair { return encodeARM64Pair(mnem, enc.op, ops, fi) } // Compare-and-branch and test-and-branch to a label. if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FBranch19 { return encodeARM64Branch19(mnem, enc.op, ops, pc, offsets, resolve) } if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FTestBranch { return encodeARM64TestBranch(mnem, enc.op, ops, pc, offsets, resolve) } // Data-processing (1 source): RBIT, REV, CLZ, CLS. if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FDP1 { return encodeARM64DP1(mnem, enc.op, ops) } // ADR/ADRP: (label, Rd) with the byte distance split into immlo and // immhi. if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FADR { return encodeARM64ADR(mnem, enc.op, ops, pc, offsets, resolve) } // Bitfield extract with wrapping immr: UBFX, SBFX. if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FBitfield2 { return encodeARM64Bitfield2(mnem, enc.op, ops) } // Conditional compare: CCMP, CCMN. if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FCondCmp { return encodeARM64CondCmp(mnem, enc.op, ops) } // System operations: BRK, SVC, DMB, DSB, ISB, DC, MRS, MSR, PRFM. if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FSys { return encodeARM64Sys(mnem, ops) } // Crypto: AESD, AESE, SHA1C, SHA256H and friends. if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FCrypto2 { return encodeARM64Crypto(mnem, enc.op, ops, 2) } if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FCrypto3 { return encodeARM64Crypto(mnem, enc.op, ops, 3) } // Move wide with an explicit immediate: MOVK. if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FMovWide { return encodeARM64MoveWide(mnem, enc.op, ops) } // SIMD element moves (VDUP, VMOV with lane indices) take precedence // over the plain arrangement paths, which carry no index. if (mnem == "VDUP" || mnem == "VMOV") && arm64SimdHasElement(ops) { return encodeARM64Dup(mnem, ops) } // Arrangement-aware SIMD three-register (VADD, VAND, VCMEQ, VZIP1, // VPMULL, VRAX1 and friends). VADD, VSUB and VMUL appear here too, so // this check precedes the plain SIMD3 path below. if spec, ok := a64SimdVTable[mnem]; ok { return encodeARM64SimdV(mnem, spec, ops) } // Arrangement-aware SIMD two-register (VREV32, VREV64, VUADDLV, VMOV). if spec, ok := a64SimdV2Table[mnem]; ok { return encodeARM64SimdV2(mnem, spec, ops) } // SIMD four-register and immediate three-register (VEOR3, VBCAX, VXAR, // VEXT). if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FSIMDV4 { return encodeARM64SimdV4(mnem, enc.op, ops) } // SIMD table lookup. if mnem == "VTBL" { return encodeARM64VTBL(ops) } // SIMD structure loads and stores (VLD1, VST1, VLD1.P, VST1.P, VLD1R, // VLD4R). if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FVLDST { return encodeARM64VLDST(mnem, enc.op, ops) } // SIMD shift by immediate (VSHL, VUSHR, VSRI). if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FShiftImm { return encodeARM64ShiftImm(mnem, enc.op, ops) } // VMOVS/VMOVD/VMOVQ with a large constant: a literal pool load. if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FMoviLit { return encodeARM64MoviLit(mnem, enc.op, ops, relocs, lits) } return nil, fmt.Errorf("unsupported arm64 instruction %q", mnem) } // ---- branch encoding ---- // encodeARM64Branch encodes an unconditional branch (B/BL) to a label. func encodeARM64Branch(mnem string, ops []*ast.Operand, pc int, offsets map[string]int, link bool, relocs *[]Reloc, resolve func(string) string) ([]byte, error) { if len(ops) != 1 { return nil, fmt.Errorf("%s expects 1 operand, got %d", mnem, len(ops)) } op := ops[0] // 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 } // 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" { if relocs != nil { *relocs = append(*relocs, Reloc{ Off: 0, After: 4, Name: op.Addr.Sym.Name, Addend: op.Addr.Sym.Offset, Kind: RelArm64Branch, }) } // 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 } target := resolve(arm64Label(op)) 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) } bop := uint32(0) // B if link { bop = 1 // BL } return a64wordLE(a64Branch(bop, int32(rel))), nil } // 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 } // 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: // ADC family carries an immediate spelling: ADC $0, Rd reads the // carry into Rd and takes ZR as the register operand. The // encoding has no immediate field, so $0 is the only value. if isImmOperand(ops[0]) { v := arm64Imm64(ops[0]) if v != 0 { return nil, fmt.Errorf("%s: only $0 is supported as immediate", mnem) } rd := arm64RegNum(operandRegName(ops[1])) if rd < 0 { return nil, fmt.Errorf("invalid register operand in %s", mnem) } return a64wordLE(baseOp | 31<<16 | uint32(rd)<<5 | uint32(rd)), nil } 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)) } // 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 } // ---- 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)) } v := arm64Imm64(ops[0]) 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 } // The imm12 field cannot carry the value; rejecting (rather than // truncating) matches the toolchain, which reports the same shape. return nil, fmt.Errorf("%s: immediate %d out of range for single instruction", mnem, v) } // ---- MOV pseudo-instruction ---- // encodeARM64Mov encodes the MOV family, the load/store/immediate workhorse // 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) } return encodeARM64LoadImm(rd, arm64Imm64(src), mnem) } // 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 } // 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 { return 4 } return len(b) 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. lt, ok := a64LoadTable[mnem] if !ok { lt = a64LoadTable["MOVD"] // the MOV pseudo is a 64-bit access } scale := int64(1) << uint(lt.size) if off >= 0 && off%scale == 0 && off/scale < 4096 { return 4 } if off >= -256 && off <= 255 { return 4 // unscaled } if _, _, _, ok := arm64SplitOffset(off, scale); ok { return 8 // ADD base, REGTMP + access } return 12 // literal pool range: encoding reports it as unsupported default: return 4 // register move } } // encodeARM64LoadImm loads an immediate into a register, matching the // 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. func encodeARM64LoadImm(rd int, v int64, mnem string) ([]byte, error) { d := v sf := uint32(1) // 64-bit if mnem == "MOVW" || mnem == "MOVWU" { d = int64(uint32(v)) sf = 0 } if d == 0 { // ORR Rd, ZR, ZR (MOV $0, Rd) op := uint32(1<<31 | 1<<29 | 0x0a<<24) // ORR 64-bit if sf == 0 { op = 0<<31 | 1<<29 | 0x0a<<24 // ORR 32-bit } return a64wordLE(op | 31<<16 | 31<<5 | uint32(rd)), nil } // 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)) if tryBitmaskFirst { // Addcon-band immediate: try bitmask first (Go uses ORR for values // like $1, $256 and $65536). 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 } } // 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 } s := arm64Movcon(d) if s >= 0 { return a64wordLE(a64MoveWide(sf, 2, uint32(s>>4), uint32((d>>uint(s))&0xFFFF), uint32(rd))), nil } sn := arm64Movcon(^d & int64(width)) if sn >= 0 { return a64wordLE(a64MoveWide(sf, 0, uint32(sn>>4), uint32(((^d)>>uint(sn))&0xFFFF), uint32(rd))), nil } // 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 } } // Multi-instruction: MOVZ + MOVK for each non-zero 16-bit chunk. var ws []uint32 first := true for i := range 4 { 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<> 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<= 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 } if off >= -256 && off <= 255 { return a64wordLE(a64LSUnscaled(lt.size, lt.V, opc, int32(off), rn, reg)), nil } // Large offset: materialise the base in REGTMP (R27) the way the // toolchain does and access what remains. The ADD offsets from the // operand's own base register, [SP] and [Rn] alike. 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( a64AddSub(1, 0, 0, addShift, addImm, uint32(rn), 27), // ADD $addImm<= 0 && rest>>12 <= 4095 { return uint32(rest >> 12), 1, off - rest, true } return 0, 0, 0, false } // ---- 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 ) } // 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. 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, Reloc{Off: 0, After: 8, Name: sym.Name, Kind: RelArm64LDST64, Addend: sym.Offset}, ) } return a64WordsLE( 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] ), nil } // encodeARM64SBStore emits ADRP R27, 0; STR Rs, [R27, 0] with relocations, // matching the toolchain's R27 scratch and R_ARM64_PCREL_LDST64 pair. 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, Reloc{Off: 0, After: 8, Name: sym.Name, Kind: RelArm64LDST64, Addend: sym.Offset}, ) } return a64WordsLE( 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] ), nil } // ---- operand helpers ---- // 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 } // arm64MemWithFrame resolves a memory operand, translating FP/SP pseudo- // 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) { if op.Addr.Sym != nil && op.Addr.Sym.Pseudo != "" { base, pseudo := arm64ResolvePseudo(op.Addr.Sym, fi) return base, int64(pseudo) } return arm64RegNum(op.Addr.Base), op.Addr.Offset } // 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 } // ---- 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. if isImmOperand(ops[0]) && arm64Imm64(ops[0]) == 0 { 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) } 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 } // 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 } // CSEL cond, Rn, Rm, Rd (4 operands), condition first. // 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 } // ---- Atomics encoding ---- // 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 } // 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. func encodeARM64Excl(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) { isLoad := strings.HasPrefix(mnem, "LD") if isLoad { // LDXR (Rn), Rt / LDXP (Rn), (Rt1, Rt2): 2 operands. if len(ops) != 2 { return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops)) } rn, err := arm64ExclMem(mnem, ops[0]) if err != nil { return nil, err } 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 } rt := arm64RegNum(operandRegName(ops[1])) if rt < 0 { return nil, fmt.Errorf("invalid operand in %s", mnem) } return a64wordLE(baseOp | uint32(rn)<<5 | uint32(rt)), nil } // STXR Rt, (Rn), Rs / STXP (Rt1, Rt2), (Rn), Rs: 3 operands. if len(ops) != 3 { return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops)) } rn, err := arm64ExclMem(mnem, ops[1]) if err != nil { return nil, err } 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])) if rt < 0 { 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])) if rs < 0 { return nil, fmt.Errorf("invalid operand in %s", mnem) } rn, err := arm64ExclMem(mnem, ops[1]) if err != nil { return nil, err } rt := arm64RegNum(operandRegName(ops[2])) if rt < 0 { return nil, fmt.Errorf("invalid operand in %s", mnem) } return a64wordLE(baseOp | uint32(rs)<<16 | uint32(rn)<<5 | uint32(rt)), nil } // encodeARM64DP1 encodes a data-processing (1 source) instruction: // RBIT, REV, CLZ and friends take (Rn, Rd), word = base | Rn<<5 | Rd. func encodeARM64DP1(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 } // encodeARM64ADR encodes ADR/ADRP: (label, Rd), the byte distance to the // target split into immlo (bits 30:29) and immhi (bits 23:5), with bit 31 // selecting the page form. func encodeARM64ADR(mnem string, page uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) { if len(ops) != 2 { return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops)) } rd := arm64RegNum(operandRegName(ops[1])) if rd < 0 { return nil, fmt.Errorf("invalid register operand in %s", mnem) } target := resolve(arm64Label(ops[0])) targetOff, ok := offsets[target] if !ok { return nil, fmt.Errorf("undefined label %q", target) } rel := int64(targetOff - pc) if rel < -(1<<20) || rel >= 1<<20 { return nil, fmt.Errorf("%s to %q too far (21-bit range)", mnem, target) } return a64wordLE(a64ADR(page, int32(rel>>2), uint32(rel)&3, uint32(rd))), nil } // encodeARM64Bitfield2 encodes UBFX/SBFX ($lsb, Rn, $width, Rd): immr // carries the lsb (six bits with the N flag on the X forms) and imms the // lsb plus width minus one. A sum beyond the register width is the // toolchain's "illegal bit number" error. func encodeARM64Bitfield2(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) { if len(ops) != 4 || !isImmOperand(ops[0]) || !isImmOperand(ops[2]) { return nil, fmt.Errorf("%s expects 4 operands ($lsb, Rn, $width, Rd)", mnem) } lsb := arm64Imm64(ops[0]) width := arm64Imm64(ops[2]) rn := arm64RegNum(operandRegName(ops[1])) rd := arm64RegNum(operandRegName(ops[3])) if rn < 0 || rd < 0 { return nil, fmt.Errorf("invalid register operand in %s", mnem) } bits := int64(32) << (baseOp >> 31 & 1) if lsb < 0 || lsb >= bits { return nil, fmt.Errorf("%s: lsb %d out of range (0..%d)", mnem, lsb, bits-1) } if width < 1 || lsb+width > bits { return nil, fmt.Errorf("%s: illegal bit number (lsb %d width %d, register %d bits)", mnem, lsb, width, bits) } immr := uint32(lsb) n := uint32(0) if immr >= 32 { n = 1 << 21 immr &^= 32 } return a64wordLE(baseOp | n | immr<<16 | uint32(lsb+width-1)<<10 | uint32(rn)<<5 | uint32(rd)), nil } // encodeARM64CondCmp encodes CCMP/CCMN: (cond, Rn, Rm|$imm, $nzcv). The // third field carries Rm or a 5-bit immediate in the same bits, at the // toolchain's choice of register or immediate operand. func encodeARM64CondCmp(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) { if len(ops) != 4 || !isImmOperand(ops[3]) { return nil, fmt.Errorf("%s expects 4 operands (cond, Rn, Rm|$imm, $nzcv)", mnem) } 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])) if rn < 0 { return nil, fmt.Errorf("invalid register operand in %s", mnem) } var v2 uint32 if isImmOperand(ops[2]) { v := arm64Imm64(ops[2]) if v < 0 || v > 31 { return nil, fmt.Errorf("%s: immediate %d out of range (0..31)", mnem, v) } v2 = uint32(v) } else { rm := arm64RegNum(operandRegName(ops[2])) if rm < 0 { return nil, fmt.Errorf("invalid register operand in %s", mnem) } v2 = uint32(rm) } nzcv := arm64Imm64(ops[3]) if nzcv < 0 || nzcv > 15 { return nil, fmt.Errorf("%s: nzcv %d out of range (0..15)", mnem, nzcv) } // Bit 11 carries the immediate-vs-register choice for the third field. op2 := uint32(0) if isImmOperand(ops[2]) { op2 = 1 << 11 } return a64wordLE(baseOp | v2<<16 | cond<<12 | op2 | uint32(rn)<<5 | uint32(nzcv)&0xF), nil } // encodeARM64Branch19 encodes CBZ/CBNZ: (Rt, label), // word = base | imm19<<5 | Rt with imm19 = (target - pc) >> 2. func encodeARM64Branch19(mnem string, baseOp uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) { if len(ops) != 2 { return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops)) } rt := arm64RegNum(operandRegName(ops[0])) if rt < 0 { return nil, fmt.Errorf("invalid register operand in %s", mnem) } target := resolve(arm64Label(ops[1])) 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) } return a64wordLE(baseOp | uint32(rel)&0x7FFFF<<5 | uint32(rt)), nil } // encodeARM64TestBranch encodes TBZ/TBNZ: ($bit, Rt, label). Bits 32 to 63 // set the b5 flag at bit 31; there is one mnemonic per polarity, no width // suffix. func encodeARM64TestBranch(mnem string, baseOp uint32, ops []*ast.Operand, pc int, offsets map[string]int, resolve func(string) string) ([]byte, error) { if len(ops) != 3 || !isImmOperand(ops[0]) { return nil, fmt.Errorf("%s expects 3 operands ($bit, Rt, label)", mnem) } bit := arm64Imm64(ops[0]) if bit < 0 || bit > 63 { return nil, fmt.Errorf("%s: bit number %d out of range (0..63)", mnem, bit) } rt := arm64RegNum(operandRegName(ops[1])) if rt < 0 { return nil, fmt.Errorf("invalid register operand in %s", mnem) } target := resolve(arm64Label(ops[2])) targetOff, ok := offsets[target] if !ok { return nil, fmt.Errorf("undefined label %q", target) } rel := (targetOff - pc) >> 2 if rel < -(1<<13) || rel >= (1<<13) { return nil, fmt.Errorf("branch to %q too far (14-bit range)", target) } return a64wordLE(baseOp | uint32(bit>>5)<<31 | uint32(bit&31)<<19 | uint32(rel)&0x3FFF<<5 | uint32(rt)), nil } // encodeARM64Pair encodes load/store pair instructions. Loads spell // (mem, (Rt1, Rt2)), stores (Rt1, Rt2), mem; the scaled immediate rides // imm7 at bits 21:15 and must fit -64..63 after division by the access // size (8 bytes for the D forms, 4 for the W forms). func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, fi arm64FrameInfo) ([]byte, error) { if len(ops) != 2 { return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops)) } scale := int64(8) if strings.HasSuffix(mnem, "W") { scale = 4 } load := strings.Contains(mnem, "LDP") memOp, pairOp := ops[0], ops[1] if !load { memOp, pairOp = ops[1], ops[0] } if !isMemOperand(memOp) { return nil, fmt.Errorf("%s: invalid memory operand", mnem) } rt1, rt2, ok := arm64PairOf(pairOp) if !ok { return nil, fmt.Errorf("%s expects a register pair (Rt1, Rt2)", mnem) } rn, off := arm64MemWithFrame(memOp, fi) if rn < 0 { return nil, fmt.Errorf("%s: invalid memory operand", mnem) } if off%scale != 0 || off < -64*scale || off > 63*scale { return nil, fmt.Errorf("%s: offset %d out of pair range or not a multiple of %d", mnem, off, scale) } imm7 := off / scale // The base carries the opc, V and L halves; only the scaled immediate // and the three registers are filled in here. return a64wordLE(baseOp | uint32(imm7)&0x7F<<15 | uint32(rt2)<<10 | uint32(rn)<<5 | uint32(rt1)), nil } // encodeARM64AcqRel encodes the acquire/release loads and stores. Loads // spell (Rn), Rt; stores Rt, (Rn). Both lay the base register at bits 9:5 // and the data register at bits 4:0 over a base that carries no offset // field, so a displaced operand is reported the way arm64ExclMem reports // one for the exclusive family. func encodeARM64AcqRel(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)) } memOp, regOp := ops[0], ops[1] if !strings.HasPrefix(mnem, "LD") { memOp, regOp = ops[1], ops[0] } rn, err := arm64ExclMem(mnem, memOp) if err != nil { return nil, err } rt := arm64RegNum(operandRegName(regOp)) if rt < 0 { return nil, fmt.Errorf("invalid register operand in %s", mnem) } return a64wordLE(baseOp | uint32(rn)<<5 | uint32(rt)), nil } // encodeARM64Sys encodes the system operations: // // BRK [$imm16] SVC $imm16 // DMB|DSB|ISB $imm4 DC , Rn // MRS , Rd MSR $imm4, // PRFM (Rn), $imm| func encodeARM64Sys(mnem string, ops []*ast.Operand) ([]byte, error) { switch mnem { case "BRK", "SVC": base := uint32(0xd4200000) if mnem == "SVC" { base = 0xd4000001 } if len(ops) == 0 { return a64wordLE(base), nil } if len(ops) != 1 || !isImmOperand(ops[0]) { return nil, fmt.Errorf("%s expects no operand or $immediate", mnem) } v := arm64Imm64(ops[0]) if v < 0 || v > 0xFFFF { return nil, fmt.Errorf("%s: immediate %d out of range (0..65535)", mnem, v) } return a64wordLE(base | uint32(v)<<5), nil case "DMB", "DSB", "ISB": if len(ops) != 1 || !isImmOperand(ops[0]) { return nil, fmt.Errorf("%s expects $immediate", mnem) } v := arm64Imm64(ops[0]) if v < 0 || v > 15 { return nil, fmt.Errorf("%s: immediate %d out of range (0..15)", mnem, v) } base := map[string]uint32{"DMB": 0xd50330bf, "DSB": 0xd503309f, "ISB": 0xd50330df}[mnem] return a64wordLE(base | uint32(v)<<8), nil case "DC": if len(ops) != 2 { return nil, fmt.Errorf("DC expects , Rn") } base, ok := a64DCOps[operandRegName(ops[0])] if !ok { return nil, fmt.Errorf("DC: unknown cache operation %q", operandRegName(ops[0])) } rn := arm64RegNum(operandRegName(ops[1])) if rn < 0 { return nil, fmt.Errorf("DC: invalid register operand") } return a64wordLE(base | uint32(rn)&31), nil case "MRS": if len(ops) != 2 { return nil, fmt.Errorf("MRS expects , Rd") } base, ok := a64MRSOps[operandRegName(ops[0])] if !ok { return nil, fmt.Errorf("MRS: unknown system register %q", operandRegName(ops[0])) } rd := arm64RegNum(operandRegName(ops[1])) if rd < 0 { return nil, fmt.Errorf("MRS: invalid register operand") } return a64wordLE(base | uint32(rd)&31), nil case "MSR": if len(ops) != 2 || !isImmOperand(ops[0]) { return nil, fmt.Errorf("MSR expects $immediate, ") } base, ok := a64MSROps[operandRegName(ops[1])] if !ok { return nil, fmt.Errorf("MSR: unknown system register %q", operandRegName(ops[1])) } v := arm64Imm64(ops[0]) if v < 0 || v > 15 { return nil, fmt.Errorf("MSR: immediate %d out of range (0..15)", v) } return a64wordLE(base | uint32(v)<<8 | 31), nil case "PRFM": if len(ops) != 2 { return nil, fmt.Errorf("PRFM expects (Rn), $immediate|") } rn, off := arm64MemWithFrame(ops[0], arm64FrameInfo{}) if rn < 0 || off != 0 { return nil, fmt.Errorf("PRFM: invalid memory operand") } var prfop int64 if isImmOperand(ops[1]) { prfop = arm64Imm64(ops[1]) if prfop < 0 || prfop > 31 { return nil, fmt.Errorf("PRFM: immediate %d out of range (0..31)", prfop) } } else { p, ok := a64PRFOps[operandRegName(ops[1])] if !ok { return nil, fmt.Errorf("PRFM: unknown prefetch operation %q", operandRegName(ops[1])) } prfop = int64(p) } return a64wordLE(0xf9800000 | uint32(rn)<<5 | uint32(prfop)), nil } return nil, fmt.Errorf("unsupported arm64 instruction %q", mnem) } // encodeARM64Crypto encodes the crypto instructions. Two-register forms // spell (Rn, Rd), three-register forms (Rm, Rn, Rd); the arrangements, when // spelled, must match the instruction's own (B16 for AES, S4 for the SHA1 // and SHA256 families, D2 for SHA512). func encodeARM64Crypto(mnem string, baseOp uint32, ops []*ast.Operand, n int) ([]byte, error) { if len(ops) != n { return nil, fmt.Errorf("%s expects %d operands, got %d", mnem, n, len(ops)) } vs := make([]a64Vec, n) for i, op := range ops { v, ok := arm64VecOf(op) if !ok || v.hasIdx { return nil, fmt.Errorf("invalid register operand in %s", mnem) } if v.arr != "" && a64ArrIndex(v.arr) != a64CryptoArr[mnem] { return nil, fmt.Errorf("%s: invalid arrangement %q", mnem, v.arr) } vs[i] = v } if n == 2 { return a64wordLE(baseOp | uint32(vs[0].reg)<<5 | uint32(vs[1].reg)), nil } return a64wordLE(baseOp | uint32(vs[0].reg)<<16 | uint32(vs[1].reg)<<5 | uint32(vs[2].reg)), nil } // encodeARM64MoveWide encodes a standalone MOVK: ($value, Rd) with the value // sitting in one 16-bit chunk, the chunk's position becoming the hw field. func encodeARM64MoveWide(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, error) { if len(ops) != 2 || !isImmOperand(ops[0]) { return nil, fmt.Errorf("%s expects $value, Rd", mnem) } rd := arm64RegNum(operandRegName(ops[1])) if rd < 0 { return nil, fmt.Errorf("invalid register operand in %s", mnem) } // The opcode (MOVN 0, MOVZ 2, MOVK 3) and the width ride the table // base, so MOVZ and MOVN come along for free. opc := baseOp >> 29 & 3 sf := baseOp >> 31 & 1 v := arm64Imm64(ops[0]) if v < 0 { return nil, fmt.Errorf("%s: negative immediate %d", mnem, v) } hw := -1 for i := range 4 { if v>>(uint(i)*16)&0xFFFF != 0 { hw = i break } } if hw < 0 { hw = 0 // zero: every chunk is zero, hw = 0 carries it } for i := hw + 1; i < 4; i++ { if v>>(uint(i)*16)&0xFFFF != 0 { return nil, fmt.Errorf("%s: immediate %d does not fit one 16-bit chunk", mnem, v) } } if sf == 0 && hw > 1 { return nil, fmt.Errorf("%s: immediate %d out of range for the 32-bit form", mnem, v) } return a64wordLE(a64MoveWide(sf, opc, uint32(hw), uint32(v>>uint(hw*16)&0xFFFF), uint32(rd))), 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)) } immr := arm64Imm64(ops[0]) rn := arm64RegNum(operandRegName(ops[1])) imms := arm64Imm64(ops[2]) rd := arm64RegNum(operandRegName(ops[3])) if rn < 0 || rd < 0 { return nil, fmt.Errorf("invalid register operand in %s", mnem) } // 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) } 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)) } lsb := arm64Imm64(ops[0]) 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) } // 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) } return a64wordLE(baseOp | uint32(rm)<<16 | uint32(lsb)<<10 | uint32(rn)<<5 | uint32(rd)), nil } // ---- SIMD/NEON encoding ---- // arm64VecOf parses a vector operand. The element suffix of V13.S[0] does // not survive into the symbol name, so the verbatim operand text is tried // first and the register name second. func arm64VecOf(op *ast.Operand) (a64Vec, bool) { if v, ok := a64VecReg(op.Raw); ok { return v, true } return a64VecReg(operandRegName(op)) } // arm64SimdHasElement reports whether any operand carries a lane index such // as V13.S[0]. func arm64SimdHasElement(ops []*ast.Operand) bool { for _, op := range ops { if v, ok := arm64VecOf(op); ok && v.hasIdx { return true } } return false } // arm64SimdArrs validates that a SIMD operand run spells one arrangement, // that it is the same on every operand that spells one, and that the table // admits it. It returns the arrangement's index, with a64Arr8B for a bare // V/F spelling. func arm64SimdArrs(mnem string, arrs []string, allowed uint16) (int, error) { sel := a64Arr8B for _, a := range arrs { if a == "" { continue } i := a64ArrIndex(a) if i < 0 || specBit(i)&allowed == 0 { return 0, fmt.Errorf("%s: invalid arrangement %q", mnem, a) } if sel != a64Arr8B && sel != i { return 0, fmt.Errorf("%s: mixed arrangements", mnem) } sel = i } return sel, nil } // specBit returns the a64SimdVSpec bitmask bit for an arrangement index. func specBit(i int) uint16 { return 1 << uint(i) } // encodeARM64SimdV encodes an arrangement-aware three-register SIMD // instruction: word = base | arrBits | Rm<<16 | Rn<<5 | Rd. VCMEQ with a // zero immediate takes its compare-against-zero form instead, and the // polynomial multiplies read the arrangement from their source operands // alone, the result spelling (H8, Q1) riding no encoding bits. func encodeARM64SimdV(mnem string, spec a64SimdVSpec, ops []*ast.Operand) ([]byte, error) { if mnem == "VPMULL" || mnem == "VPMULL2" { if len(ops) != 3 { return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops)) } vs := make([]a64Vec, 2) arrs := make([]string, 2) for i, op := range ops[:2] { v, ok := arm64VecOf(op) if !ok || v.hasIdx { return nil, fmt.Errorf("invalid register operand in %s", mnem) } vs[i], arrs[i] = v, v.arr } if _, ok := arm64VecOf(ops[2]); !ok { return nil, fmt.Errorf("invalid register operand in %s", mnem) } arr, err := arm64SimdArrs(mnem, arrs, spec.arrs) if err != nil { return nil, err } rd, _ := arm64VecOf(ops[2]) return a64wordLE(spec.base | a64ArrBits[arr] | uint32(vs[0].reg)<<16 | uint32(vs[1].reg)<<5 | uint32(rd.reg)), nil } if mnem == "VCMEQ" && len(ops) == 3 && isImmOperand(ops[0]) { if arm64Imm64(ops[0]) != 0 { return nil, fmt.Errorf("%s: only $0 is supported as immediate", mnem) } vn, ok1 := arm64VecOf(ops[1]) vd, ok2 := arm64VecOf(ops[2]) if !ok1 || !ok2 || vn.hasIdx || vd.hasIdx { return nil, fmt.Errorf("invalid register operand in %s", mnem) } arr, err := arm64SimdArrs(mnem, []string{vn.arr, vd.arr}, 0x7f) if err != nil { return nil, err } return a64wordLE(0x0e209800 | a64ArrBits[arr] | uint32(vn.reg)<<5 | uint32(vd.reg)), nil } if len(ops) != 3 { return nil, fmt.Errorf("%s expects 3 operands, got %d", mnem, len(ops)) } vs := make([]a64Vec, 3) arrs := make([]string, 3) for i, op := range ops { v, ok := arm64VecOf(op) if !ok || v.hasIdx { return nil, fmt.Errorf("invalid register operand in %s", mnem) } vs[i], arrs[i] = v, v.arr } arr, err := arm64SimdArrs(mnem, arrs, spec.arrs) if err != nil { return nil, err } arrBits := a64ArrBits[arr] if spec.fixed { arrBits = 0 } return a64wordLE(spec.base | arrBits | uint32(vs[0].reg)<<16 | uint32(vs[1].reg)<<5 | uint32(vs[2].reg)), nil } // encodeARM64SimdV2 encodes an arrangement-aware two-register SIMD // instruction: word = base | arrBits | Rn<<5 | Rd. VMOV is the exception: // its register pair spelling ORRs the source with itself into the // destination (base | Rm<<16 | Rn<<5 | Rd with Rm = Rn = source). func encodeARM64SimdV2(mnem string, spec a64SimdVSpec, ops []*ast.Operand) ([]byte, error) { if len(ops) != 2 { return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops)) } vs := make([]a64Vec, 2) arrs := make([]string, 2) for i, op := range ops { v, ok := arm64VecOf(op) if !ok || v.hasIdx { return nil, fmt.Errorf("invalid register operand in %s", mnem) } vs[i], arrs[i] = v, v.arr } if mnem == "VMOV" { if (vs[0].arr != "" && vs[1].arr != "") && vs[0].arr != vs[1].arr { return nil, fmt.Errorf("%s: mixed arrangements", mnem) } arr, err := arm64SimdArrs(mnem, arrs, spec.arrs) if err != nil { return nil, err } return a64wordLE(spec.base | a64ArrBits[arr] | uint32(vs[0].reg)<<16 | uint32(vs[0].reg)<<5 | uint32(vs[1].reg)), nil } // VUADDLV spells its arrangement on the source alone; the rest take it // on both. vn, ok1 := arm64VecOf(ops[0]) vd, ok2 := arm64VecOf(ops[1]) if !ok1 || !ok2 { return nil, fmt.Errorf("invalid register operand in %s", mnem) } arr, err := arm64SimdArrs(mnem, []string{vn.arr, vd.arr}, spec.arrs) if err != nil { return nil, err } return a64wordLE(spec.base | a64ArrBits[arr] | uint32(vn.reg)<<5 | uint32(vd.reg)), nil } // encodeARM64SimdV4 encodes the four-register crypto group (VEOR3, VBCAX: // ops ride Rm, Sa, Rn, Rd at 16, 10, 5 and 0) and its immediate relatives // (VXAR with a 6-bit rotation at bits 15:10, VEXT with the index at bits // 15:11 and a B16 flag at bit 30). func encodeARM64SimdV4(mnem string, base uint32, ops []*ast.Operand) ([]byte, error) { switch mnem { case "VEOR3", "VBCAX": if len(ops) != 4 { return nil, fmt.Errorf("%s expects 4 operands, got %d", mnem, len(ops)) } vs := make([]a64Vec, 4) arrs := make([]string, 4) for i, op := range ops { v, ok := arm64VecOf(op) if !ok || v.hasIdx { return nil, fmt.Errorf("invalid register operand in %s", mnem) } vs[i], arrs[i] = v, v.arr } if _, err := arm64SimdArrs(mnem, arrs, 1< 63 { return nil, fmt.Errorf("%s: rotation %d out of range (0..63)", mnem, rot) } vs := make([]a64Vec, 3) arrs := make([]string, 3) for i, op := range ops[1:] { v, ok := arm64VecOf(op) if !ok || v.hasIdx { return nil, fmt.Errorf("invalid register operand in %s", mnem) } vs[i], arrs[i] = v, v.arr } if _, err := arm64SimdArrs(mnem, arrs, 1< int64(max) { return nil, fmt.Errorf("%s: index %d out of range (0..%d)", mnem, idx, max) } return a64wordLE(base | b16 | uint32(idx)<<11 | uint32(vs[0].reg)<<16 | uint32(vs[1].reg)<<5 | uint32(vs[2].reg)), nil } return nil, fmt.Errorf("unsupported arm64 instruction %q", mnem) } // encodeARM64VTBL encodes VTBL Vidx.arr, [Vt1.arr, ...], Vdest.arr: the // index register rides bits 19:16, the first table register bits 9:5, the // destination bits 4:0 and the table length (registers minus one) bits // 14:13. The table registers must be consecutive. func encodeARM64VTBL(ops []*ast.Operand) ([]byte, error) { if len(ops) < 3 { return nil, fmt.Errorf("VTBL expects index, table list and destination") } vi, ok := arm64VecOf(ops[0]) if !ok || vi.hasIdx { return nil, fmt.Errorf("invalid index register in VTBL") } ts, end, ok := a64VecListOf(ops, 1) if !ok || len(ts) < 1 || len(ts) > 4 { return nil, fmt.Errorf("VTBL expects a table of one to four registers") } vd, ok := a64VecReg(operandRegName(ops[end+1])) if !ok || end+2 != len(ops) || vd.hasIdx { return nil, fmt.Errorf("invalid destination register in VTBL") } for i, t := range ts { if t.hasIdx || t.reg != ts[0].reg+i { return nil, fmt.Errorf("VTBL table registers must be consecutive") } } q := uint32(0) switch vi.arr { case "B16": q = 1 << 30 case "B8", "": default: return nil, fmt.Errorf("VTBL: invalid arrangement %q", vi.arr) } return a64wordLE(0x0e000000 | q | uint32(len(ts)-1)<<13 | uint32(vi.reg)<<16 | uint32(ts[0].reg)<<5 | uint32(vd.reg)), nil } // encodeARM64Dup encodes the SIMD element moves VDUP and VMOV spell with // lane indices: // // Vn.[i], Rd UMOV, element to general register // Vn.[i], Vd.arr DUP, element across a vector // Vn.[i], Vd.[j] INS, element to element // Rs, Vd.[i] INS, general register into an element func encodeARM64Dup(mnem string, ops []*ast.Operand) ([]byte, error) { if len(ops) != 2 { return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops)) } dst, dstVec := arm64VecOf(ops[1]) dstGP := false if !dstVec { // A general-register spelling as destination (UMOV forms). if rd := arm64RegNum(operandRegName(ops[1])); rd >= 0 { dst, dstGP, dstVec = a64Vec{reg: rd}, true, true } } if !dstVec { return nil, fmt.Errorf("%s: invalid destination operand", mnem) } src, ok1 := arm64VecOf(ops[0]) if (!ok1 || !src.hasIdx) && !dst.hasIdx { return nil, fmt.Errorf("%s expects an element operand Vn.[i]", mnem) } if !ok1 || !src.hasIdx { // General register into a vector element. f, ok := a64ElemField(dst.arr, dst.idx) if !ok { return nil, fmt.Errorf("%s: invalid element operand", mnem) } rs := arm64RegNum(operandRegName(ops[0])) if rs < 0 { return nil, fmt.Errorf("%s: source must be a general register", mnem) } return a64wordLE(0x4e001c00 | f<<16 | uint32(rs)<<5 | uint32(dst.reg)), nil } sf, ok := a64ElemField(src.arr, src.idx) if !ok { return nil, fmt.Errorf("%s: invalid element operand", mnem) } if dst.hasIdx { // Element to element. df, ok := a64ElemField(dst.arr, dst.idx) if !ok { return nil, fmt.Errorf("%s: invalid element operand", mnem) } return a64wordLE(0x6e000400 | df<<16 | sf>>1<<11 | uint32(src.reg)<<5 | uint32(dst.reg)), nil } if dstGP { // Element to a general register: UMOV, with the D form setting bit // 30. base := uint32(0x0e003c00) if src.arr == "D" { base = 0x4e003c00 } return a64wordLE(base | sf<<16 | uint32(src.reg)<<5 | uint32(dst.reg)), nil } if dst.arr == "" { // Element across a bare V register. return a64wordLE(0x5e000400 | sf<<16 | uint32(src.reg)<<5 | uint32(dst.reg)), nil } // Element across an arranged vector; the 128-bit arrangements set bit // 30. q := uint32(0) switch dst.arr { case "B16", "H8", "S4", "D2": q = 1 << 30 case "B8", "H4", "S2", "D1": default: return nil, fmt.Errorf("%s: invalid destination arrangement %q", mnem, dst.arr) } return a64wordLE(0x0e000400 | q | sf<<16 | uint32(src.reg)<<5 | uint32(dst.reg)), nil } // encodeARM64VLDST encodes the SIMD structure loads and stores: // // VLD1 (Rn), [Vt.arr, ...] VST1 [Vt.arr, ...], (Rn) // VLD1.P off(Rn), [Vt.arr, ...] VST1.P [Vt.arr, ...], off(Rn) // VLD1R (Rn), [Vt.arr] VLD4R (Rn), [Vt.arr, Vt+1, Vt+2, Vt+3] // // The post-index forms set the post bit and Rm = 11111; the increment is // implied by the register list, and a spelled offset rides along the way the // toolchain's own encodings ignore it. func encodeARM64VLDST(mnem string, post uint32, ops []*ast.Operand) ([]byte, error) { load := strings.HasPrefix(mnem, "VLD") listStart, memAt := 0, 1 if load { // Every load spells the memory operand first. listStart, memAt = 1, 0 } vs, end, ok := a64VecListOf(ops, listStart) if !ok { return nil, fmt.Errorf("%s: invalid register list", mnem) } memIdx := end + 1 if memAt == 0 { memIdx = 0 } if memIdx >= len(ops) { return nil, fmt.Errorf("%s expects a (Rn) memory operand", mnem) } rn, off := arm64MemWithFrame(ops[memIdx], arm64FrameInfo{}) if rn < 0 { return nil, fmt.Errorf("%s: invalid memory operand", mnem) } if off != 0 && post == 0 { return nil, fmt.Errorf("%s: offset %d not supported, plain and list accesses take a plain (Rn) operand", mnem, off) } // VLD1R loads one register and replicates; VLD4R loads four. if strings.HasPrefix(mnem, "VLD1R") || strings.HasPrefix(mnem, "VLD4R") { want := 1 base := uint32(0x0d40c000) if strings.HasPrefix(mnem, "VLD4R") { want, base = 4, 0x0d60e000 } if len(vs) != want { return nil, fmt.Errorf("%s expects a list of %d registers", mnem, want) } size, q, ok := a64ArrSizeQ(vs[0].arr) if !ok { return nil, fmt.Errorf("%s: invalid arrangement %q", mnem, vs[0].arr) } return a64wordLE(base | q<<30 | size<<10 | uint32(rn)<<5 | uint32(vs[0].reg)), nil } if len(vs) < 1 || len(vs) > 4 { return nil, fmt.Errorf("%s expects a list of one to four registers", mnem) } for i, v := range vs { if v.hasIdx || v.reg != vs[0].reg+i { return nil, fmt.Errorf("%s: register list must be consecutive", mnem) } _, _, okArr := a64ArrSizeQ(v.arr) if !okArr || (i > 0 && v.arr != vs[0].arr) { return nil, fmt.Errorf("%s: invalid arrangement %q", mnem, v.arr) } } size, q, ok := a64ArrSizeQ(vs[0].arr) if !ok { return nil, fmt.Errorf("%s: invalid arrangement %q", mnem, vs[0].arr) } base := a64VLD1Base[len(vs)] if !load { base = a64VST1Base[len(vs)] } postBits := uint32(0) if post != 0 { postBits = 0x9f0000 } return a64wordLE(base | q<<30 | size<<10 | postBits | uint32(rn)<<5 | uint32(vs[0].reg)), nil } // a64ArrSizeQ maps an arrangement to its size code (bits 11:10) and 128-bit // flag for the structure load/store words. func a64ArrSizeQ(arr string) (size, q uint32, ok bool) { switch arr { case "B8": return 0, 0, true case "B16": return 0, 1, true case "H4": return 1, 0, true case "H8": return 1, 1, true case "S2": return 2, 0, true case "S4": return 2, 1, true case "D1": return 3, 0, true case "D2": return 3, 1, true } return 0, 0, false } // encodeARM64ShiftImm encodes a SIMD shift by immediate: // word = base | Q<<30 | immh:immb<<16 | Rn<<5 | Rd, where immh:immb is the // element size plus the shift for a left shift (VSHL) and twice the element // size minus the shift for right shifts (VUSHR, VSRI). func encodeARM64ShiftImm(mnem string, base uint32, ops []*ast.Operand) ([]byte, error) { if len(ops) != 3 || !isImmOperand(ops[0]) { return nil, fmt.Errorf("%s expects 3 operands ($shift, Vn.arr, Vd.arr)", mnem) } sh := arm64Imm64(ops[0]) vn, ok1 := arm64VecOf(ops[1]) vd, ok2 := arm64VecOf(ops[2]) if !ok1 || !ok2 || vn.hasIdx || vd.hasIdx || vn.arr != vd.arr { return nil, fmt.Errorf("%s: operands must share one arrangement", mnem) } var esize int64 q := uint32(0) switch vn.arr { case "B8", "B": esize = 8 case "B16": esize, q = 8, 1 case "H4", "H": esize = 16 case "H8": esize, q = 16, 1 case "S2", "S": esize = 32 case "S4": esize, q = 32, 1 case "D2": esize, q = 64, 1 default: return nil, fmt.Errorf("%s: invalid arrangement %q", mnem, vn.arr) } var immval int64 switch mnem { case "VSHL": if sh < 0 || sh >= esize { return nil, fmt.Errorf("%s: shift %d out of range (0..%d)", mnem, sh, esize-1) } immval = esize + sh default: // VUSHR, VSRI if sh < 1 || sh > esize { return nil, fmt.Errorf("%s: shift %d out of range (1..%d)", mnem, sh, esize) } immval = 2*esize - sh } return a64wordLE(base | q<<30 | uint32(immval)<<16 | uint32(vn.reg)<<5 | uint32(vd.reg)), nil } // encodeARM64MoviLit loads a large vector constant the way the toolchain // does: ADRP R27 and ADD materialise the literal's address, then FMOVS, // FMOVD or the 128-bit FMOVQ form loads it, with R_ADDRARM64 relocations // against a read-only literal the file assembler lays out. func encodeARM64MoviLit(mnem string, ldr uint32, ops []*ast.Operand, relocs *[]Reloc, lits *arm64Literals) ([]byte, error) { var vd a64Vec var ok bool var data []byte switch mnem { case "VMOVS", "VMOVD": if len(ops) != 2 || !isImmOperand(ops[0]) { return nil, fmt.Errorf("%s expects $value, Vd", mnem) } v := arm64Imm64(ops[0]) if mnem == "VMOVS" { if v < -2147483648 || v > 0xFFFFFFFF { return nil, fmt.Errorf("%s: constant does not fit 32 bits", mnem) } data = a64wordLE(uint32(v)) } else { data = a64WordsLE(uint32(v), uint32(v>>32)) } vd, ok = arm64VecOf(ops[1]) if !ok || vd.hasIdx || vd.arr != "" { return nil, fmt.Errorf("%s: destination must be a bare V register", mnem) } case "VMOVQ": if len(ops) != 3 || !isImmOperand(ops[0]) || !isImmOperand(ops[1]) { return nil, fmt.Errorf("VMOVQ expects $lo, $hi, Vd") } lo, hi := arm64Imm64(ops[0]), arm64Imm64(ops[1]) data = a64WordsLE(uint32(lo), uint32(lo>>32), uint32(hi), uint32(hi>>32)) vd, ok = arm64VecOf(ops[2]) if !ok || vd.hasIdx || vd.arr != "" { return nil, fmt.Errorf("VMOVQ: destination must be a bare V register") } default: return nil, fmt.Errorf("unsupported arm64 instruction %q", mnem) } name := lits.add(moviLitName(mnem, data), data) if relocs != nil { *relocs = append(*relocs, Reloc{Off: 0, After: 0, Name: name, Kind: RelArm64Addr}, Reloc{Off: 4, After: 4, Name: name, Kind: RelArm64Addr}, ) } return a64WordsLE( a64ADR(1, 0, 0, 27), a64AddSub(1, 0, 0, 0, 0, 27, 27), ldr|0<<10|27<<5|uint32(vd.reg), ), nil } // moviLitName mirrors the toolchain's literal naming: $i32/$i64/$i128 // followed by the constant's value in hex. func moviLitName(mnem string, data []byte) string { switch mnem { case "VMOVS": v := uint32(data[0]) | uint32(data[1])<<8 | uint32(data[2])<<16 | uint32(data[3])<<24 return "$i32." + strconv.FormatUint(uint64(v), 16) case "VMOVD": v := uint64(data[0]) | uint64(data[1])<<8 | uint64(data[2])<<16 | uint64(data[3])<<24 | uint64(data[4])<<32 | uint64(data[5])<<40 | uint64(data[6])<<48 | uint64(data[7])<<56 return "$i64." + strconv.FormatUint(v, 16) default: hi := uint64(data[8]) | uint64(data[9])<<8 | uint64(data[10])<<16 | uint64(data[11])<<24 | uint64(data[12])<<32 | uint64(data[13])<<40 | uint64(data[14])<<48 | uint64(data[15])<<56 lo := uint64(data[0]) | uint64(data[1])<<8 | uint64(data[2])<<16 | uint64(data[3])<<24 | uint64(data[4])<<32 | uint64(data[5])<<40 | uint64(data[6])<<48 | uint64(data[7])<<56 return "$i128." + strings.Repeat("0", max(0, 16-len(strconv.FormatUint(hi, 16)))) + strconv.FormatUint(hi, 16) + strings.Repeat("0", max(0, 16-len(strconv.FormatUint(lo, 16)))) + strconv.FormatUint(lo, 16) } } // arm64Literals collects the read-only constants the VMOVS/VMOVD/VMOVQ // loads refer to. Names follow the toolchain's $i32/$i64/$i128 spellings so // equal constants deduplicate to one literal. type arm64Literals struct { order []Arm64Literal seen map[string]bool } // Arm64Literal is one pooled vector constant. type Arm64Literal struct { Name string Data []byte } // add registers a literal under its name and returns it. func (l *arm64Literals) add(name string, data []byte) string { if l.seen == nil { l.seen = map[string]bool{} } if !l.seen[name] { l.seen[name] = true l.order = append(l.order, Arm64Literal{Name: name, Data: data}) } return name } // list returns the literals in first-use order. func (l *arm64Literals) list() []Arm64Literal { return l.order } // 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. func AssembleFileARM64(f *ast.File) (*Image, error) { dataSyms, err := collectData(f) if err != nil { return nil, err } img := &Image{Symbols: map[string]int{}, SourcePath: f.Path} var pendingLits []Arm64Literal litSeen := map[string]bool{} for _, d := range f.Decls { t, ok := d.(*ast.Text) if !ok { continue } code, labels, relocs, lines, spadj, lits, err := assembleARM64(t) if err != nil { return nil, fmt.Errorf("%s: %w", t.Name.Name, err) } // The literals this function's constant loads refer to join the // data section once, deduplicated by name. for _, lit := range lits { if _, seen := litSeen[lit.Name]; seen { continue } litSeen[lit.Name] = true pendingLits = append(pendingLits, lit) } 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, }) } // The read-only literals the VMOVS/VMOVD/VMOVQ constant loads refer to // follow the declared data, deduplicated across the file. for _, lit := range pendingLits { pos := dataStart + len(img.Data) for pos%16 != 0 { img.Data = append(img.Data, 0) pos++ } img.Symbols[lit.Name] = pos img.Data = append(img.Data, lit.Data...) img.DataSyms = append(img.DataSyms, DataSymbol{ Name: lit.Name, Offset: len(img.Data) - len(lit.Data), Size: len(lit.Data), Rodata: true, Dupok: true, }) } markExternals(img, dataSyms) return img, nil }