feat(asm): pool the arm64 offsets the split bands cannot carry

Offsets beyond the split bands ride a per-function literal pool the way
the toolchain lays one out: a PC-relative literal load into REGTMP, then
the register-offset access (the pair family adds the base addition), the
pooled words appended after the last instruction behind the UNDEF guard,
deduplicated by value with the sign- and width-aware load selection.

The same differential pass against the corpus exposed three wrong-code
bugs and fixes them: the logical-immediate period marker rode the wrong
position for every element below 64 bits, so the 32-bit forms encoded a
different constant than written; the plain register operand of an
ADD/SUB against SP took the shifted-register form where the toolchain
uses the extended one with the identity extend, silently truncating
through UXTB; and the AUTIA1716 and AUTIB1716 hint constants were the
PACIA and PACIB encodings.  An offset sweep across every band boundary
now pins all three against the live oracle.

Assisted-by: GLM 5.3 Flash
This commit is contained in:
petrbalvin committed 2026-10-07 02:36:24 +02:00
1 parent 71e8dd550d
commit a0fa7e802c
4 files changed
+381 -38

No files matched your search

+251 -36
View File
@@ -6,6 +6,7 @@ package asm
import (
"fmt"
"math/bits"
"slices"
"strconv"
"strings"
@@ -43,6 +44,7 @@ func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
var relocs []Reloc
var spadj []SpadjStep
lits := &arm64Literals{}
pool := &arm64Pool{}
// The prologue (3 instructions when a small frame, 4 for large)
// raises the SP delta by autosize. The guard prefix shifts its PC.
@@ -85,6 +87,12 @@ func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
}
out = append(out, prologue...)
pc := guardLen + len(prologue)
// The offset literal pool lands after the last instruction (and after
// the morestack block); a function whose last instruction does not
// branch gets an UNDEF first, the toolchain's flushpool guard against
// falling through into the words. The base decides the PC-relative
// distances the pool loads encode, so it is fixed before pass 2.
poolBase := guardLen + len(prologue) + bodyLen + arm64PoolPadLen(t)
preCount := len(relocs)
var lines []LineEntry
for _, stmt := range t.Body {
@@ -107,7 +115,7 @@ func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
}
continue
}
code, err := encodeARM64Instr(in, pc, offsets, fi, &relocs, resolve, lits)
code, err := encodeARM64Instr(in, pc, offsets, fi, &relocs, resolve, lits, pool, poolBase)
if err != nil {
return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", in.Mnemonic.Text, err)
}
@@ -135,6 +143,20 @@ func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
relocs = append(relocs, blReloc)
pc += len(block)
}
// The pool itself: the UNDEF guard word when the body does not end in a
// branch, then the pooled constants in first-use order. The guard is
// the toolchain's word-zero UNDEF, not the BRK the UNDEF statement
// spells: it only has to be a faulting word nothing jumps to.
if pool.size > 0 {
if arm64PoolPadLen(t) > 0 {
out = append(out, a64wordLE(0)...)
pc += 4
}
for _, e := range pool.order {
out = append(out, e.data...)
pc += len(e.data)
}
}
return out, offsets, relocs, lines, spadj, lits.list(), nil
}
@@ -305,7 +327,7 @@ func arm64InstrSize(instr *ast.Instr, fi arm64FrameInfo, pos int) int {
rn = n
}
}
if ws, err := arm64AddSubImmWords(mnem, v, rn, rd); err == nil {
if ws, err := arm64AddSubImmWords(mnem, v, rn, rd, false); err == nil {
return 4 * len(ws)
}
}
@@ -318,7 +340,7 @@ func arm64InstrSize(instr *ast.Instr, fi arm64FrameInfo, pos int) int {
// 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) {
func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64FrameInfo, relocs *[]Reloc, resolve func(string) string, lits *arm64Literals, pool *arm64Pool, poolBase int) ([]byte, error) {
mnem := strings.ToUpper(instr.Mnemonic.Text)
ops := instr.Operands
@@ -362,7 +384,7 @@ func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64
return encodeARM64Branch(mnem, ops, pc, offsets, true, relocs, resolve)
case "MOV", "MOVD", "MOVW", "MOVWU", "MOVH", "MOVHU", "MOVB", "MOVBU",
"FMOVS", "FMOVD", "FMOVQ":
return encodeARM64Mov(instr, mnem, "", fi, relocs)
return encodeARM64Mov(instr, mnem, "", fi, relocs, pool, poolBase, pc)
}
// Post-index (.P) and pre-index (.W) writeback forms: the MOV family and
@@ -372,9 +394,9 @@ func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64
if base, wb, ok := arm64WritebackSuffix(mnem); ok {
switch {
case isARM64MovMnemonic(base):
return encodeARM64Mov(instr, base, wb, fi, relocs)
return encodeARM64Mov(instr, base, wb, fi, relocs, pool, poolBase, pc)
case a64InstrTable[base].format == a64FPair:
return encodeARM64Pair(base, a64InstrTable[base].op, ops, fi, wb, relocs)
return encodeARM64Pair(base, a64InstrTable[base].op, ops, pc, fi, wb, relocs, pool, poolBase)
}
}
@@ -521,7 +543,7 @@ func encodeARM64Instr(instr *ast.Instr, pc int, offsets map[string]int, fi arm64
// Load/store pairs (LDP, STP, LDPW, STPW, FLDPD, FSTPD). The .P/.W
// writeback forms are routed earlier, straight from the mnemonic.
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FPair {
return encodeARM64Pair(mnem, enc.op, ops, fi, "", relocs)
return encodeARM64Pair(mnem, enc.op, ops, pc, fi, "", relocs, pool, poolBase)
}
// Compare-and-branch and test-and-branch to a label.
@@ -848,7 +870,13 @@ func encodeARM64DPSR(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
if merr != nil || rn < 0 || rd < 0 {
return nil, fmt.Errorf("%s: immediate %q is not a logical (bitmask) immediate", mnem, strings.Join(strings.Fields(ops[0].Raw), " "))
}
return append(mw, a64wordLE(baseOp|27<<16|uint32(rn)<<5|uint32(rd))...), nil
// The register tail against SP takes the extended form, like the
// plain register path below.
tail := baseOp | 27<<16 | uint32(rn)<<5 | uint32(rd)
if opt, spok := arm64SpExtendOpt(mnem, ops[1:]); spok {
tail = baseOp | 1<<21 | opt<<13 | 27<<16 | uint32(rn)<<5 | uint32(rd)
}
return append(mw, a64wordLE(tail)...), nil
}
opc := (baseOp >> 29) & 7
sf := (baseOp >> 31) & 1
@@ -935,6 +963,12 @@ func encodeARM64DPSR(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
if rm < 0 || rn < 0 || rd < 0 {
return nil, fmt.Errorf("invalid register operand in %s", mnem)
}
// ADD/SUB against SP take the extended-register form with the
// identity extend, the toolchain's spelling of a plain register
// operand against the stack pointer (asm7.go opxrrr against C_RSP).
if opt, ok := arm64SpExtendOpt(mnem, ops[1:]); ok {
return a64wordLE(baseOp | 1<<21 | opt<<13 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd)), nil
}
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd)), nil
}
@@ -961,6 +995,12 @@ func encodeARM64DPSR(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
if rm < 0 || rn < 0 || rd < 0 {
return nil, fmt.Errorf("invalid register operand in %s", mnem)
}
// ADD/SUB against SP take the extended-register form with the
// identity extend, the toolchain's spelling of a plain register
// operand against the stack pointer (asm7.go opxrrr against C_RSP).
if opt, ok := arm64SpExtendOpt(mnem, ops[1:]); ok {
return a64wordLE(baseOp | 1<<21 | opt<<13 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd)), nil
}
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
@@ -984,6 +1024,9 @@ func encodeARM64DPSR(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er
if rm < 0 || rn < 0 {
return nil, fmt.Errorf("invalid register operand in %s", mnem)
}
if opt, ok := arm64SpExtendOpt(mnem, ops[1:]); ok {
return a64wordLE(baseOp | 1<<21 | opt<<13 | uint32(rm)<<16 | uint32(rn)<<5 | 31), nil
}
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rn)<<5 | 31), nil
}
if isNeg {
@@ -1214,6 +1257,12 @@ func encodeARM64MAddSub(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte,
if rm < 0 || rn < 0 || rd < 0 {
return nil, fmt.Errorf("invalid register operand in %s", mnem)
}
// ADD/SUB against SP take the extended-register form with the
// identity extend, the toolchain's spelling of a plain register
// operand against the stack pointer (asm7.go opxrrr against C_RSP).
if opt, ok := arm64SpExtendOpt(mnem, ops[1:]); ok {
return a64wordLE(baseOp | 1<<21 | opt<<13 | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd)), nil
}
return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd)), nil
}
if len(ops) != 4 {
@@ -1257,7 +1306,18 @@ func encodeARM64AddSubImm(mnem string, ops []*ast.Operand) ([]byte, error) {
if mnem == "CMP" || mnem == "CMPW" || mnem == "CMN" || mnem == "CMNW" {
rd = 31 // ZR
}
ws, err := arm64AddSubImmWords(mnem, v, rn, rd)
// Against SP the register tail is the extended form with the identity
// extend; the comparison zeroing above hides the destination, so the
// decision reads the written destination first.
rdWritten := arm64RegNum(operandRegName(ops[len(ops)-1]))
ext := rn == 31 || rdWritten == 31
switch mnem {
case "ADD", "ADDW", "ADDS", "ADDSW", "SUB", "SUBW", "SUBS", "SUBSW",
"CMP", "CMPW", "CMN", "CMNW":
default:
ext = false
}
ws, err := arm64AddSubImmWords(mnem, v, rn, rd, ext)
if err != nil {
return nil, fmt.Errorf("%s: %w", mnem, err)
}
@@ -1268,7 +1328,7 @@ func encodeARM64AddSubImm(mnem string, ops []*ast.Operand) ([]byte, error) {
// ADD/SUB-family immediate: the mnemonics ADD, ADDS, SUB, SUBS, CMP, CMN and
// their W forms. rn and rd are resolved register numbers (a comparison
// discards rd, so the caller passes 31).
func arm64AddSubImmWords(mnem string, v int64, rn, rd int) ([]uint32, error) {
func arm64AddSubImmWords(mnem string, v int64, rn, rd int, ext bool) ([]uint32, error) {
w := strings.HasSuffix(mnem, "W")
sf := uint32(1) // 64-bit
d := v
@@ -1335,7 +1395,17 @@ func arm64AddSubImmWords(mnem string, v int64, rn, rd int) ([]uint32, error) {
seq = arm64MovLConst(d, sf)
}
// The register form reads REGTMP: Rd = Rn op R27 (opxrrr/oprrr).
seq = append(seq, a64InstrTable[mnem].op|27<<16|uint32(rn)<<5|uint32(rd))
// Against SP it takes the extended form with the identity extend,
// UXTX, or UXTW in the 32-bit forms.
tail := a64InstrTable[mnem].op | 27<<16 | uint32(rn)<<5 | uint32(rd)
if ext {
opt := uint32(3)
if w {
opt = 2
}
tail = a64InstrTable[mnem].op | 1<<21 | opt<<13 | 27<<16 | uint32(rn)<<5 | uint32(rd)
}
seq = append(seq, tail)
for i := range seq[:len(seq)-1] {
seq[i] |= 27 // REGTMP
}
@@ -1479,7 +1549,7 @@ func arm64MovLConst(d int64, sf uint32) []uint32 {
// post-index or pre-index access whose offset is the base writeback amount.
// Storing a $0 immediate stores ZR; any other immediate is rejected, matching
// the toolchain.
func encodeARM64Mov(instr *ast.Instr, mnem string, wb string, fi arm64FrameInfo, relocs *[]Reloc) ([]byte, error) {
func encodeARM64Mov(instr *ast.Instr, mnem string, wb string, fi arm64FrameInfo, relocs *[]Reloc, pool *arm64Pool, poolBase int, pc int) ([]byte, error) {
ops := instr.Operands
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
@@ -1507,7 +1577,7 @@ func encodeARM64Mov(instr *ast.Instr, mnem string, wb string, fi arm64FrameInfo,
if rd < 0 {
return nil, fmt.Errorf("%s: invalid destination register", mnem)
}
return encodeARM64MemOp(mnem, src, rd, true, fi, wb)
return encodeARM64MemOp(mnem, src, rd, true, fi, wb, pool, poolBase, pc)
case isMemOperand(dst) && !isMemOperand(src):
rs := arm64RegNum(operandRegName(src))
if rs < 0 {
@@ -1517,7 +1587,7 @@ func encodeARM64Mov(instr *ast.Instr, mnem string, wb string, fi arm64FrameInfo,
// Storing a constant zero stores the zero register.
rs = 31
}
return encodeARM64MemOp(mnem, dst, rs, false, fi, wb)
return encodeARM64MemOp(mnem, dst, rs, false, fi, wb, pool, poolBase, pc)
default:
return nil, fmt.Errorf("%s: writeback form needs a register and a memory operand", mnem)
}
@@ -1539,7 +1609,7 @@ func encodeARM64Mov(instr *ast.Instr, mnem string, wb string, fi arm64FrameInfo,
if arm64Imm64(src) != 0 {
return nil, fmt.Errorf("%s: illegal combination: an immediate store must be zero", mnem)
}
return encodeARM64MemOp(mnem, dst, 31, false, fi, "")
return encodeARM64MemOp(mnem, dst, 31, false, fi, "", pool, poolBase, pc)
}
rd := arm64RegNum(operandRegName(dst))
if rd < 0 {
@@ -1591,14 +1661,14 @@ func encodeARM64Mov(instr *ast.Instr, mnem string, wb string, fi arm64FrameInfo,
if rd < 0 {
return nil, fmt.Errorf("%s: invalid destination register", mnem)
}
return encodeARM64MemOp(mnem, src, rd, true, fi, "")
return encodeARM64MemOp(mnem, src, rd, true, fi, "", pool, poolBase, pc)
}
if !arm64IsMemOperand(src) && arm64IsMemOperand(dst) {
rs := arm64RegNum(operandRegName(src))
if rs < 0 {
return nil, fmt.Errorf("%s: invalid source register", mnem)
}
return encodeARM64MemOp(mnem, dst, rs, false, fi, "")
return encodeARM64MemOp(mnem, dst, rs, false, fi, "", pool, poolBase, pc)
}
// Register → register.
@@ -1621,9 +1691,6 @@ func arm64PairSize(mnem string, ops []*ast.Operand, fi arm64FrameInfo) int {
return 12 // ADRP + ADD + pair
}
_, off := arm64MemWithFrame(memOp, fi)
if off < 0 {
return 4 // the encoder rejects it in pass 2
}
scale := arm64PairScale(mnem)
if off%scale == 0 && off >= -64*scale && off <= 63*scale {
return 4
@@ -1631,10 +1698,7 @@ func arm64PairSize(mnem string, ops []*ast.Operand, fi arm64FrameInfo) int {
if off >= -4095 && off <= 4095 {
return 8 // ADD/SUB the whole offset into REGTMP + pair
}
if off >= 0 && off <= 0xffffff {
return 12 // the two-ADD split
}
return 12 // the pool sequence: LDR literal + ADD + pair
return 12 // the two-ADD split within 16 MiB, or the pool sequence
}
// arm64MovSize returns the encoded size of a MOV instruction.
@@ -1697,7 +1761,7 @@ func arm64MovSize(mnem string, ops []*ast.Operand, fi arm64FrameInfo) int {
return 8 // ADD base, REGTMP + access
}
}
return 12 // the pool sequence: LDR literal + register-offset access
return 8 // the pool sequence: LDR literal + register-offset access
default:
return 4 // register move
}
@@ -1839,7 +1903,11 @@ func arm64Bitmask(v uint64, sf int) (N, immr, imms uint32, ok bool) {
} else {
N = 0
}
imms = uint32((^(esize - 1))&0x3F) | uint32(ones-1)
// The period marker rides the high imms bits one position above
// the element size: zero for 32 and 64 (N and the absence carry
// them), then 0x20, 0x30, 0x38 and 0x3C for 16, 8, 4 and 2. The
// toolchain's bitconEncode computes it as 63 & ^(period*2 - 1).
imms = ^uint32(uint32(esize*2-1))&0x3F | uint32(ones-1)
immr = uint32((esize - r) % esize)
return N, immr, imms, true
}
@@ -1929,7 +1997,7 @@ func encodeARM64RegMove(mnem string, src, dst *ast.Operand) ([]byte, error) {
// register after (post) or before (pre) the access; the offset must fit the
// unscaled 9-bit field and the base must be a real register, since a pseudo
// frame base cannot be written back.
func encodeARM64MemOp(mnem string, mem *ast.Operand, reg int, load bool, fi arm64FrameInfo, wb string) ([]byte, error) {
func encodeARM64MemOp(mnem string, mem *ast.Operand, reg int, load bool, fi arm64FrameInfo, wb string, pool *arm64Pool, poolBase int, pc int) ([]byte, error) {
rn, off := arm64MemWithFrame(mem, fi)
if rn < 0 {
return nil, fmt.Errorf("invalid memory operand")
@@ -1990,7 +2058,7 @@ func encodeARM64MemOp(mnem string, mem *ast.Operand, reg int, load bool, fi arm6
// ADD offsets from the operand's own base register, [SP] and [Rn]
// alike; beyond the split band the offset reaches the literal pool.
if !arm64OffsetSplitReach(off, lt) {
return nil, fmt.Errorf("%s: offset %d out of range (literal pool not supported)", mnem, off)
return arm64PoolAccess(mnem, lt, opc, off, rn, reg, pc, pool, poolBase)
}
hi, lo, ok := arm64SplitImm24(off, bits.TrailingZeros64(uint64(scale)))
if !ok {
@@ -2002,6 +2070,33 @@ func encodeARM64MemOp(mnem string, mem *ast.Operand, reg int, load bool, fi arm6
), nil
}
// arm64PoolAccess lowers an offset beyond the split band the way the
// toolchain's pool branch does: a PC-relative literal load of the offset
// into REGTMP, then the access against the register pair (asm7.go cases
// 30/31 and omovlit). The pooled words sit at poolBase plus the entry's
// offset, both function-relative, so the imm19 distance resolves here.
func arm64PoolAccess(mnem string, lt a64LSType, opc int, off int64, rn, reg, pc int, pool *arm64Pool, poolBase int) ([]byte, error) {
if pool == nil {
return nil, fmt.Errorf("%s: offset %d out of range (literal pool not supported)", mnem, off)
}
entryOff, w := pool.add(off)
dist := (poolBase + entryOff - pc) >> 2
if dist < -(1<<18) || dist >= 1<<18 {
return nil, fmt.Errorf("%s: literal pool %d out of 19-bit reach", mnem, dist<<2)
}
return a64WordsLE(
w<<30|3<<27|uint32(dist)&0x7FFFF<<5|27, // LDR R27, pool
a64LSReg(uint32(lt.size), uint32(lt.V), uint32(opc), 27, uint32(rn), uint32(reg)),
), nil
}
// a64LSReg encodes a load/store register (register offset) with the LSL-0
// option the toolchain's register-offset accesses use:
// size<<30 | 0x38<<24 | V<<26 | opc<<22 | 1<<21 | Rm<<16 | 011<<13 | 10<<10 | Rn<<5 | Rt.
func a64LSReg(size, V, opc, rm, rn, rt uint32) uint32 {
return size<<30 | 0x38<<24 | V<<26 | opc<<22 | 1<<21 | rm<<16 | 3<<13 | 2<<10 | rn<<5 | rt
}
// arm64OffsetSplitReach reports whether an offset stays within the band the
// toolchain lowers to ADD plus access instead of pooling (loadStoreClass):
// ±4095 for every width, then the width's own 24-bit band, aligned to the
@@ -3217,7 +3312,7 @@ func encodeARM64TestBranch(mnem string, baseOp uint32, ops []*ast.Operand, pc in
// addressing mode: "" the signed-offset form, "P" post-index, "W" pre-index;
// in the writeback forms the immediate is the amount added to the base
// register around the access.
func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, fi arm64FrameInfo, wb string, relocs *[]Reloc) ([]byte, error) {
func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, pc int, fi arm64FrameInfo, wb string, relocs *[]Reloc, pool *arm64Pool, poolBase int) ([]byte, error) {
if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops))
}
@@ -3294,7 +3389,19 @@ func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, fi arm64Fra
// from the base register into REGTMP, the high multiple of 0x1000 on top
// (asm7.go cases 75/77). Beyond the band the offset reaches the pool.
if off < 0 || off > 0xffffff {
return nil, fmt.Errorf("%s: offset %d out of range (literal pool not supported)", mnem, off)
if pool == nil {
return nil, fmt.Errorf("%s: offset %d out of range (literal pool not supported)", mnem, off)
}
entryOff, w := pool.add(off)
dist := (poolBase + entryOff - pc) >> 2
if dist < -(1<<18) || dist >= 1<<18 {
return nil, fmt.Errorf("%s: literal pool %d out of 19-bit reach", mnem, dist<<2)
}
return a64WordsLE(
w<<30|3<<27|uint32(dist)&0x7FFFF<<5|27, // LDR R27, pool
a64AddSubReg(1, 27, uint32(rn), 27), // ADD R27, Rn, R27
baseOp|uint32(rt2)<<10|27<<5|uint32(rt1), // LDP/STP (R27), (…)
), nil
}
hi, lo, ok := arm64SplitImm24(off, 0)
if !ok {
@@ -3307,19 +3414,54 @@ func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, fi arm64Fra
), nil
}
// a64AddSubReg encodes ADD/SUB (extended register) with the identity
// extend: sf | 0xB<<24 | 1<<21 | Rm<<16 | UXTX<<13 | Rn<<5 | Rd, the shape
// opxrrr emits for the pool's base addition.
func a64AddSubReg(sf, rm, rn, rd uint32) uint32 {
return sf<<31 | 0xB<<24 | 1<<21 | rm<<16 | 3<<13 | rn<<5 | rd
}
// arm64PairScale returns the byte width a pair access's imm7 offset divides
// by: four for the 32-bit pairs, sixteen for the 128-bit FP pairs, eight for
// everything else.
// by: four for the 32-bit pairs (integer W, signed W and the FP S pairs),
// sixteen for the 128-bit FP pairs, eight for everything else.
func arm64PairScale(mnem string) int64 {
switch {
case strings.HasSuffix(mnem, "W"):
return 4 // the 32-bit pairs, signed and unsigned
case strings.HasSuffix(mnem, "W"), mnem == "FLDPS", mnem == "FSTPS":
return 4
case strings.HasSuffix(mnem, "Q"):
return 16 // the 128-bit FP pairs
return 16
}
return 8
}
// arm64SpExtendOpt reports the extended-register encoding an ADD/SUB-family
// instruction takes when the stack pointer sits among the base and
// destination registers: the identity extend UXTX (UXTW in the 32-bit
// forms), how the toolchain spells a plain register operand against SP.
// ok is false for the logical and carry groups, whose register form has no
// extend field and admits no SP operand.
func arm64SpExtendOpt(mnem string, ops []*ast.Operand) (uint32, bool) {
sp := false
for _, op := range ops {
if n := operandRegName(op); n == "SP" || n == "RSP" {
sp = true
}
}
if !sp {
return 0, false
}
switch {
case strings.HasPrefix(mnem, "ADD"), strings.HasPrefix(mnem, "SUB"),
mnem == "CMP", mnem == "CMPW", mnem == "CMN", mnem == "CMNW":
opt := uint32(3) // UXTX
if strings.HasSuffix(mnem, "W") {
opt = 2 // UXTW
}
return opt, true
}
return 0, false
}
// arm64AddShift returns the ADD-immediate shift bit: a non-zero value whose
// low twelve bits are zero encodes shifted left by twelve.
func arm64AddShift(v int64) uint32 {
@@ -3396,7 +3538,7 @@ func encodeARM64Sys(mnem string, ops []*ast.Operand) ([]byte, error) {
// Operand-less returns and pointer-authentication hints.
if w, ok := map[string]uint32{"DRPS": 0xd6bf03e0, "ERET": 0xd69f03e0,
"AUTIASP": 0xd50323bf, "AUTIBSP": 0xd50323ff, "PACIASP": 0xd503233f, "PACIBSP": 0xd503237f,
"AUTIA1716": 0xd503211f, "AUTIB1716": 0xd503213f,
"AUTIA1716": 0xd503219f, "AUTIB1716": 0xd50321df,
"YIELD": 0xd503203d, "WFE": 0xd503205f, "WFI": 0xd503207f,
"SEVL": 0xd50320bf, "SEV": 0xd503209f}[mnem]; ok {
if len(ops) != 0 {
@@ -4533,6 +4675,25 @@ func moviLitName(mnem string, data []byte) string {
}
}
// arm64PoolPadLen returns the UNDEF word the pool guard needs: four bytes
// when the body's last instruction does not branch (the toolchain's
// flushpool inserts one so execution cannot fall through into the words),
// zero otherwise.
func arm64PoolPadLen(t *ast.Text) int {
for _, v := range slices.Backward(t.Body) {
in, ok := v.(*ast.Instr)
if !ok {
continue
}
switch strings.ToUpper(in.Mnemonic.Text) {
case "RET", "B", "JMP", "ERET":
return 0
}
return 4
}
return 4
}
// 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.
@@ -4562,6 +4723,60 @@ func (l *arm64Literals) add(name string, data []byte) string {
// list returns the literals in first-use order.
func (l *arm64Literals) list() []Arm64Literal { return l.order }
// arm64Pool collects the out-of-range load/store offsets a function pools.
// The toolchain appends them after the last instruction (asm7.go addpool and
// flushpool) and reaches them with PC-relative literal loads into REGTMP;
// equal values deduplicate to one entry regardless of which instruction
// pooled them first.
type arm64Pool struct {
order []arm64PoolEntry
seen map[int64]int // pooled value → entry index
size int // bytes the pool occupies so far
}
// arm64PoolEntry is one pooled constant: its bytes, its offset from the pool
// start and the literal-load width the first referrer selected (0 = LDR W,
// 2 = LDRSW for a negative word, 1 = LDR X for an 8-byte entry).
type arm64PoolEntry struct {
data []byte
off int
w uint32
}
// add interns a pooled value and returns its offset from the pool start and
// the literal-load width. A value beyond the 32-bit reach takes an
// eight-byte entry aligned to eight; a negative word takes the sign-extended
// load, the toolchain's omovlit choice for its AMOVD pool reference.
func (p *arm64Pool) add(v int64) (int, uint32) {
if i, ok := p.seen[v]; ok {
return p.order[i].off, p.order[i].w
}
wide := v != int64(int32(v)) || uint64(v) != uint64(uint32(v))
off := p.size
var data []byte
var w uint32
switch {
case wide:
w = 1 // LDR X
off = (p.size + 7) &^ 7
data = a64WordsLE(uint32(v), uint32(v>>32))
p.size = off + 8
case v < 0:
w = 2 // LDRSW, sign-extended to 64
data = a64wordLE(uint32(v))
p.size = off + 4
default:
data = a64wordLE(uint32(v))
p.size = off + 4
}
if p.seen == nil {
p.seen = map[int64]int{}
}
p.seen[v] = len(p.order)
p.order = append(p.order, arm64PoolEntry{data: data, off: off, w: w})
return off, w
}
// 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
+6 -2
View File
@@ -167,8 +167,9 @@ func TestArm64Bitmask(t *testing.T) {
{2, 1, 1, 63, 0, true}, // single bit at pos 1 (immr = esize-1)
{0, 1, 0, 0, 0, false}, // zero is not a bitmask
{0xFFFFFFFFFFFFFFFF, 1, 0, 0, 0, false}, // all ones is not a bitmask
{0x5555555555555555, 1, 0, 0, 0x3E, true}, // alternating bits (esize=2, ones=1)
{0x5555555555555555, 1, 0, 0, 0x3C, true}, // alternating bits (esize=2, ones=1)
{0xFFFFFFFF00000000, 1, 1, 32, 31, true}, // upper 32 bits set (esize=64, ones=32)
{0x3fffffc0, 0, 0, 26, 23, true}, // 24 ones at bit 6: the 32-bit period marker
}
for _, tt := range tests {
N, immr, imms, ok := arm64Bitmask(tt.v, tt.sf)
@@ -630,7 +631,8 @@ func TestArm64ADR(t *testing.T) {
func TestArm64PairLoadStore(t *testing.T) {
got := arm64Words(t, "\tSTP (R2, R3), 8(R5)\n\tLDP -8(R5), (R2, R3)\n\tLDPW 4(R0), (R1, R2)\n\tSTPW (R1, R2), 4(R0)\n"+
"\tFLDPD 8(R0), (F1, F2)\n\tFSTPD (F3, F4), -8(R5)\n"+
"\tFLDPQ 16(R0), (F1, F2)\n\tFSTPQ (F1, F2), 16(R0)\n")
"\tFLDPQ 16(R0), (F1, F2)\n\tFSTPQ (F1, F2), 16(R0)\n"+
"\tFLDPS 4(R0), (F1, F2)\n\tFSTPS (F1, F2), -4(R0)\n")
want := []uint32{
0xa9008ca2, // STP (R2, R3), 8(R5)
0xa97f8ca2, // LDP -8(R5), (R2, R3)
@@ -640,6 +642,8 @@ func TestArm64PairLoadStore(t *testing.T) {
0x6d3f90a3, // FSTPD (F3, F4), -8(R5)
0xad408801, // FLDPQ 16(R0), (F1, F2)
0xad008801, // FSTPQ (F1, F2), 16(R0)
0x2d408801, // FLDPS 4(R0), (F1, F2)
0x2d3f8801, // FSTPS (F1, F2), -4(R0)
0xd65f03c0,
}
if len(got) != len(want) {
+1
View File
@@ -69,6 +69,7 @@ TEXT ·pairs(SB), NOSPLIT, $0-0
FLDPD (R8), (F1, F2)
FLDPD 8(R8), (F3, F4)
FSTPD (F3, F4),-8(R9)
FLDPS 4(R8), (F1, F2)
FLDPQ 16(R8), (F1, F2)
FSTPQ (F1, F2), 48(R9)
RET
+123
View File
@@ -5,7 +5,13 @@ package verify
import (
"bytes"
"encoding/binary"
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"testing"
"sourcedock.dev/petrbalvin/gasm-sdk/asm"
@@ -30,6 +36,7 @@ func TestGroundTruthARM64(t *testing.T) {
"../testdata/verify/shifts_arm64.s",
"../testdata/verify/atomics_arm64.s",
"../testdata/verify/qmov_arm64.s",
"../testdata/verify/splits_arm64.s",
"../testdata/verify/crypto_arm64.s",
"../testdata/verify/integer_arm64.s",
"../testdata/verify/simd_arm64.s",
@@ -97,3 +104,119 @@ func TestGroundTruthARM64(t *testing.T) {
})
}
}
// TestGroundTruthARM64OffsetSweep sweeps load/store offsets across every
// band boundary the encoder branches on and compares the assembled bytes
// against the toolchain for the whole sweep in one go tool asm run. The
// offsets the toolchain itself rejects are dropped first, so the comparison
// pins the split and single-instruction encodings, not the error parity.
func TestGroundTruthARM64OffsetSweep(t *testing.T) {
if testing.Short() {
t.Skip("live go tool asm oracle: skipped in -short mode")
}
goBin, err := exec.LookPath("go")
if err != nil {
t.Skip("no Go toolchain available")
}
out, err := exec.Command(goBin, "env", "GOROOT").Output()
if err != nil {
t.Fatalf("go env GOROOT: %v", err)
}
includeDir := filepath.Join(strings.TrimSpace(string(out)), "pkg", "include")
bands := []int64{
0, 255, 256, 4094, 4095, 4096, 4097, 8190, 8191, 8192,
16380, 16384, 32760, 32768, 65504, 65520, 65536, 65552,
0xfff000, 0xfff000 + 8190, 0xfff000 + 16380, 0xfff000 + 32760,
0xfff000 + 65520, 0xfffff8, 0xffffff, 0x1000000, 0x1006ff8,
0xfff000 + 0xfff<<4, 0x1000ffe,
}
var offs []int64
for _, b := range bands {
for _, d := range []int64{-8, -2, -1, 0, 1, 2, 8} {
if v := b + d; v >= 0 {
offs = append(offs, v)
}
}
offs = append(offs, -b-1, -b, -b+1)
}
slices.Sort(offs)
offs = slices.Compact(offs)
// One function per mnemonic; a line the toolchain rejects (an unaligned
// band edge, a pool case gasm rejects until the pool lands) would fail
// the whole oracle run, so the sweep keeps only the offsets both
// assemblers accept: build per-mnemonic files and compare the ones that
// assemble on both sides.
for _, mnem := range []string{"MOVB", "MOVH", "MOVW", "MOVD", "FMOVS", "FMOVD", "FMOVQ", "LDP", "STP"} {
t.Run(mnem, func(t *testing.T) {
var body strings.Builder
for _, off := range offs {
switch mnem {
case "LDP":
fmt.Fprintf(&body, "\t%s\t%d(R2), (R1, R3)\n", mnem, off)
case "STP":
fmt.Fprintf(&body, "\t%s\t(R1, R3), %d(R2)\n", mnem, off)
case "FMOVS", "FMOVD", "FMOVQ":
fmt.Fprintf(&body, "\t%s\tF1, %d(R2)\n", mnem, off)
default:
fmt.Fprintf(&body, "\t%s\tR1, %d(R2)\n", mnem, off)
}
}
src := "TEXT ·f(SB), 7, $0-0\n" + body.String() + "\tRET\n"
dir := t.TempDir()
path := filepath.Join(dir, "sweep.s")
if err := os.WriteFile(path, []byte(src), 0o644); err != nil {
t.Fatal(err)
}
f, errs := parser.Parse(path, src)
if len(errs) > 0 {
t.Fatalf("parse: %v", errs)
}
gasmImg, gasmErr := asm.AssembleFileARM64(f)
objPath := filepath.Join(dir, "oracle.o")
cmd := exec.Command(goBin, "tool", "asm", "-I", includeDir, "-p", "sweep", "-o", objPath, path)
cmd.Env = append(os.Environ(), "GOARCH=arm64")
oracleOut, oracleErr := cmd.CombinedOutput()
if gasmErr != nil && oracleErr != nil {
return // both reject: agreement on the boundary
}
if gasmErr != nil {
t.Fatalf("gasm rejects what the toolchain accepts:\n%v", gasmErr)
}
if oracleErr != nil {
t.Fatalf("gasm accepts what the toolchain rejects:\n%s", oracleOut)
}
gt, err := GroundTruthARM64(path)
if err != nil {
t.Fatalf("ground truth: %v", err)
}
goCode, ok := gt["f"]
if !ok {
t.Fatal("oracle function missing")
}
for _, fn := range gasmImg.Funcs {
if fn.Name != "f" {
continue
}
gasmCode := gasmImg.Code[fn.Offset : fn.Offset+fn.Size]
cmpLen := min(len(goCode), len(gasmCode))
if !bytes.Equal(gasmCode[:cmpLen], goCode[:cmpLen]) {
for w := 0; w < cmpLen/4; w++ {
g := binary.LittleEndian.Uint32(gasmCode[w*4 : w*4+4])
o := binary.LittleEndian.Uint32(goCode[w*4 : w*4+4])
if g != o {
t.Fatalf("word %d (offset %d) differs: gasm %#08x go %#08x", w, w*4, g, o)
}
}
t.Fatalf("prefixes equal but lengths differ (gasm %d, go %d)", len(gasmCode), len(goCode))
}
for _, b := range goCode[len(gasmCode):] {
if b != 0 {
t.Fatalf("non-zero trailing oracle bytes (gasm %d, go %d)", len(gasmCode), len(goCode))
}
}
}
})
}
}