diff --git a/asm/arm64_assemble.go b/asm/arm64_assemble.go index c9e7144..e47af38 100644 --- a/asm/arm64_assemble.go +++ b/asm/arm64_assemble.go @@ -980,9 +980,13 @@ func encodeARM64DPSR(mnem string, baseOp uint32, ops []*ast.Operand) ([]byte, er } // Beyond the bitmask immediates, and for the ZR destinations, the // toolchain materialises the constant into REGTMP and uses the - // register form (asm7.go cases 62 and 13). BIC/ORN/EON read the - // written value, so the materialisation uses v before any - // inversion. + // register form (asm7.go cases 62 and 13). A REGTMP source + // register is refused, the materialisation clobbering it before + // the register form reads it. BIC/ORN/EON read the written + // value, so the materialisation uses v before any inversion. + if rn == 27 { + return nil, fmt.Errorf("%s: cannot use REGTMP as source", mnem) + } written := v if inverted { written = ^v @@ -1515,7 +1519,12 @@ func arm64AddSubImmWords(mnem string, v int64, rn, rd int, ext bool) ([]uint32, // Constant into REGTMP (R27), then the register form. The first word // mirrors omovconst (asm7.go case 62): MOVZ for a movcon value, MOVN for // the complement form, the bitmask ORR otherwise, and the full - // omovlconst sequence when no single word carries the value. + // omovlconst sequence when no single word carries the value. A REGTMP + // source register is refused: the materialisation would clobber it + // before the register form reads it (asm7.go cases 13 and 62). + if rn == 27 { + return nil, fmt.Errorf("%s: cannot use REGTMP as source", mnem) + } var seq []uint32 switch s := arm64Movcon(d); { case s >= 0: @@ -2284,7 +2293,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 arm64PoolAccess(mnem, lt, opc, off, rn, reg, pc, pool, poolBase) + return arm64PoolAccess(mnem, lt, opc, off, rn, reg, pc, pool, poolBase, load) } hi, lo, ok := arm64SplitImm24(off, bits.TrailingZeros64(uint64(scale))) if !ok { @@ -2301,7 +2310,16 @@ func encodeARM64MemOp(mnem string, mem *ast.Operand, reg int, load bool, fi arm6 // 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) { +func arm64PoolAccess(mnem string, lt a64LSType, opc int, off int64, rn, reg, pc int, pool *arm64Pool, poolBase int, load bool) ([]byte, error) { + // The toolchain refuses a REGTMP data register or base on the pool path + // (asm7.go cases 30/31): the literal load itself rides REGTMP. + if reg == 27 || rn == 27 { + kind := "load" + if !load { + kind = "store" + } + return nil, fmt.Errorf("%s: REGTMP used in large offset %s", mnem, kind) + } if pool == nil { return nil, fmt.Errorf("%s: offset %d out of range (literal pool not supported)", mnem, off) } @@ -3904,7 +3922,12 @@ func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, pc int, fi } // Offsets within ±4095 the imm7 field cannot carry move the whole // distance into REGTMP first (asm7.go cases 74/76: add/sub + ldp/stp). + // A store refuses a REGTMP pair member here (case 76: the add would + // clobber it before the store reads it); a load allows one. if off >= -4095 && off <= 4095 { + if !load && (rt1 == 27 || rt2 == 27) { + return nil, fmt.Errorf("%s: cannot use REGTMP as source", mnem) + } op, v := uint32(0), off // ADD if v < 0 { op, v = 1, -v // SUB @@ -3916,8 +3939,20 @@ func encodeARM64Pair(mnem string, baseOp uint32, ops []*ast.Operand, pc int, fi } // Positive offsets up to 16 MiB split into two ADDs: the low imm12 bits // 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, + // which refuses a REGTMP base outright and, for the stores, a REGTMP + // pair member as well. if off < 0 || off > 0xffffff { + if rn == 27 { + kind := "load" + if !load { + kind = "store" + } + return nil, fmt.Errorf("%s: REGTMP used in large offset %s", mnem, kind) + } + if !load && (rt1 == 27 || rt2 == 27) { + return nil, fmt.Errorf("%s: REGTMP used in large offset store", mnem) + } if pool == nil { return nil, fmt.Errorf("%s: offset %d out of range (literal pool not supported)", mnem, off) } diff --git a/asm/arm64_encode_test.go b/asm/arm64_encode_test.go index 2e948fc..1449a60 100644 --- a/asm/arm64_encode_test.go +++ b/asm/arm64_encode_test.go @@ -2267,6 +2267,52 @@ func TestArm64NoopVsNop(t *testing.T) { } } +// TestArm64RegtmpBoundaries pins the REGTMP acceptance the toolchain holds +// (probed against `go tool asm`, Go 1.27, arm64): the plain imm12 and the +// ±4095 offsets tolerate a REGTMP source or pair member, while every +// lowering that itself writes REGTMP (the constant materialisations, the +// pool path, the pair store expansions) refuses one, and the pool path +// refuses a REGTMP base on both the loads and the stores. +func TestArm64RegtmpBoundaries(t *testing.T) { + accept := []string{ + "\tADD\t$5, R27, R3\n", // imm12, no REGTMP sequence + "\tLDP\t700(R2), (R26, R27)\n", // load add/sub path: unchecked pair + "\tMOVD\tR27, 4000(R2)\n", // misaligned ±4095: unchecked data + "\tSTP\t(R26, R3), 700(R2)\n", // store add/sub path, no pair member + "\tLDP\t0x1234567(R2), (R27, R3)\n", // pool load: base alone decides + "\tMOVD\t$0x1000000(R27), R1\n", // lacon: case 34 reads the base + } + reject := []string{ + "\tADD\t$0x1234567, R27, R3\n", // materialisation, REGTMP source + "\tAND\t$0x22220000, R27, R4\n", // logical materialisation, ditto + "\tSTP\t(R26, R27), 700(R2)\n", // store add/sub path, pair member + "\tSTP\t(R3, R4), 0x1234567(R27)\n", // pool store, REGTMP base + "\tLDP\t0x1234567(R27), (R3, R4)\n", // pool load, REGTMP base + "\tSTP\t(R27, R3), 0x1234567(R2)\n", // pool store, REGTMP pair member + "\tMOVD\tR27, 0x1234567(R2)\n", // pool store, REGTMP data + "\tMOVD\t0x1234567(R2), R27\n", // pool load, REGTMP data + } + for _, src := range accept { + f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") + if len(errs) > 0 { + t.Errorf("expected acceptance for %q, got parse rejection", strings.TrimSpace(src)) + continue + } + if _, err := AssembleFileARM64(f); err != nil { + t.Errorf("expected acceptance for %q, got %v", strings.TrimSpace(src), err) + } + } + for _, src := range reject { + f, errs := parser.Parse("test_arm64.s", "#include \"textflag.h\"\n\nTEXT ·f(SB), NOSPLIT, $0-0\n"+src+"\tRET\n") + if len(errs) > 0 { + continue // a parse rejection is a rejection + } + if _, err := AssembleFileARM64(f); err == nil { + t.Errorf("expected rejection for %q, got nil", strings.TrimSpace(src)) + } + } +} + // TestArm64VLDSTPostIndexContract pins the structure-load post-index rules // against `go tool asm` (Go 1.27, arm64): the implicit by-size increment of // an unspelled or zero offset (both load and replicate forms, the words diff --git a/asm/arm64_errorparity_test.go b/asm/arm64_errorparity_test.go index f8c128f..5e7fdeb 100644 --- a/asm/arm64_errorparity_test.go +++ b/asm/arm64_errorparity_test.go @@ -20,13 +20,6 @@ var arm64AcceptedErrorShapes = []string{ // spelling, the .P immediate against the aggregate register size, the // REGTMP (R27) as an explicit operand the toolchain refuses, the pair // split paths included. - "ADD $0x1234567, R27, R3", - "ADD $0x3fffffffc000, R27, R5", - "AND $0x22220000, R27, R4", - "ANDW $0x6006000060060, R27, R5", - "STP (R3, R4), 0x1234567(R27)", - "LDP 0x1234567(R27), (R3, R4)", - "STP (R26, R27), 700(R2)", // TLBI operand arity and the range-prefetch operand shapes. "TLBI VMALLE1IS, R0", "TLBI ALLE3OS, ZR",