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:
1 parent
71e8dd550d
commit
a0fa7e802c
4 files changed
+380
-37
No files matched your search
+250
-35
@@ -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,8 +3389,20 @@ 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 {
|
||||||
|
if pool == nil {
|
||||||
return nil, fmt.Errorf("%s: offset %d out of range (literal pool not supported)", mnem, off)
|
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 {
|
||||||
return nil, fmt.Errorf("%s: offset %d out of range (literal pool not supported)", mnem, off)
|
return nil, fmt.Errorf("%s: offset %d out of range (literal pool not supported)", mnem, off)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Vendored
+1
@@ -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
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user