[llvm-branch-commits] [llvm] [AArch64] Use the Windows ARM64 ABI for UEFI targets (PR #232700)
Prabhu Rajasekaran via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Fri Oct 16 23:26:58 PDT 2026
https://github.com/Prabhuk updated https://github.com/llvm/llvm-project/pull/232700
>From b648d706347804ca40393c70f6fb53440dbd4d12 Mon Sep 17 00:00:00 2001
From: prabhukr <prabhukr at google.com>
Date: Fri, 16 Oct 2026 04:47:06 -0700
Subject: [PATCH] [AArch64] Use the Windows ARM64 ABI for UEFI targets
UEFI follows the Windows ARM64 procedure call standard (the UEFI
specification defines AArch64 EFI images in terms of the Microsoft
toolchain, and that is how EDK2 and clang-built firmware are linked
today), and x86_64-unknown-uefi likewise uses the Win64 ABI. Introduce
AArch64Subtarget::isTargetUEFI() and isTargetWindowsOrUEFI() and use the
latter for the core ABI decisions that currently key off
isTargetWindows():
* the Win64 calling convention for C/Fast/Swift calls, including the
Windows variadic convention which homes all argument registers and
passes floating point variadic arguments in GPRs,
* the char* based va_list (va_copy size and getVaListSizeInBits),
* the Windows callee saved register list and the matching frame record
pairing rules in AArch64FrameLowering, which have to agree so that the
SEH save_regp/save_fplr unwind codes describe consecutive registers,
* the requirement that the frame chain stays valid in every function.
The last two points fix an assertion failure for any aarch64-unknown-uefi
function that sets up a frame record: the MC layer already emitted
Windows CFI for the COFF UEFI triple, but CodeGen used the AAPCS callee
saved register order, which produced non-consecutive save_regp pairs
("Non-consecutive registers not allowed for save_regp").
The existing Windows varargs and win64cc tests gain aarch64-unknown-uefi
run lines, and a new test covers the frame record pairing together with
the object file emission.
Assisted-by: Claude
---
.../AArch64/AArch64CallingConvention.td | 6 +-
llvm/lib/Target/AArch64/AArch64FastISel.cpp | 2 +-
.../Target/AArch64/AArch64FrameLowering.cpp | 15 ++--
.../Target/AArch64/AArch64ISelLowering.cpp | 15 ++--
.../Target/AArch64/AArch64RegisterInfo.cpp | 7 +-
llvm/lib/Target/AArch64/AArch64Subtarget.h | 11 ++-
.../AArch64/GISel/AArch64LegalizerInfo.cpp | 9 ++-
.../AArch64/uefi-frame-record-pairing.ll | 76 +++++++++++++++++++
.../CodeGen/AArch64/win64_vararg_float.ll | 1 +
llvm/test/CodeGen/AArch64/win64cc-x18.ll | 1 +
10 files changed, 115 insertions(+), 28 deletions(-)
create mode 100644 llvm/test/CodeGen/AArch64/uefi-frame-record-pairing.ll
diff --git a/llvm/lib/Target/AArch64/AArch64CallingConvention.td b/llvm/lib/Target/AArch64/AArch64CallingConvention.td
index d71c6491eee1a1..70186eda852977 100644
--- a/llvm/lib/Target/AArch64/AArch64CallingConvention.td
+++ b/llvm/lib/Target/AArch64/AArch64CallingConvention.td
@@ -545,7 +545,7 @@ def CC_AArch64_Preserve_None : CallingConv<[
// This handles the non-variadic parameter case. Variadic parameters
// are handled in CCAssignFnForCall.
CCIfVarArg<CCIfSubtarget<"isTargetDarwin()", CCDelegateTo<CC_AArch64_DarwinPCS>>>,
- CCIfVarArg<CCIfSubtarget<"isTargetWindows()", CCDelegateTo<CC_AArch64_Win64PCS>>>,
+ CCIfVarArg<CCIfSubtarget<"isTargetWindowsOrUEFI()", CCDelegateTo<CC_AArch64_Win64PCS>>>,
CCIfVarArg<CCDelegateTo<CC_AArch64_AAPCS>>,
// We can pass arguments in all general registers, except:
@@ -574,9 +574,9 @@ def CC_AArch64_Preserve_None : CallingConv<[
X12, X13, X14, X9]>>,
// Windows uses X15 for stack allocation
- CCIf<"!State.getMachineFunction().getSubtarget<AArch64Subtarget>().isTargetWindows()",
+ CCIf<"!State.getMachineFunction().getSubtarget<AArch64Subtarget>().isTargetWindowsOrUEFI()",
CCIfType<[i32], CCAssignToReg<[W15]>>>,
- CCIf<"!State.getMachineFunction().getSubtarget<AArch64Subtarget>().isTargetWindows()",
+ CCIf<"!State.getMachineFunction().getSubtarget<AArch64Subtarget>().isTargetWindowsOrUEFI()",
CCIfType<[i64], CCAssignToReg<[X15]>>>,
CCDelegateTo<CC_AArch64_AAPCS>
diff --git a/llvm/lib/Target/AArch64/AArch64FastISel.cpp b/llvm/lib/Target/AArch64/AArch64FastISel.cpp
index 29689dbd68bf98..aaa95824bf6030 100644
--- a/llvm/lib/Target/AArch64/AArch64FastISel.cpp
+++ b/llvm/lib/Target/AArch64/AArch64FastISel.cpp
@@ -335,7 +335,7 @@ CCAssignFn *AArch64FastISel::CCAssignFnForCall(CallingConv::ID CC) const {
return CC_AArch64_Win64_CFGuard_Check;
if (Subtarget->isTargetDarwin())
return CC_AArch64_DarwinPCS;
- if (Subtarget->isTargetWindows())
+ if (Subtarget->isTargetWindowsOrUEFI())
return CC_AArch64_Win64PCS;
return CC_AArch64_AAPCS;
}
diff --git a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
index cd9f6317da211e..4a35ab0b129b77 100644
--- a/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64FrameLowering.cpp
@@ -346,12 +346,11 @@ static bool isLikelyToHaveSVEStack(const AArch64FrameLowering &AFL,
}
static bool isTargetWindows(const MachineFunction &MF) {
- // TODO: Should this include targets like UEFI (which use Windows CFI)?
- // Note: Currently, there is not AArch64 support for UEFI. The value returned
- // here must align with the predicate used for returning the list of callee
- // saved regs in AArch64RegisterInfo::getCalleeSavedRegs(), so that we use
- // invalidateWindowsRegisterPairing() where appropriate.
- return MF.getSubtarget<AArch64Subtarget>().isTargetWindows();
+ // Note: UEFI follows the Windows ARM64 ABI and uses Windows CFI. The value
+ // returned here must align with the predicate used for returning the list of
+ // callee saved regs in AArch64RegisterInfo::getCalleeSavedRegs(), so that we
+ // use invalidateWindowsRegisterPairing() where appropriate.
+ return MF.getSubtarget<AArch64Subtarget>().isTargetWindowsOrUEFI();
}
bool AArch64FrameLowering::hasSVECalleeSavesAboveFrameRecord(
@@ -600,7 +599,7 @@ bool AArch64FrameLowering::isFPReserved(const MachineFunction &MF) const {
// These OSes require the frame chain is valid, even if the current frame does
// not use a frame pointer.
- if (TT.isOSDarwin() || TT.isOSWindows())
+ if (TT.isOSDarwin() || TT.isOSWindowsOrUEFI())
return true;
// If the function has a frame pointer, it is reserved.
@@ -2629,7 +2628,7 @@ void AArch64FrameLowering::determineCalleeSaves(MachineFunction &MF,
}
if (MF.getFunction().getCallingConv() == CallingConv::Win64 &&
- !Subtarget.isTargetWindows()) {
+ !Subtarget.isTargetWindowsOrUEFI()) {
// For Windows calling convention on a non-windows OS, where X18 is treated
// as reserved, back up X18 when entering non-windows code (marked with the
// Windows calling convention) and restore when returning regardless of
diff --git a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
index e3257af6445507..9d47ef0a28c08e 100644
--- a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
@@ -9225,7 +9225,7 @@ CCAssignFn *AArch64TargetLowering::CCAssignFnForCall(CallingConv::ID CC,
case CallingConv::SwiftTail:
case CallingConv::Tail:
case CallingConv::GRAAL:
- if (Subtarget->isTargetWindows()) {
+ if (Subtarget->isTargetWindowsOrUEFI()) {
if (IsVarArg) {
if (Subtarget->isWindowsArm64EC())
return CC_AArch64_Arm64EC_VarArg;
@@ -10093,7 +10093,7 @@ bool AArch64TargetLowering::isEligibleForTailCallOptimization(
// When using the Windows calling convention on a non-windows OS, we want
// to back up and restore X18 in such functions; we can't do a tail call
// from those functions.
- if (CallerCC == CallingConv::Win64 && !Subtarget->isTargetWindows() &&
+ if (CallerCC == CallingConv::Win64 && !Subtarget->isTargetWindowsOrUEFI() &&
CalleeCC != CallingConv::Win64)
return false;
@@ -13761,10 +13761,11 @@ SDValue AArch64TargetLowering::LowerVACOPY(SDValue Op,
// pointer.
SDLoc DL(Op);
unsigned PtrSize = Subtarget->isTargetILP32() ? 4 : 8;
- unsigned VaListSize =
- (Subtarget->isTargetDarwin() || Subtarget->isTargetWindows())
- ? PtrSize
- : Subtarget->isTargetILP32() ? 20 : 32;
+ unsigned VaListSize;
+ if (Subtarget->isTargetDarwin() || Subtarget->isTargetWindowsOrUEFI())
+ VaListSize = PtrSize;
+ else
+ VaListSize = Subtarget->isTargetILP32() ? 20 : 32;
const Value *DestSV = cast<SrcValueSDNode>(Op.getOperand(3))->getValue();
const Value *SrcSV = cast<SrcValueSDNode>(Op.getOperand(4))->getValue();
@@ -34009,7 +34010,7 @@ bool AArch64TargetLowering::enableAggressiveFMAFusion(EVT VT) const {
unsigned
AArch64TargetLowering::getVaListSizeInBits(const DataLayout &DL) const {
- if (Subtarget->isTargetDarwin() || Subtarget->isTargetWindows())
+ if (Subtarget->isTargetDarwin() || Subtarget->isTargetWindowsOrUEFI())
return getPointerTy(DL).getSizeInBits();
return 3 * getPointerTy(DL).getSizeInBits() + 2 * 32;
diff --git a/llvm/lib/Target/AArch64/AArch64RegisterInfo.cpp b/llvm/lib/Target/AArch64/AArch64RegisterInfo.cpp
index 672441c51286c8..3b4271a877fb2f 100644
--- a/llvm/lib/Target/AArch64/AArch64RegisterInfo.cpp
+++ b/llvm/lib/Target/AArch64/AArch64RegisterInfo.cpp
@@ -75,7 +75,8 @@ AArch64RegisterInfo::getCalleeSavedRegs(const MachineFunction *MF) const {
const auto &F = MF->getFunction();
const auto *TLI = MF->getSubtarget<AArch64Subtarget>().getTargetLowering();
const bool Darwin = MF->getSubtarget<AArch64Subtarget>().isTargetDarwin();
- const bool Windows = MF->getSubtarget<AArch64Subtarget>().isTargetWindows();
+ const bool Windows =
+ MF->getSubtarget<AArch64Subtarget>().isTargetWindowsOrUEFI();
if (TLI->supportSwiftError() &&
F.getAttributes().hasAttrSomewhere(Attribute::SwiftError)) {
@@ -691,7 +692,7 @@ bool AArch64RegisterInfo::isArgumentRegister(const MachineFunction &MF,
case CallingConv::Swift:
case CallingConv::SwiftTail:
case CallingConv::Tail:
- if (STI.isTargetWindows()) {
+ if (STI.isTargetWindowsOrUEFI()) {
if (IsVarArg)
return HasReg(CC_AArch64_Win64_VarArg_ArgRegs, Reg);
switch (CC) {
@@ -737,7 +738,7 @@ bool AArch64RegisterInfo::isArgumentRegister(const MachineFunction &MF,
case CallingConv::AArch64_SME_ABI_Support_Routines_PreserveMost_From_X0:
case CallingConv::AArch64_SME_ABI_Support_Routines_PreserveMost_From_X1:
case CallingConv::AArch64_SME_ABI_Support_Routines_PreserveMost_From_X2:
- if (STI.isTargetWindows())
+ if (STI.isTargetWindowsOrUEFI())
return HasReg(CC_AArch64_Win64PCS_ArgRegs, Reg);
return HasReg(CC_AArch64_AAPCS_ArgRegs, Reg);
}
diff --git a/llvm/lib/Target/AArch64/AArch64Subtarget.h b/llvm/lib/Target/AArch64/AArch64Subtarget.h
index e75f7b0b6d80a0..d0cc600b538953 100644
--- a/llvm/lib/Target/AArch64/AArch64Subtarget.h
+++ b/llvm/lib/Target/AArch64/AArch64Subtarget.h
@@ -345,6 +345,13 @@ class AArch64Subtarget final : public AArch64GenSubtargetInfo {
bool isTargetIOS() const { return TargetTriple.isiOS(); }
bool isTargetLinux() const { return TargetTriple.isOSLinux(); }
bool isTargetWindows() const { return TargetTriple.isOSWindows(); }
+ bool isTargetUEFI() const { return TargetTriple.isUEFI(); }
+ /// UEFI follows the Windows ARM64 ABI and produces PE/COFF images, so most
+ /// of the Windows specific ABI handling (calling convention, callee saved
+ /// registers, frame chain, stack probing and unwind info) applies to it.
+ bool isTargetWindowsOrUEFI() const {
+ return TargetTriple.isOSWindowsOrUEFI();
+ }
bool isTargetAndroid() const { return TargetTriple.isAndroid(); }
bool isTargetFuchsia() const { return TargetTriple.isOSFuchsia(); }
bool isWindowsArm64EC() const { return TargetTriple.isWindowsArm64EC(); }
@@ -420,9 +427,9 @@ class AArch64Subtarget final : public AArch64GenSubtargetInfo {
case CallingConv::Fast:
case CallingConv::Swift:
case CallingConv::SwiftTail:
- return isTargetWindows();
+ return isTargetWindowsOrUEFI();
case CallingConv::PreserveNone:
- return IsVarArg && isTargetWindows();
+ return IsVarArg && isTargetWindowsOrUEFI();
case CallingConv::Win64:
return true;
default:
diff --git a/llvm/lib/Target/AArch64/GISel/AArch64LegalizerInfo.cpp b/llvm/lib/Target/AArch64/GISel/AArch64LegalizerInfo.cpp
index 16dd0959cd8443..2a2270f8457927 100644
--- a/llvm/lib/Target/AArch64/GISel/AArch64LegalizerInfo.cpp
+++ b/llvm/lib/Target/AArch64/GISel/AArch64LegalizerInfo.cpp
@@ -1854,10 +1854,11 @@ bool AArch64LegalizerInfo::legalizeIntrinsic(LegalizerHelper &Helper,
switch (IntrinsicID) {
case Intrinsic::vacopy: {
unsigned PtrSize = ST->isTargetILP32() ? 4 : 8;
- unsigned VaListSize =
- (ST->isTargetDarwin() || ST->isTargetWindows())
- ? PtrSize
- : ST->isTargetILP32() ? 20 : 32;
+ unsigned VaListSize;
+ if (ST->isTargetDarwin() || ST->isTargetWindowsOrUEFI())
+ VaListSize = PtrSize;
+ else
+ VaListSize = ST->isTargetILP32() ? 20 : 32;
MachineFunction &MF = *MI.getMF();
auto Val = MF.getRegInfo().createGenericVirtualRegister(
diff --git a/llvm/test/CodeGen/AArch64/uefi-frame-record-pairing.ll b/llvm/test/CodeGen/AArch64/uefi-frame-record-pairing.ll
new file mode 100644
index 00000000000000..fac1d19ba103f1
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/uefi-frame-record-pairing.ll
@@ -0,0 +1,76 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6
+; RUN: llc -mtriple=aarch64-unknown-uefi < %s | FileCheck %s
+; RUN: llc -mtriple=aarch64-unknown-uefi -filetype=obj -o - < %s | llvm-readobj --file-headers --sections - | FileCheck %s --check-prefix=OBJ
+
+; UEFI follows the Windows ARM64 ABI: the Windows callee-saved register list
+; and frame record pairing rules must be used together with SEH unwind info.
+; Previously the Windows CSR list was not used for UEFI while the SEH
+; directives were, which tripped the "Non-consecutive registers not allowed
+; for save_regp" assertion in the AsmPrinter.
+
+; OBJ: Machine: IMAGE_FILE_MACHINE_ARM64
+; OBJ: Name: .text
+; OBJ: Name: .xdata
+; OBJ: Name: .pdata
+
+define i32 @large_stack_requires_frame_record() "frame-pointer"="all" {
+; CHECK-LABEL: large_stack_requires_frame_record:
+; CHECK: .seh_proc large_stack_requires_frame_record
+; CHECK-NEXT: // %bb.0:
+; CHECK-NEXT: str x28, [sp, #-32]! // 8-byte Folded Spill
+; CHECK-NEXT: .seh_save_reg_x x28, 32
+; CHECK-NEXT: stp x29, x30, [sp, #8] // 16-byte Folded Spill
+; CHECK-NEXT: .seh_save_fplr 8
+; CHECK-NEXT: add x29, sp, #8
+; CHECK-NEXT: .seh_add_fp 8
+; CHECK-NEXT: .seh_endprologue
+; CHECK-NEXT: sub sp, sp, #512
+; CHECK-NEXT: mov x0, sp
+; CHECK-NEXT: bl baz
+; CHECK-NEXT: mov w0, wzr
+; CHECK-NEXT: .seh_startepilogue
+; CHECK-NEXT: add sp, sp, #512
+; CHECK-NEXT: .seh_stackalloc 512
+; CHECK-NEXT: ldp x29, x30, [sp, #8] // 16-byte Folded Reload
+; CHECK-NEXT: .seh_save_fplr 8
+; CHECK-NEXT: ldr x28, [sp], #32 // 8-byte Folded Reload
+; CHECK-NEXT: .seh_save_reg_x x28, 32
+; CHECK-NEXT: .seh_endepilogue
+; CHECK-NEXT: ret
+; CHECK-NEXT: .seh_endfunclet
+; CHECK-NEXT: .seh_endproc
+ %x = alloca [500 x i8], align 16
+ call void @baz(ptr %x)
+ ret i32 0
+}
+
+; The Windows calling convention is used for variadic functions: all the
+; remaining argument registers are homed to the stack and va_list is a plain
+; pointer.
+define i64 @vararg_callee(i32 %n, ...) nounwind {
+; CHECK-LABEL: vararg_callee:
+; CHECK: // %bb.0:
+; CHECK-NEXT: sub sp, sp, #80
+; CHECK-NEXT: add x8, sp, #24
+; CHECK-NEXT: mov x0, x1
+; CHECK-NEXT: stp x3, x4, [sp, #40]
+; CHECK-NEXT: add x8, x8, #8
+; CHECK-NEXT: stp x5, x6, [sp, #56]
+; CHECK-NEXT: str x7, [sp, #72]
+; CHECK-NEXT: stp x1, x2, [sp, #24]
+; CHECK-NEXT: str x8, [sp, #8]
+; CHECK-NEXT: add sp, sp, #80
+; CHECK-NEXT: ret
+ %ap = alloca ptr, align 8
+ call void @llvm.va_start(ptr %ap)
+ %cur = load ptr, ptr %ap, align 8
+ %next = getelementptr inbounds i8, ptr %cur, i64 8
+ store ptr %next, ptr %ap, align 8
+ %v = load i64, ptr %cur, align 8
+ call void @llvm.va_end(ptr %ap)
+ ret i64 %v
+}
+
+declare void @baz(ptr)
+declare void @llvm.va_start(ptr)
+declare void @llvm.va_end(ptr)
diff --git a/llvm/test/CodeGen/AArch64/win64_vararg_float.ll b/llvm/test/CodeGen/AArch64/win64_vararg_float.ll
index d3f9b45c6b687c..679998fc735498 100644
--- a/llvm/test/CodeGen/AArch64/win64_vararg_float.ll
+++ b/llvm/test/CodeGen/AArch64/win64_vararg_float.ll
@@ -1,5 +1,6 @@
; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py
; RUN: llc < %s -mtriple=aarch64-windows -verify-machineinstrs | FileCheck %s --check-prefixes=DAGISEL
+; RUN: llc < %s -mtriple=aarch64-unknown-uefi -verify-machineinstrs | FileCheck %s --check-prefixes=DAGISEL
; RUN: llc < %s -mtriple=aarch64-windows -verify-machineinstrs -O0 -fast-isel | FileCheck %s --check-prefixes=O0,FASTISEL
; RUN: llc < %s -mtriple=aarch64-windows -verify-machineinstrs -O0 -global-isel | FileCheck %s --check-prefixes=O0,GISEL
diff --git a/llvm/test/CodeGen/AArch64/win64cc-x18.ll b/llvm/test/CodeGen/AArch64/win64cc-x18.ll
index 4b45c300e9c1d5..d533760c805a22 100644
--- a/llvm/test/CodeGen/AArch64/win64cc-x18.ll
+++ b/llvm/test/CodeGen/AArch64/win64cc-x18.ll
@@ -1,6 +1,7 @@
;; Testing that nest uses x15 on all calling conventions (except Arm64EC)
; RUN: llc < %s -mtriple=aarch64-pc-windows-msvc | FileCheck %s
+; RUN: llc < %s -mtriple=aarch64-unknown-uefi | FileCheck %s
; RUN: llc < %s -mtriple=aarch64-linux-gnu | FileCheck %s
; RUN: llc < %s -mtriple=aarch64-apple-darwin- | FileCheck %s
More information about the llvm-branch-commits
mailing list