diff --git a/asm/arm64_assemble.go b/asm/arm64_assemble.go index b71856a..faf7130 100644 --- a/asm/arm64_assemble.go +++ b/asm/arm64_assemble.go @@ -2008,6 +2008,14 @@ func encodeARM64MemOp(mnem string, mem *ast.Operand, reg int, load bool, fi arm6 lt = a64LoadTable["MOVD"] } + // Register-offset addressing: (Rn)(Rm), (Rn)(Rm< 1 { + return nil, fmt.Errorf("%s: the register offset addressing mode takes no scale factor", mnem) + } + name, ext, _ := strings.Cut(mem.Addr.Index, ".") + var option uint32 + switch strings.ToUpper(ext) { + case "": + option = 3 // UXTX / LSL + case "UXTW": + option = 2 + case "SXTW": + option = 6 + case "SXTX": + option = 7 + default: + return nil, fmt.Errorf("%s: invalid shift for the register offset addressing mode: %s", mnem, ext) + } + rm := arm64RegNum(name) + if rm < 0 { + return nil, fmt.Errorf("%s: invalid index register %q", mnem, name) + } + amount := 0 + shift := strings.Join(strings.Fields(mem.Addr.Shift), "") + shift = strings.TrimSuffix(shift, ")") + if shift != "" { + if !strings.HasPrefix(shift, "<<") { + return nil, fmt.Errorf("%s: invalid shift for the register offset addressing mode: %s", mnem, shift) + } + v, err := strconv.Atoi(strings.TrimPrefix(shift, "<<")) + if err != nil { + return nil, fmt.Errorf("%s: invalid shift for the register offset addressing mode: %s", mnem, shift) + } + amount = v + } + log2 := bits.TrailingZeros64(uint64(a64LSScale(lt))) + if amount != 0 && amount != log2 { + return nil, fmt.Errorf("%s: invalid index shift amount %d", mnem, amount) + } + s := uint32(0) + if amount == log2 && log2 > 0 { + s = 1 + } + opc := lt.opc + if !load { + opc = a64StoreOpc(lt) + } + return a64wordLE(a64LSRegExt(uint32(lt.size), uint32(lt.V), uint32(opc), uint32(rm), uint32(rn), uint32(reg), option, s)), nil } // arm64OffsetSplitReach reports whether an offset stays within the band the diff --git a/asm/arm64_encode_test.go b/asm/arm64_encode_test.go index 6efc39d..9b8f7b9 100644 --- a/asm/arm64_encode_test.go +++ b/asm/arm64_encode_test.go @@ -1825,3 +1825,72 @@ func TestArm64SplitFrameSpadj(t *testing.T) { } } } + +// TestArm64RegOffsetEncodings pins the register-offset addressing forms of +// the MOV family against `go tool asm` words (Go 1.27, arm64): the LSL, +// UXTW, SXTW and SXTX options, the shift amount equal to the access size's +// log2, and the rejections the toolchain raises for the other spellings. +func TestArm64RegOffsetEncodings(t *testing.T) { + got := arm64Words(t, + "\tMOVD (R2)(R6.SXTW), R4\n"+ + "\tMOVD (R3)(R6), R5\n"+ + "\tMOVD (R2)(R6<<3), R4\n"+ + "\tMOVWU (R5)(R4.UXTW), R10\n"+ + "\tMOVW (R9)(R8.SXTW<<2), R19\n"+ + "\tMOVD (R3)(R7.SXTX<<3), R8\n"+ + "\tMOVBU (R10)(R6), R15\n"+ + "\tFMOVS (R2)(R6<<2), F4\n"+ + "\tFMOVD (R2)(R6), F4\n"+ + "\tMOVD R5, (R2)(R6<<3)\n"+ + "\tMOVW R7, (R3)(R4.SXTW)\n"+ + "\tMOVD ZR, (R6)(R7.SXTX<<3)\n") + want := []uint32{ + 0xf866c844, // MOVD (R2)(R6.SXTW), R4 + 0xf8666865, // MOVD (R3)(R6), R5 + 0xf8667844, // MOVD (R2)(R6<<3), R4 + 0xb86448aa, // MOVWU (R5)(R4.UXTW), R10 + 0xb8a8d933, // MOVW (R9)(R8.SXTW<<2), R19 + 0xf867f868, // MOVD (R3)(R7.SXTX<<3), R8 + 0x3866694f, // MOVBU (R10)(R6), R15 + 0xbc667844, // FMOVS (R2)(R6<<2), F4 + 0xfc666844, // FMOVD (R2)(R6), F4 + 0xf8267845, // MOVD R5, (R2)(R6<<3) + 0xb824c867, // MOVW R7, (R3)(R4.SXTW) + 0xf827f8df, // MOVD ZR, (R6)(R7.SXTX<<3) + 0xd65f03c0, // RET + } + if len(got) != len(want) { + t.Fatalf("word count = %d, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("word %d = %08x, want %08x", i, got[i], want[i]) + } + } +} + +// TestArm64RegOffsetRejections pins the spellings the toolchain refuses for +// the register-offset addressing: the UXTX/UXTB/SXTH extend names, a shift +// amount outside {0, log2(access)}, the scaled *k index, writeback with an +// index, and the FMOVQ form the toolchain rejects outright. +func TestArm64RegOffsetRejections(t *testing.T) { + for _, src := range []string{ + "\tMOVD\t(R2)(R3.UXTX), R1\n", + "\tMOVD\t(R2)(R3.UXTB), R1\n", + "\tMOVD\t(R2)(R3.SXTH), R1\n", + "\tMOVD\t(R2)(R3<<1), R1\n", + "\tMOVD\t(R2)(R3<<2), R1\n", + "\tMOVBU\t(R1)(R2<<1), R3\n", + "\tMOVD\t(R2)(R3*8), R1\n", + "\tMOVD.P\tR5, (R2)(R3)\n", + "\tFMOVQ\t(R2)(R6), F4\n", + } { + 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)) + } + } +} diff --git a/testdata/verify/regoffset_arm64.s b/testdata/verify/regoffset_arm64.s new file mode 100644 index 0000000..917608b --- /dev/null +++ b/testdata/verify/regoffset_arm64.s @@ -0,0 +1,71 @@ +// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) +// SPDX-License-Identifier: BSD-3-Clause + +// Differential kernel for the arm64 register-offset addressing forms: +// (Rn)(Rm), (Rn)(Rm<