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 ( import (
"fmt" "fmt"
"math/bits" "math/bits"
"slices"
"strconv" "strconv"
"strings" "strings"
@@ -43,6 +44,7 @@ func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
var relocs []Reloc var relocs []Reloc
var spadj []SpadjStep var spadj []SpadjStep
lits := &arm64Literals{} lits := &arm64Literals{}
pool := &arm64Pool{}
// The prologue (3 instructions when a small frame, 4 for large) // The prologue (3 instructions when a small frame, 4 for large)
// raises the SP delta by autosize. The guard prefix shifts its PC. // 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...) out = append(out, prologue...)
pc := guardLen + len(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) preCount := len(relocs)
var lines []LineEntry var lines []LineEntry
for _, stmt := range t.Body { for _, stmt := range t.Body {
@@ -107,7 +115,7 @@ func assembleARM64(t *ast.Text) ([]byte, map[string]int, []Reloc, []LineEntry, [
} }
continue 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 { if err != nil {
return nil, nil, nil, nil, nil, nil, fmt.Errorf("%s: %w", in.Mnemonic.Text, err) 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) relocs = append(relocs, blReloc)
pc += len(block) 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 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 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) 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 // encodeARM64Instr encodes a single AArch64 instruction. lits collects the
// read-only literals a VMOVS/VMOVD/VMOVQ constant load needs; the file // read-only literals a VMOVS/VMOVD/VMOVQ constant load needs; the file
// assembler lays them out once every function is encoded. // 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) mnem := strings.ToUpper(instr.Mnemonic.Text)
ops := instr.Operands 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) return encodeARM64Branch(mnem, ops, pc, offsets, true, relocs, resolve)
case "MOV", "MOVD", "MOVW", "MOVWU", "MOVH", "MOVHU", "MOVB", "MOVBU", case "MOV", "MOVD", "MOVW", "MOVWU", "MOVH", "MOVHU", "MOVB", "MOVBU",
"FMOVS", "FMOVD", "FMOVQ": "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 // 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 { if base, wb, ok := arm64WritebackSuffix(mnem); ok {
switch { switch {
case isARM64MovMnemonic(base): 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: 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 // Load/store pairs (LDP, STP, LDPW, STPW, FLDPD, FSTPD). The .P/.W
// writeback forms are routed earlier, straight from the mnemonic. // writeback forms are routed earlier, straight from the mnemonic.
if enc, ok := a64InstrTable[mnem]; ok && enc.format == a64FPair { 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. // 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 { 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 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 opc := (baseOp >> 29) & 7
sf := (baseOp >> 31) & 1 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 { if rm < 0 || rn < 0 || rd < 0 {
return nil, fmt.Errorf("invalid register operand in %s", mnem) 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 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 { if rm < 0 || rn < 0 || rd < 0 {
return nil, fmt.Errorf("invalid register operand in %s", mnem) 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 return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd)), nil
case 2: case 2:
// ADC family carries an immediate spelling: ADC $0, Rd reads the // 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 { if rm < 0 || rn < 0 {
return nil, fmt.Errorf("invalid register operand in %s", mnem) 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 return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rn)<<5 | 31), nil
} }
if isNeg { if isNeg {
@@ -1214,6 +1257,12 @@ func encodeARM64MAddSub(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte,
if rm < 0 || rn < 0 || rd < 0 { if rm < 0 || rn < 0 || rd < 0 {
return nil, fmt.Errorf("invalid register operand in %s", mnem) 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 return a64wordLE(baseOp | uint32(rm)<<16 | uint32(rn)<<5 | uint32(rd)), nil
} }
if len(ops) != 4 { 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" { if mnem == "CMP" || mnem == "CMPW" || mnem == "CMN" || mnem == "CMNW" {
rd = 31 // ZR 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 { if err != nil {
return nil, fmt.Errorf("%s: %w", mnem, err) 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 // 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 // their W forms. rn and rd are resolved register numbers (a comparison
// discards rd, so the caller passes 31). // 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") w := strings.HasSuffix(mnem, "W")
sf := uint32(1) // 64-bit sf := uint32(1) // 64-bit
d := v d := v
@@ -1335,7 +1395,17 @@ func arm64AddSubImmWords(mnem string, v int64, rn, rd int) ([]uint32, error) {
seq = arm64MovLConst(d, sf) seq = arm64MovLConst(d, sf)
} }
// The register form reads REGTMP: Rd = Rn op R27 (opxrrr/oprrr). // 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] { for i := range seq[:len(seq)-1] {
seq[i] |= 27 // REGTMP 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. // 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 // Storing a $0 immediate stores ZR; any other immediate is rejected, matching
// the toolchain. // 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 ops := instr.Operands
if len(ops) != 2 { if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops)) 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 { if rd < 0 {
return nil, fmt.Errorf("%s: invalid destination register", mnem) 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): case isMemOperand(dst) && !isMemOperand(src):
rs := arm64RegNum(operandRegName(src)) rs := arm64RegNum(operandRegName(src))
if rs < 0 { 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. // Storing a constant zero stores the zero register.
rs = 31 rs = 31
} }
return encodeARM64MemOp(mnem, dst, rs, false, fi, wb) return encodeARM64MemOp(mnem, dst, rs, false, fi, wb, pool, poolBase, pc)
default: default:
return nil, fmt.Errorf("%s: writeback form needs a register and a memory operand", mnem) 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 { if arm64Imm64(src) != 0 {
return nil, fmt.Errorf("%s: illegal combination: an immediate store must be zero", mnem) 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)) rd := arm64RegNum(operandRegName(dst))
if rd < 0 { if rd < 0 {
@@ -1591,14 +1661,14 @@ func encodeARM64Mov(instr *ast.Instr, mnem string, wb string, fi arm64FrameInfo,
if rd < 0 { if rd < 0 {
return nil, fmt.Errorf("%s: invalid destination register", mnem) 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) { if !arm64IsMemOperand(src) && arm64IsMemOperand(dst) {
rs := arm64RegNum(operandRegName(src)) rs := arm64RegNum(operandRegName(src))
if rs < 0 { if rs < 0 {
return nil, fmt.Errorf("%s: invalid source register", mnem) 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. // Register → register.
@@ -1621,9 +1691,6 @@ func arm64PairSize(mnem string, ops []*ast.Operand, fi arm64FrameInfo) int {
return 12 // ADRP + ADD + pair return 12 // ADRP + ADD + pair
} }
_, off := arm64MemWithFrame(memOp, fi) _, off := arm64MemWithFrame(memOp, fi)
if off < 0 {
return 4 // the encoder rejects it in pass 2
}
scale := arm64PairScale(mnem) scale := arm64PairScale(mnem)
if off%scale == 0 && off >= -64*scale && off <= 63*scale { if off%scale == 0 && off >= -64*scale && off <= 63*scale {
return 4 return 4
@@ -1631,10 +1698,7 @@ func arm64PairSize(mnem string, ops []*ast.Operand, fi arm64FrameInfo) int {
if off >= -4095 && off <= 4095 { if off >= -4095 && off <= 4095 {
return 8 // ADD/SUB the whole offset into REGTMP + pair return 8 // ADD/SUB the whole offset into REGTMP + pair
} }
if off >= 0 && off <= 0xffffff { return 12 // the two-ADD split within 16 MiB, or the pool sequence
return 12 // the two-ADD split
}
return 12 // the pool sequence: LDR literal + ADD + pair
} }
// arm64MovSize returns the encoded size of a MOV instruction. // 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 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: default:
return 4 // register move return 4 // register move
} }
@@ -1839,7 +1903,11 @@ func arm64Bitmask(v uint64, sf int) (N, immr, imms uint32, ok bool) {
} else { } else {
N = 0 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) immr = uint32((esize - r) % esize)
return N, immr, imms, true 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 // 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 // unscaled 9-bit field and the base must be a real register, since a pseudo
// frame base cannot be written back. // 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) rn, off := arm64MemWithFrame(mem, fi)
if rn < 0 { if rn < 0 {
return nil, fmt.Errorf("invalid memory operand") 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] // ADD offsets from the operand's own base register, [SP] and [Rn]
// alike; beyond the split band the offset reaches the literal pool. // alike; beyond the split band the offset reaches the literal pool.
if !arm64OffsetSplitReach(off, lt) { 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))) hi, lo, ok := arm64SplitImm24(off, bits.TrailingZeros64(uint64(scale)))
if !ok { if !ok {
@@ -2002,6 +2070,33 @@ func encodeARM64MemOp(mnem string, mem *ast.Operand, reg int, load bool, fi arm6
), nil ), 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 // arm64OffsetSplitReach reports whether an offset stays within the band the
// toolchain lowers to ADD plus access instead of pooling (loadStoreClass): // 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 // ±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; // 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 // in the writeback forms the immediate is the amount added to the base
// register around the access. // 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 { if len(ops) != 2 {
return nil, fmt.Errorf("%s expects 2 operands, got %d", mnem, len(ops)) 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 // 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. // (asm7.go cases 75/77). Beyond the band the offset reaches the pool.
if off < 0 || off > 0xffffff { 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) hi, lo, ok := arm64SplitImm24(off, 0)
if !ok { if !ok {
@@ -3307,19 +3414,54 @@ func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, fi arm64Fra
), nil ), 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 // 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 // by: four for the 32-bit pairs (integer W, signed W and the FP S pairs),
// everything else. // sixteen for the 128-bit FP pairs, eight for everything else.
func arm64PairScale(mnem string) int64 { func arm64PairScale(mnem string) int64 {
switch { switch {
case strings.HasSuffix(mnem, "W"): case strings.HasSuffix(mnem, "W"), mnem == "FLDPS", mnem == "FSTPS":
return 4 // the 32-bit pairs, signed and unsigned return 4
case strings.HasSuffix(mnem, "Q"): case strings.HasSuffix(mnem, "Q"):
return 16 // the 128-bit FP pairs return 16
} }
return 8 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 // arm64AddShift returns the ADD-immediate shift bit: a non-zero value whose
// low twelve bits are zero encodes shifted left by twelve. // low twelve bits are zero encodes shifted left by twelve.
func arm64AddShift(v int64) uint32 { 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. // Operand-less returns and pointer-authentication hints.
if w, ok := map[string]uint32{"DRPS": 0xd6bf03e0, "ERET": 0xd69f03e0, if w, ok := map[string]uint32{"DRPS": 0xd6bf03e0, "ERET": 0xd69f03e0,
"AUTIASP": 0xd50323bf, "AUTIBSP": 0xd50323ff, "PACIASP": 0xd503233f, "PACIBSP": 0xd503237f, "AUTIASP": 0xd50323bf, "AUTIBSP": 0xd50323ff, "PACIASP": 0xd503233f, "PACIBSP": 0xd503237f,
"AUTIA1716": 0xd503211f, "AUTIB1716": 0xd503213f, "AUTIA1716": 0xd503219f, "AUTIB1716": 0xd50321df,
"YIELD": 0xd503203d, "WFE": 0xd503205f, "WFI": 0xd503207f, "YIELD": 0xd503203d, "WFE": 0xd503205f, "WFI": 0xd503207f,
"SEVL": 0xd50320bf, "SEV": 0xd503209f}[mnem]; ok { "SEVL": 0xd50320bf, "SEV": 0xd503209f}[mnem]; ok {
if len(ops) != 0 { 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 // arm64Literals collects the read-only constants the VMOVS/VMOVD/VMOVQ
// loads refer to. Names follow the toolchain's $i32/$i64/$i128 spellings so // loads refer to. Names follow the toolchain's $i32/$i64/$i128 spellings so
// equal constants deduplicate to one literal. // 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. // list returns the literals in first-use order.
func (l *arm64Literals) list() []Arm64Literal { return l.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 // AssembleFileARM64 assembles every TEXT function of a parsed arm64 file
// and lays out its static symbols (GLOBL/DATA) in a data section behind the // 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 // 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) {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 {0, 1, 0, 0, 0, false}, // zero is not a bitmask
{0xFFFFFFFFFFFFFFFF, 1, 0, 0, 0, false}, // all ones 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) {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 { for _, tt := range tests {
N, immr, imms, ok := arm64Bitmask(tt.v, tt.sf) N, immr, imms, ok := arm64Bitmask(tt.v, tt.sf)
@@ -630,7 +631,8 @@ func TestArm64ADR(t *testing.T) {
func TestArm64PairLoadStore(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"+ 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"+ "\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{ want := []uint32{
0xa9008ca2, // STP (R2, R3), 8(R5) 0xa9008ca2, // STP (R2, R3), 8(R5)
0xa97f8ca2, // LDP -8(R5), (R2, R3) 0xa97f8ca2, // LDP -8(R5), (R2, R3)
@@ -640,6 +642,8 @@ func TestArm64PairLoadStore(t *testing.T) {
0x6d3f90a3, // FSTPD (F3, F4), -8(R5) 0x6d3f90a3, // FSTPD (F3, F4), -8(R5)
0xad408801, // FLDPQ 16(R0), (F1, F2) 0xad408801, // FLDPQ 16(R0), (F1, F2)
0xad008801, // FSTPQ (F1, F2), 16(R0) 0xad008801, // FSTPQ (F1, F2), 16(R0)
0x2d408801, // FLDPS 4(R0), (F1, F2)
0x2d3f8801, // FSTPS (F1, F2), -4(R0)
0xd65f03c0, 0xd65f03c0,
} }
if len(got) != len(want) { if len(got) != len(want) {
+1
View File
@@ -69,6 +69,7 @@ TEXT ·pairs(SB), NOSPLIT, $0-0
FLDPD (R8), (F1, F2) FLDPD (R8), (F1, F2)
FLDPD 8(R8), (F3, F4) FLDPD 8(R8), (F3, F4)
FSTPD (F3, F4),-8(R9) FSTPD (F3, F4),-8(R9)
FLDPS 4(R8), (F1, F2)
FLDPQ 16(R8), (F1, F2) FLDPQ 16(R8), (F1, F2)
FSTPQ (F1, F2), 48(R9) FSTPQ (F1, F2), 48(R9)
RET RET
+123
View File
@@ -5,7 +5,13 @@ package verify
import ( import (
"bytes" "bytes"
"encoding/binary"
"fmt"
"os" "os"
"os/exec"
"path/filepath"
"slices"
"strings"
"testing" "testing"
"sourcedock.dev/petrbalvin/gasm-sdk/asm" "sourcedock.dev/petrbalvin/gasm-sdk/asm"
@@ -30,6 +36,7 @@ func TestGroundTruthARM64(t *testing.T) {
"../testdata/verify/shifts_arm64.s", "../testdata/verify/shifts_arm64.s",
"../testdata/verify/atomics_arm64.s", "../testdata/verify/atomics_arm64.s",
"../testdata/verify/qmov_arm64.s", "../testdata/verify/qmov_arm64.s",
"../testdata/verify/splits_arm64.s",
"../testdata/verify/crypto_arm64.s", "../testdata/verify/crypto_arm64.s",
"../testdata/verify/integer_arm64.s", "../testdata/verify/integer_arm64.s",
"../testdata/verify/simd_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))
}
}
}
})
}
}