[llvm] [AArch64] Fix miscompilation due to integer overflow in immediate offset for stack store/load instructions with preserve_all (PR #207026)

Jerry Dang via llvm-commits llvm-commits at lists.llvm.org
Wed Jul 8 19:12:09 PDT 2026


https://github.com/kuroyukiasuna updated https://github.com/llvm/llvm-project/pull/207026

>From ce8bffc8b7e1b140c1575caf512e3053577bd11f Mon Sep 17 00:00:00 2001
From: Jerry Dang <kuroyukiasuna at gmail.com>
Date: Wed, 1 Jul 2026 11:49:05 -0400
Subject: [PATCH 1/5] [AArch64 backend] Miscompile fix: Avoid out-of-range
 STP/LDP for large callee-save areas. Only pair a register when the resulting
 offset fits the LDP/STP immediate; otherwise leave it unpaired and
 spill/reload it with a single STR/LDR, whose 12-bit unsigned scaled immediate
 (up to 32760 bytes) easily covers any callee-save area.

Accounting fix: Don't double count sub/super register overlap in CSR size
---
 .../Target/AArch64/AArch64FrameLowering.cpp   | 33 ++++++++++++++++---
 .../CodeGen/AArch64/framelayout-fpr128-csr.ll | 12 +++----
 .../AArch64/framelayout-fpr128-spill.mir      |  4 +--
 .../CodeGen/AArch64/preserve-all-large-csr.ll | 31 +++++++++++++++++
 4 files changed, 68 insertions(+), 12 deletions(-)
 create mode 100644 llvm/test/CodeGen/AArch64/preserve-all-large-csr.ll

diff --git a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
index 026f807124d2f..777aaed23baef 100644
--- a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
@@ -1784,27 +1784,39 @@ void computeCalleeSaveRegisterPairs(const AArch64FrameLowering &AFL,
 
     bool NeedsWinCFI = AFL.needsWinCFI(MF);
     int Scale = TRI->getSpillSize(*RPI.RC);
+    // A paired LDP/STP can only encode a signed 7-bit scaled offset ([-64,
+    // 63]). When the callee-save area is large (e.g. preserve_all spills 30+
+    // registers), the highest pairs fall outside that range. Only pair if the
+    // resulting offset is encodable; otherwise leave the register unpaired and
+    // let it be spilled with a single STR/LDR, which has a much wider 12-bit
+    // immediate.
+    auto PairFitsImmRange = [&]() {
+      int PairOffset =
+          IsWindows ? ByteOffset : ByteOffset + StackFillDir * 2 * Scale;
+      int Scaled = PairOffset / Scale;
+      return Scaled >= -64 && Scaled <= 63;
+    };
     // Add the next reg to the pair if it is in the same register class.
     if (unsigned(i + RegInc) < Count && !HasCSHazardPadding) {
       MCRegister NextReg = CSI[i + RegInc].getReg();
       unsigned SpillCount = NeedsWinCFI ? FirstReg - i : i;
       switch (RPI.Type) {
       case RegPairInfo::GPR:
-        if (AArch64::GPR64RegClass.contains(NextReg) &&
+        if (AArch64::GPR64RegClass.contains(NextReg) && PairFitsImmRange() &&
             !invalidateRegisterPairing(SpillExtendedVolatile, SpillCount,
                                        RPI.Reg1, NextReg, IsWindows,
                                        NeedsWinCFI, NeedsFrameRecord, TRI))
           RPI.Reg2 = NextReg;
         break;
       case RegPairInfo::FPR64:
-        if (AArch64::FPR64RegClass.contains(NextReg) &&
+        if (AArch64::FPR64RegClass.contains(NextReg) && PairFitsImmRange() &&
             !invalidateRegisterPairing(SpillExtendedVolatile, SpillCount,
                                        RPI.Reg1, NextReg, IsWindows,
                                        NeedsWinCFI, NeedsFrameRecord, TRI))
           RPI.Reg2 = NextReg;
         break;
       case RegPairInfo::FPR128:
-        if (AArch64::FPR128RegClass.contains(NextReg))
+        if (AArch64::FPR128RegClass.contains(NextReg) && PairFitsImmRange())
           RPI.Reg2 = NextReg;
         break;
       case RegPairInfo::PPR:
@@ -2631,17 +2643,30 @@ void AArch64FrameLowering::determineCalleeSaves(MachineFunction &MF,
   unsigned ZPRCSStackSize = 0;
   unsigned PPRCSStackSize = 0;
   const TargetRegisterInfo *TRI = MF.getSubtarget().getRegisterInfo();
+  // A register and its super-register can both appear in SavedRegs (e.g. for
+  // preserve_all, AAPCS contributes D8-D15 while the extended convention
+  // contributes the enclosing Q8-Q31). Only the widest register is actually
+  // spilled, skip such sub-registers here to avoid double-counting the overlap.
+  BitVector CSMask(SavedRegs.size());
+  for (unsigned i = 0; CSRegs[i]; ++i)
+    CSMask.set(CSRegs[i]);
   for (unsigned Reg : SavedRegs.set_bits()) {
     auto *RC = TRI->getMinimalPhysRegClass(MCRegister(Reg));
     assert(RC && "expected register class!");
     auto SpillSize = TRI->getSpillSize(*RC);
     bool IsZPR = AArch64::ZPRRegClass.contains(Reg);
     bool IsPPR = !IsZPR && AArch64::PPRRegClass.contains(Reg);
+    bool SavedSuper = false;
+    for (MCPhysReg SuperReg : TRI->superregs(MCRegister(Reg)))
+      if (SavedRegs.test(SuperReg) && CSMask.test(SuperReg)) {
+        SavedSuper = true;
+        break;
+      }
     if (IsZPR)
       ZPRCSStackSize += SpillSize;
     else if (IsPPR)
       PPRCSStackSize += SpillSize;
-    else
+    else if (!SavedSuper)
       CSStackSize += SpillSize;
   }
 
diff --git a/llvm/test/CodeGen/AArch64/framelayout-fpr128-csr.ll b/llvm/test/CodeGen/AArch64/framelayout-fpr128-csr.ll
index 4cce7ec8a47cd..98275757cb5e5 100644
--- a/llvm/test/CodeGen/AArch64/framelayout-fpr128-csr.ll
+++ b/llvm/test/CodeGen/AArch64/framelayout-fpr128-csr.ll
@@ -3,18 +3,18 @@
 ; RUN: llc -verify-machineinstrs -mtriple=aarch64-windows-msvc < %s | FileCheck %s --check-prefix=CHECK-WINDOWS
 
 ; The purpose of this test is to verify q8 is assigned a 16-byte aligned offset
-; after the x10 is assigned an offset. The CSRs (on Linux) are assigned offsets
-; in the order GPRs then FPRs. The stack size of this function is 32
-; (alignTo((16 + 8), 16)), so after x8 is given the offset 24, q8 originally
-; would be assigned offset 8, which is not 16-byte aligned.
+; when a GPR (x10) is also callee-saved. The CSRs (on Linux) are assigned
+; offsets in the order GPRs then FPRs. The stack size of this function is 32
+; (alignTo((16 + 8), 16)); q8 must be assigned the 16-byte aligned offset 0,
+; with x10 in the 8-byte slot above it and the alignment padding at the top.
 define preserve_allcc void @d(ptr %ptr) nounwind {
 ; CHECK-LABEL: d:
 ; CHECK:       // %bb.0: // %entry
 ; CHECK-NEXT:    str q8, [sp, #-32]! // 16-byte Folded Spill
-; CHECK-NEXT:    str x10, [sp, #24] // 8-byte Spill
+; CHECK-NEXT:    str x10, [sp, #16] // 8-byte Spill
 ; CHECK-NEXT:    //APP
 ; CHECK-NEXT:    //NO_APP
-; CHECK-NEXT:    ldr x10, [sp, #24] // 8-byte Reload
+; CHECK-NEXT:    ldr x10, [sp, #16] // 8-byte Reload
 ; CHECK-NEXT:    ldr q8, [sp], #32 // 16-byte Folded Reload
 ; CHECK-NEXT:    ret
 ;
diff --git a/llvm/test/CodeGen/AArch64/framelayout-fpr128-spill.mir b/llvm/test/CodeGen/AArch64/framelayout-fpr128-spill.mir
index a6236bd917129..dff5841442c45 100644
--- a/llvm/test/CodeGen/AArch64/framelayout-fpr128-spill.mir
+++ b/llvm/test/CodeGen/AArch64/framelayout-fpr128-spill.mir
@@ -15,10 +15,10 @@ body: |
     ; CHECK: liveins: $q8, $x10
     ; CHECK-NEXT: {{  $}}
     ; CHECK-NEXT: early-clobber $sp = frame-setup STRQpre killed $q8, $sp, -32 :: (store (s128) into %stack.1)
-    ; CHECK-NEXT: frame-setup STRXui killed $x10, $sp, 3 :: (store (s64) into %stack.0)
+    ; CHECK-NEXT: frame-setup STRXui killed $x10, $sp, 2 :: (store (s64) into %stack.0)
     ; CHECK-NEXT: $q8 = IMPLICIT_DEF
     ; CHECK-NEXT: $x10 = IMPLICIT_DEF
-    ; CHECK-NEXT: $x10 = frame-destroy LDRXui $sp, 3 :: (load (s64) from %stack.0)
+    ; CHECK-NEXT: $x10 = frame-destroy LDRXui $sp, 2 :: (load (s64) from %stack.0)
     ; CHECK-NEXT: early-clobber $sp, $q8 = frame-destroy LDRQpost $sp, 32 :: (load (s128) from %stack.1)
     ; CHECK-NEXT: RET_ReallyLR
     ;
diff --git a/llvm/test/CodeGen/AArch64/preserve-all-large-csr.ll b/llvm/test/CodeGen/AArch64/preserve-all-large-csr.ll
new file mode 100644
index 0000000000000..2e621606d14c8
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/preserve-all-large-csr.ll
@@ -0,0 +1,31 @@
+; RUN: llc -mtriple=aarch64-linux-gnu -verify-machineinstrs < %s | FileCheck %s
+
+; The preserve_all calling convention promotes almost all registers to
+; callee-saved, so a function that clobbers them all spills a callee-save area
+; larger than the +/-504 byte reach of the paired LDP/STP scaled 7-bit
+; immediate. Registers whose offset exceeds that range must be spilled and
+; reloaded with a single STR/LDR (which has a wider 12-bit immediate) instead
+; of an out-of-range STP/LDP.
+;
+; This also covers the related callee-save size accounting: D8-D15 (AAPCS) and
+; the enclosing Q8-Q15 (preserve_all) must not be double-counted, otherwise the
+; frame is over-sized and the prologue emits a redundant SP adjustment.
+
+; CHECK-LABEL: trigger_stack_spill:
+; The last in-range pair sits at the +504 boundary; the registers above it are
+; spilled as single STR (offset > 504, unencodable as a pair).
+; CHECK:      stp x22, x21, [sp, #504]
+; CHECK-NEXT: str x20, [sp, #520]
+; CHECK-NEXT: str x19, [sp, #528]
+; The unwind offsets must match where the registers are actually stored.
+; CHECK:      .cfi_offset w19, -16
+; CHECK:      .cfi_offset w20, -24
+; The epilogue reloads them symmetrically with single LDR.
+; CHECK-DAG:  ldr x19, [sp, #528]
+; CHECK-DAG:  ldr x20, [sp, #520]
+
+define preserve_allcc void @trigger_stack_spill() {
+entry:
+  call void asm sideeffect "", "~{x0},~{x1},~{x2},~{x3},~{x4},~{x5},~{x6},~{x7},~{x8},~{x9},~{x10},~{x11},~{x12},~{x13},~{x14},~{x15},~{x16},~{x17},~{x18},~{x19},~{x20},~{x21},~{x22},~{x23},~{x24},~{x25},~{x26},~{x27},~{x28},~{fp},~{lr},~{v0},~{v1},~{v2},~{v3},~{v4},~{v5},~{v6},~{v7},~{v8},~{v9},~{v10},~{v11},~{v12},~{v13},~{v14},~{v15},~{v16},~{v17},~{v18},~{v19},~{v20},~{v21},~{v22},~{v23},~{v24},~{v25},~{v26},~{v27},~{v28},~{v29},~{v30},~{v31},~{memory},~{cc}"()
+  ret void
+}

>From e21a5857d789901f07ead88d93efe2ace51ccac8 Mon Sep 17 00:00:00 2001
From: Jerry Dang <kuroyukiasuna at gmail.com>
Date: Wed, 1 Jul 2026 15:59:30 -0400
Subject: [PATCH 2/5] Address comments on test cases

---
 .../CodeGen/AArch64/framelayout-fpr128-csr.ll |  28 +++
 .../CodeGen/AArch64/preserve-all-large-csr.ll | 220 ++++++++++++++++--
 2 files changed, 235 insertions(+), 13 deletions(-)

diff --git a/llvm/test/CodeGen/AArch64/framelayout-fpr128-csr.ll b/llvm/test/CodeGen/AArch64/framelayout-fpr128-csr.ll
index 98275757cb5e5..c4e384277502e 100644
--- a/llvm/test/CodeGen/AArch64/framelayout-fpr128-csr.ll
+++ b/llvm/test/CodeGen/AArch64/framelayout-fpr128-csr.ll
@@ -31,3 +31,31 @@ entry:
   tail call void asm sideeffect "", "~{x10},~{q8}"()
   ret void
 }
+
+; A GPR pair (x10, x11) plus q8 fills the frame exactly (16 + 8 + 8 = 32) with
+; no padding. q8 must still be assigned a 16-byte aligned offset, and the
+; d8/q8 sub/super-register overlap must not inflate the callee-save size.
+define preserve_allcc void @e(ptr %ptr) nounwind {
+; CHECK-LABEL: e:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    str q8, [sp, #-32]! // 16-byte Folded Spill
+; CHECK-NEXT:    stp x11, x10, [sp, #16] // 16-byte Folded Spill
+; CHECK-NEXT:    //APP
+; CHECK-NEXT:    //NO_APP
+; CHECK-NEXT:    ldp x11, x10, [sp, #16] // 16-byte Folded Reload
+; CHECK-NEXT:    ldr q8, [sp], #32 // 16-byte Folded Reload
+; CHECK-NEXT:    ret
+;
+; CHECK-WINDOWS-LABEL: e:
+; CHECK-WINDOWS:       // %bb.0: // %entry
+; CHECK-WINDOWS-NEXT:    stp x10, x11, [sp, #-32]! // 16-byte Folded Spill
+; CHECK-WINDOWS-NEXT:    str q8, [sp, #16] // 16-byte Spill
+; CHECK-WINDOWS-NEXT:    //APP
+; CHECK-WINDOWS-NEXT:    //NO_APP
+; CHECK-WINDOWS-NEXT:    ldr q8, [sp, #16] // 16-byte Reload
+; CHECK-WINDOWS-NEXT:    ldp x10, x11, [sp], #32 // 16-byte Folded Reload
+; CHECK-WINDOWS-NEXT:    ret
+entry:
+  tail call void asm sideeffect "", "~{x10},~{x11},~{q8}"()
+  ret void
+}
diff --git a/llvm/test/CodeGen/AArch64/preserve-all-large-csr.ll b/llvm/test/CodeGen/AArch64/preserve-all-large-csr.ll
index 2e621606d14c8..6426483ebbce9 100644
--- a/llvm/test/CodeGen/AArch64/preserve-all-large-csr.ll
+++ b/llvm/test/CodeGen/AArch64/preserve-all-large-csr.ll
@@ -1,4 +1,6 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6
 ; RUN: llc -mtriple=aarch64-linux-gnu -verify-machineinstrs < %s | FileCheck %s
+; RUN: llc -mtriple=aarch64-pc-windows-msvc -verify-machineinstrs < %s | FileCheck %s --check-prefix=CHECK-WIN
 
 ; The preserve_all calling convention promotes almost all registers to
 ; callee-saved, so a function that clobbers them all spills a callee-save area
@@ -10,21 +12,213 @@
 ; This also covers the related callee-save size accounting: D8-D15 (AAPCS) and
 ; the enclosing Q8-Q15 (preserve_all) must not be double-counted, otherwise the
 ; frame is over-sized and the prologue emits a redundant SP adjustment.
-
-; CHECK-LABEL: trigger_stack_spill:
-; The last in-range pair sits at the +504 boundary; the registers above it are
-; spilled as single STR (offset > 504, unencodable as a pair).
-; CHECK:      stp x22, x21, [sp, #504]
-; CHECK-NEXT: str x20, [sp, #520]
-; CHECK-NEXT: str x19, [sp, #528]
-; The unwind offsets must match where the registers are actually stored.
-; CHECK:      .cfi_offset w19, -16
-; CHECK:      .cfi_offset w20, -24
-; The epilogue reloads them symmetrically with single LDR.
-; CHECK-DAG:  ldr x19, [sp, #528]
-; CHECK-DAG:  ldr x20, [sp, #520]
+;
+; The Windows target is checked too: it spills in a different (bottom-up) order
+; that keeps every callee-save offset in range, so no unpairing is needed there
+; and the offset math must still be correct.
 
 define preserve_allcc void @trigger_stack_spill() {
+; CHECK-LABEL: trigger_stack_spill:
+; CHECK:       // %bb.0: // %entry
+; CHECK-NEXT:    stp q31, q30, [sp, #-544]! // 32-byte Folded Spill
+; CHECK-NEXT:    stp q29, q28, [sp, #32] // 32-byte Folded Spill
+; CHECK-NEXT:    stp q27, q26, [sp, #64] // 32-byte Folded Spill
+; CHECK-NEXT:    stp q25, q24, [sp, #96] // 32-byte Folded Spill
+; CHECK-NEXT:    stp q23, q22, [sp, #128] // 32-byte Folded Spill
+; CHECK-NEXT:    stp q21, q20, [sp, #160] // 32-byte Folded Spill
+; CHECK-NEXT:    stp q19, q18, [sp, #192] // 32-byte Folded Spill
+; CHECK-NEXT:    stp q17, q16, [sp, #224] // 32-byte Folded Spill
+; CHECK-NEXT:    stp q15, q14, [sp, #256] // 32-byte Folded Spill
+; CHECK-NEXT:    stp q13, q12, [sp, #288] // 32-byte Folded Spill
+; CHECK-NEXT:    stp q11, q10, [sp, #320] // 32-byte Folded Spill
+; CHECK-NEXT:    stp q9, q8, [sp, #352] // 32-byte Folded Spill
+; CHECK-NEXT:    str x15, [sp, #384] // 8-byte Spill
+; CHECK-NEXT:    stp x14, x13, [sp, #392] // 16-byte Folded Spill
+; CHECK-NEXT:    stp x12, x11, [sp, #408] // 16-byte Folded Spill
+; CHECK-NEXT:    stp x10, x9, [sp, #424] // 16-byte Folded Spill
+; CHECK-NEXT:    stp x29, x30, [sp, #440] // 16-byte Folded Spill
+; CHECK-NEXT:    stp x28, x27, [sp, #456] // 16-byte Folded Spill
+; CHECK-NEXT:    stp x26, x25, [sp, #472] // 16-byte Folded Spill
+; CHECK-NEXT:    stp x24, x23, [sp, #488] // 16-byte Folded Spill
+; CHECK-NEXT:    stp x22, x21, [sp, #504] // 16-byte Folded Spill
+; CHECK-NEXT:    str x20, [sp, #520] // 8-byte Spill
+; CHECK-NEXT:    str x19, [sp, #528] // 8-byte Spill
+; CHECK-NEXT:    sub sp, sp, #16
+; CHECK-NEXT:    .cfi_def_cfa_offset 560
+; CHECK-NEXT:    .cfi_offset w19, -16
+; CHECK-NEXT:    .cfi_offset w20, -24
+; CHECK-NEXT:    .cfi_offset w21, -32
+; CHECK-NEXT:    .cfi_offset w22, -40
+; CHECK-NEXT:    .cfi_offset w23, -48
+; CHECK-NEXT:    .cfi_offset w24, -56
+; CHECK-NEXT:    .cfi_offset w25, -64
+; CHECK-NEXT:    .cfi_offset w26, -72
+; CHECK-NEXT:    .cfi_offset w27, -80
+; CHECK-NEXT:    .cfi_offset w28, -88
+; CHECK-NEXT:    .cfi_offset w30, -96
+; CHECK-NEXT:    .cfi_offset w29, -104
+; CHECK-NEXT:    .cfi_offset w9, -112
+; CHECK-NEXT:    .cfi_offset w10, -120
+; CHECK-NEXT:    .cfi_offset w11, -128
+; CHECK-NEXT:    .cfi_offset w12, -136
+; CHECK-NEXT:    .cfi_offset w13, -144
+; CHECK-NEXT:    .cfi_offset w14, -152
+; CHECK-NEXT:    .cfi_offset w15, -160
+; CHECK-NEXT:    .cfi_offset b8, -176
+; CHECK-NEXT:    .cfi_offset b9, -192
+; CHECK-NEXT:    .cfi_offset b10, -208
+; CHECK-NEXT:    .cfi_offset b11, -224
+; CHECK-NEXT:    .cfi_offset b12, -240
+; CHECK-NEXT:    .cfi_offset b13, -256
+; CHECK-NEXT:    .cfi_offset b14, -272
+; CHECK-NEXT:    .cfi_offset b15, -288
+; CHECK-NEXT:    .cfi_offset b16, -304
+; CHECK-NEXT:    .cfi_offset b17, -320
+; CHECK-NEXT:    .cfi_offset b18, -336
+; CHECK-NEXT:    .cfi_offset b19, -352
+; CHECK-NEXT:    .cfi_offset b20, -368
+; CHECK-NEXT:    .cfi_offset b21, -384
+; CHECK-NEXT:    .cfi_offset b22, -400
+; CHECK-NEXT:    .cfi_offset b23, -416
+; CHECK-NEXT:    .cfi_offset b24, -432
+; CHECK-NEXT:    .cfi_offset b25, -448
+; CHECK-NEXT:    .cfi_offset b26, -464
+; CHECK-NEXT:    .cfi_offset b27, -480
+; CHECK-NEXT:    .cfi_offset b28, -496
+; CHECK-NEXT:    .cfi_offset b29, -512
+; CHECK-NEXT:    .cfi_offset b30, -528
+; CHECK-NEXT:    .cfi_offset b31, -544
+; CHECK-NEXT:    //APP
+; CHECK-NEXT:    //NO_APP
+; CHECK-NEXT:    add sp, sp, #16
+; CHECK-NEXT:    ldp x22, x21, [sp, #504] // 16-byte Folded Reload
+; CHECK-NEXT:    ldr x19, [sp, #528] // 8-byte Reload
+; CHECK-NEXT:    ldp x24, x23, [sp, #488] // 16-byte Folded Reload
+; CHECK-NEXT:    ldr x20, [sp, #520] // 8-byte Reload
+; CHECK-NEXT:    ldr x15, [sp, #384] // 8-byte Reload
+; CHECK-NEXT:    ldp x26, x25, [sp, #472] // 16-byte Folded Reload
+; CHECK-NEXT:    ldp x28, x27, [sp, #456] // 16-byte Folded Reload
+; CHECK-NEXT:    ldp x29, x30, [sp, #440] // 16-byte Folded Reload
+; CHECK-NEXT:    ldp x10, x9, [sp, #424] // 16-byte Folded Reload
+; CHECK-NEXT:    ldp x12, x11, [sp, #408] // 16-byte Folded Reload
+; CHECK-NEXT:    ldp x14, x13, [sp, #392] // 16-byte Folded Reload
+; CHECK-NEXT:    ldp q9, q8, [sp, #352] // 32-byte Folded Reload
+; CHECK-NEXT:    ldp q11, q10, [sp, #320] // 32-byte Folded Reload
+; CHECK-NEXT:    ldp q13, q12, [sp, #288] // 32-byte Folded Reload
+; CHECK-NEXT:    ldp q15, q14, [sp, #256] // 32-byte Folded Reload
+; CHECK-NEXT:    ldp q17, q16, [sp, #224] // 32-byte Folded Reload
+; CHECK-NEXT:    ldp q19, q18, [sp, #192] // 32-byte Folded Reload
+; CHECK-NEXT:    ldp q21, q20, [sp, #160] // 32-byte Folded Reload
+; CHECK-NEXT:    ldp q23, q22, [sp, #128] // 32-byte Folded Reload
+; CHECK-NEXT:    ldp q25, q24, [sp, #96] // 32-byte Folded Reload
+; CHECK-NEXT:    ldp q27, q26, [sp, #64] // 32-byte Folded Reload
+; CHECK-NEXT:    ldp q29, q28, [sp, #32] // 32-byte Folded Reload
+; CHECK-NEXT:    ldp q31, q30, [sp], #544 // 32-byte Folded Reload
+; CHECK-NEXT:    ret
+;
+; CHECK-WIN-LABEL: trigger_stack_spill:
+; CHECK-WIN:       .seh_proc trigger_stack_spill
+; CHECK-WIN-NEXT:  // %bb.0: // %entry
+; CHECK-WIN-NEXT:    sub sp, sp, #544
+; CHECK-WIN-NEXT:    .seh_stackalloc 544
+; CHECK-WIN-NEXT:    stp x19, x20, [sp] // 16-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_regp x19, 0
+; CHECK-WIN-NEXT:    stp x21, x22, [sp, #16] // 16-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_regp x21, 16
+; CHECK-WIN-NEXT:    stp x23, x24, [sp, #32] // 16-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_regp x23, 32
+; CHECK-WIN-NEXT:    stp x25, x26, [sp, #48] // 16-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_regp x25, 48
+; CHECK-WIN-NEXT:    stp x27, x28, [sp, #64] // 16-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_regp x27, 64
+; CHECK-WIN-NEXT:    stp x29, x30, [sp, #80] // 16-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_fplr 80
+; CHECK-WIN-NEXT:    stp x9, x10, [sp, #96] // 16-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p x9, 96
+; CHECK-WIN-NEXT:    stp x11, x12, [sp, #112] // 16-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p x11, 112
+; CHECK-WIN-NEXT:    stp x13, x14, [sp, #128] // 16-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p x13, 128
+; CHECK-WIN-NEXT:    str x15, [sp, #144] // 8-byte Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg x15, 144
+; CHECK-WIN-NEXT:    stp q8, q9, [sp, #160] // 32-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q8, 160
+; CHECK-WIN-NEXT:    stp q10, q11, [sp, #192] // 32-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q10, 192
+; CHECK-WIN-NEXT:    stp q12, q13, [sp, #224] // 32-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q12, 224
+; CHECK-WIN-NEXT:    stp q14, q15, [sp, #256] // 32-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q14, 256
+; CHECK-WIN-NEXT:    stp q16, q17, [sp, #288] // 32-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q16, 288
+; CHECK-WIN-NEXT:    stp q18, q19, [sp, #320] // 32-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q18, 320
+; CHECK-WIN-NEXT:    stp q20, q21, [sp, #352] // 32-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q20, 352
+; CHECK-WIN-NEXT:    stp q22, q23, [sp, #384] // 32-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q22, 384
+; CHECK-WIN-NEXT:    stp q24, q25, [sp, #416] // 32-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q24, 416
+; CHECK-WIN-NEXT:    stp q26, q27, [sp, #448] // 32-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q26, 448
+; CHECK-WIN-NEXT:    stp q28, q29, [sp, #480] // 32-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q28, 480
+; CHECK-WIN-NEXT:    stp q30, q31, [sp, #512] // 32-byte Folded Spill
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q30, 512
+; CHECK-WIN-NEXT:    .seh_endprologue
+; CHECK-WIN-NEXT:    //APP
+; CHECK-WIN-NEXT:    //NO_APP
+; CHECK-WIN-NEXT:    .seh_startepilogue
+; CHECK-WIN-NEXT:    ldp q30, q31, [sp, #512] // 32-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q30, 512
+; CHECK-WIN-NEXT:    ldp q28, q29, [sp, #480] // 32-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q28, 480
+; CHECK-WIN-NEXT:    ldp q26, q27, [sp, #448] // 32-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q26, 448
+; CHECK-WIN-NEXT:    ldp q24, q25, [sp, #416] // 32-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q24, 416
+; CHECK-WIN-NEXT:    ldp q22, q23, [sp, #384] // 32-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q22, 384
+; CHECK-WIN-NEXT:    ldp q20, q21, [sp, #352] // 32-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q20, 352
+; CHECK-WIN-NEXT:    ldp q18, q19, [sp, #320] // 32-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q18, 320
+; CHECK-WIN-NEXT:    ldp q16, q17, [sp, #288] // 32-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q16, 288
+; CHECK-WIN-NEXT:    ldp q14, q15, [sp, #256] // 32-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q14, 256
+; CHECK-WIN-NEXT:    ldp q12, q13, [sp, #224] // 32-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q12, 224
+; CHECK-WIN-NEXT:    ldp q10, q11, [sp, #192] // 32-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q10, 192
+; CHECK-WIN-NEXT:    ldp q8, q9, [sp, #160] // 32-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p q8, 160
+; CHECK-WIN-NEXT:    ldr x15, [sp, #144] // 8-byte Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg x15, 144
+; CHECK-WIN-NEXT:    ldp x13, x14, [sp, #128] // 16-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p x13, 128
+; CHECK-WIN-NEXT:    ldp x11, x12, [sp, #112] // 16-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p x11, 112
+; CHECK-WIN-NEXT:    ldp x9, x10, [sp, #96] // 16-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_any_reg_p x9, 96
+; CHECK-WIN-NEXT:    ldp x29, x30, [sp, #80] // 16-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_fplr 80
+; CHECK-WIN-NEXT:    ldp x27, x28, [sp, #64] // 16-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_regp x27, 64
+; CHECK-WIN-NEXT:    ldp x25, x26, [sp, #48] // 16-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_regp x25, 48
+; CHECK-WIN-NEXT:    ldp x23, x24, [sp, #32] // 16-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_regp x23, 32
+; CHECK-WIN-NEXT:    ldp x21, x22, [sp, #16] // 16-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_regp x21, 16
+; CHECK-WIN-NEXT:    ldp x19, x20, [sp] // 16-byte Folded Reload
+; CHECK-WIN-NEXT:    .seh_save_regp x19, 0
+; CHECK-WIN-NEXT:    add sp, sp, #544
+; CHECK-WIN-NEXT:    .seh_stackalloc 544
+; CHECK-WIN-NEXT:    .seh_endepilogue
+; CHECK-WIN-NEXT:    ret
+; CHECK-WIN-NEXT:    .seh_endfunclet
+; CHECK-WIN-NEXT:    .seh_endproc
 entry:
   call void asm sideeffect "", "~{x0},~{x1},~{x2},~{x3},~{x4},~{x5},~{x6},~{x7},~{x8},~{x9},~{x10},~{x11},~{x12},~{x13},~{x14},~{x15},~{x16},~{x17},~{x18},~{x19},~{x20},~{x21},~{x22},~{x23},~{x24},~{x25},~{x26},~{x27},~{x28},~{fp},~{lr},~{v0},~{v1},~{v2},~{v3},~{v4},~{v5},~{v6},~{v7},~{v8},~{v9},~{v10},~{v11},~{v12},~{v13},~{v14},~{v15},~{v16},~{v17},~{v18},~{v19},~{v20},~{v21},~{v22},~{v23},~{v24},~{v25},~{v26},~{v27},~{v28},~{v29},~{v30},~{v31},~{memory},~{cc}"()
   ret void

>From 70d78e03ea26a0bbf9c1e6e94b0a63e988c77fc0 Mon Sep 17 00:00:00 2001
From: Jerry Dang <kuroyukiasuna at gmail.com>
Date: Wed, 1 Jul 2026 17:23:31 -0400
Subject: [PATCH 3/5] Remove CSMask as it's redundant for AArch64

---
 llvm/lib/Target/AArch64/AArch64FrameLowering.cpp | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)

diff --git a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
index 777aaed23baef..9fc7a69f18837 100644
--- a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
@@ -2643,22 +2643,21 @@ void AArch64FrameLowering::determineCalleeSaves(MachineFunction &MF,
   unsigned ZPRCSStackSize = 0;
   unsigned PPRCSStackSize = 0;
   const TargetRegisterInfo *TRI = MF.getSubtarget().getRegisterInfo();
-  // A register and its super-register can both appear in SavedRegs (e.g. for
-  // preserve_all, AAPCS contributes D8-D15 while the extended convention
-  // contributes the enclosing Q8-Q31). Only the widest register is actually
-  // spilled, skip such sub-registers here to avoid double-counting the overlap.
-  BitVector CSMask(SavedRegs.size());
-  for (unsigned i = 0; CSRegs[i]; ++i)
-    CSMask.set(CSRegs[i]);
   for (unsigned Reg : SavedRegs.set_bits()) {
     auto *RC = TRI->getMinimalPhysRegClass(MCRegister(Reg));
     assert(RC && "expected register class!");
     auto SpillSize = TRI->getSpillSize(*RC);
     bool IsZPR = AArch64::ZPRRegClass.contains(Reg);
     bool IsPPR = !IsZPR && AArch64::PPRRegClass.contains(Reg);
+
+    // A register and its super-register can both appear in SavedRegs (e.g. for
+    // preserve_all, AAPCS contributes D8-D15 while the extended convention
+    // contributes the enclosing Q8-Q31). Only the widest register is actually
+    // spilled, skip such sub-registers here to avoid double-counting the
+    // overlap.
     bool SavedSuper = false;
     for (MCPhysReg SuperReg : TRI->superregs(MCRegister(Reg)))
-      if (SavedRegs.test(SuperReg) && CSMask.test(SuperReg)) {
+      if (SavedRegs.test(SuperReg)) {
         SavedSuper = true;
         break;
       }

>From 8c0aaf0af33c43d928a24acf09a5e8d59615ae15 Mon Sep 17 00:00:00 2001
From: Jerry Dang <kuroyukiasuna at gmail.com>
Date: Thu, 2 Jul 2026 13:23:29 -0400
Subject: [PATCH 4/5] truncate down the comments

---
 llvm/lib/Target/AArch64/AArch64FrameLowering.cpp | 15 ++++-----------
 1 file changed, 4 insertions(+), 11 deletions(-)

diff --git a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
index 9fc7a69f18837..fb4cd521c6178 100644
--- a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
@@ -1784,12 +1784,7 @@ void computeCalleeSaveRegisterPairs(const AArch64FrameLowering &AFL,
 
     bool NeedsWinCFI = AFL.needsWinCFI(MF);
     int Scale = TRI->getSpillSize(*RPI.RC);
-    // A paired LDP/STP can only encode a signed 7-bit scaled offset ([-64,
-    // 63]). When the callee-save area is large (e.g. preserve_all spills 30+
-    // registers), the highest pairs fall outside that range. Only pair if the
-    // resulting offset is encodable; otherwise leave the register unpaired and
-    // let it be spilled with a single STR/LDR, which has a much wider 12-bit
-    // immediate.
+    // True when the pair's ldp/stp offset fits the signed 7-bit scaled imm.
     auto PairFitsImmRange = [&]() {
       int PairOffset =
           IsWindows ? ByteOffset : ByteOffset + StackFillDir * 2 * Scale;
@@ -2650,11 +2645,9 @@ void AArch64FrameLowering::determineCalleeSaves(MachineFunction &MF,
     bool IsZPR = AArch64::ZPRRegClass.contains(Reg);
     bool IsPPR = !IsZPR && AArch64::PPRRegClass.contains(Reg);
 
-    // A register and its super-register can both appear in SavedRegs (e.g. for
-    // preserve_all, AAPCS contributes D8-D15 while the extended convention
-    // contributes the enclosing Q8-Q31). Only the widest register is actually
-    // spilled, skip such sub-registers here to avoid double-counting the
-    // overlap.
+    // A register and its super-register can both appear in SavedRegs.
+    // Only the widest register is actually spilled, so skip such sub-registers
+    // here to avoid double-counting the overlap.
     bool SavedSuper = false;
     for (MCPhysReg SuperReg : TRI->superregs(MCRegister(Reg)))
       if (SavedRegs.test(SuperReg)) {

>From 6a48d5d27c2d2a6af20c3e3d7f0b725035965762 Mon Sep 17 00:00:00 2001
From: Jerry Dang <kuroyukiasuna at gmail.com>
Date: Tue, 7 Jul 2026 21:15:57 -0400
Subject: [PATCH 5/5] Refactor; Use AlignOffset on ByteOffset.

---
 .../Target/AArch64/AArch64FrameLowering.cpp   | 29 ++++++++++---------
 1 file changed, 15 insertions(+), 14 deletions(-)

diff --git a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
index 9805cd3390d9b..4c55e91fa6960 100644
--- a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
@@ -1796,8 +1796,8 @@ void computeCalleeSaveRegisterPairs(const AArch64FrameLowering &AFL,
     int Scale = TRI->getSpillSize(*RPI.RC);
     // True when the pair's ldp/stp offset fits the signed 7-bit scaled imm.
     auto PairFitsImmRange = [&]() {
-      int PairOffset =
-          IsWindows ? ByteOffset : ByteOffset + StackFillDir * 2 * Scale;
+      int Aligned = AlignOffset(ByteOffset, Scale);
+      int PairOffset = IsWindows ? Aligned : Aligned + StackFillDir * 2 * Scale;
       int Scaled = PairOffset / Scale;
       return Scaled >= -64 && Scaled <= 63;
     };
@@ -2654,22 +2654,23 @@ void AArch64FrameLowering::determineCalleeSaves(MachineFunction &MF,
     auto SpillSize = TRI->getSpillSize(*RC);
     bool IsZPR = AArch64::ZPRRegClass.contains(Reg);
     bool IsPPR = !IsZPR && AArch64::PPRRegClass.contains(Reg);
-
-    // A register and its super-register can both appear in SavedRegs.
-    // Only the widest register is actually spilled, so skip such sub-registers
-    // here to avoid double-counting the overlap.
-    bool SavedSuper = false;
-    for (MCPhysReg SuperReg : TRI->superregs(MCRegister(Reg)))
-      if (SavedRegs.test(SuperReg)) {
-        SavedSuper = true;
-        break;
-      }
     if (IsZPR)
       ZPRCSStackSize += SpillSize;
     else if (IsPPR)
       PPRCSStackSize += SpillSize;
-    else if (!SavedSuper)
-      CSStackSize += SpillSize;
+    else {
+      // A register and its super-register can both appear in SavedRegs.
+      // Only the widest register is actually spilled, so skip such
+      // sub-registers here to avoid double-counting the overlap.
+      bool SavedSuper = false;
+      for (MCPhysReg SuperReg : TRI->superregs(MCRegister(Reg)))
+        if (SavedRegs.test(SuperReg)) {
+          SavedSuper = true;
+          break;
+        }
+      if (!SavedSuper)
+        CSStackSize += SpillSize;
+    }
   }
 
   // Save number of saved regs, so we can easily update CSStackSize later to



More information about the llvm-commits mailing list