[llvm] [AArch64] Fix handling of x29/x30 in inline assembly clobbers (PR #167783)
Lukáš Lalinský via llvm-commits
llvm-commits at lists.llvm.org
Sat Nov 29 03:16:17 PST 2025
https://github.com/lalinsky updated https://github.com/llvm/llvm-project/pull/167783
>From dae807661da46947d29049ca07aced1b2514afdd Mon Sep 17 00:00:00 2001
From: Lukas Lalinsky <lukas at lalinsky.com>
Date: Wed, 12 Nov 2025 23:10:20 +0100
Subject: [PATCH] [AArch64] Fix handling of x29/x30 in inline assembly clobbers
The AArch64 backend was silently ignoring inline assembly clobbers when
numeric register names (x29, x30) were used instead of their
architectural aliases (fp, lr). I found this bug via inline assembly
in Zig, which not normalize the register names the way clang does.
There is an incoplete workaround for this in Rust, but that only
handles `x30/lr`, not `x29/fp`. I thought it would make
sense to fix this properly rather than adding a workaround to Zig.
This patch adds explicit handling in getRegForInlineAsmConstraint() to
map both numeric and alias forms to the correct physical registers,
following the same pattern used by the RISC-V backend.
I've left `x31/sp` without changes, it would nice to have to have
warning when trying to clobber `x31`, just like there is for `sp`,
but that register needs different handling, so it's best done
separately.
If you have code like this:
define void @clobber_x30() nounwind {
tail call void asm sideeffect "nop", "~{x30}"()
ret void
}
Here is the generated assembly before:
clobber_x30: // @clobber_x30
//APP
nop
//NO_APP
ret
And after:
clobber_x30: // @clobber_x30
str x30, [sp, #-16]! // 8-byte Folded Spill
//APP
nop
//NO_APP
ldr x30, [sp], #16 // 8-byte Folded Reload
ret
---
llvm/docs/ReleaseNotes.md | 6 +++
.../Target/AArch64/AArch64ISelLowering.cpp | 15 +++++++
.../AArch64/inline-asm-clobber-x29-x30.ll | 44 +++++++++++++++++++
3 files changed, 65 insertions(+)
create mode 100644 llvm/test/CodeGen/AArch64/inline-asm-clobber-x29-x30.ll
diff --git a/llvm/docs/ReleaseNotes.md b/llvm/docs/ReleaseNotes.md
index c6c527d1ae964..1791f4f5fbdca 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -111,6 +111,12 @@ Changes to the AArch64 Backend
* `FEAT_TME` support has been removed, as it has been withdrawn from
all future versions of the A-profile architecture.
+* A bug was fixed that caused LLVM IR inline assembly clobbers of the x29 and
+ x30 registers to be ignored when they were written using their xN names
+ instead of the ABI names FP and LR. Note that LLVM IR produced by Clang
+ always uses the ABI names, but other frontends may not.
+ ([#167783](https://github.com/llvm/llvm-project/pull/167783))
+
Changes to the AMDGPU Backend
-----------------------------
diff --git a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
index 6072fd9d8f242..69630dd18615e 100644
--- a/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
+++ b/llvm/lib/Target/AArch64/AArch64ISelLowering.cpp
@@ -30,6 +30,7 @@
#include "llvm/ADT/SmallVectorExtras.h"
#include "llvm/ADT/Statistic.h"
#include "llvm/ADT/StringRef.h"
+#include "llvm/ADT/StringSwitch.h"
#include "llvm/ADT/Twine.h"
#include "llvm/Analysis/LoopInfo.h"
#include "llvm/Analysis/MemoryLocation.h"
@@ -13356,6 +13357,20 @@ AArch64TargetLowering::getRegForInlineAsmConstraint(
return std::make_pair(unsigned(AArch64::ZT0), &AArch64::ZTRRegClass);
}
+ // Clang will correctly decode the usage of register name aliases into their
+ // official names. However, other frontends like `rustc` do not. This allows
+ // users of these frontends to use the ABI names for registers in LLVM-style
+ // register constraints.
+ //
+ // x31->sp is not included here because it's not a general register and
+ // needs different handling
+ unsigned XRegFromAlias = StringSwitch<unsigned>(Constraint.lower())
+ .Cases({"{x29}", "{fp}"}, AArch64::FP)
+ .Cases({"{x30}", "{lr}"}, AArch64::LR)
+ .Default(AArch64::NoRegister);
+ if (XRegFromAlias != AArch64::NoRegister)
+ return std::make_pair(XRegFromAlias, &AArch64::GPR64RegClass);
+
// Use the default implementation in TargetLowering to convert the register
// constraint into a member of a register class.
std::pair<unsigned, const TargetRegisterClass *> Res;
diff --git a/llvm/test/CodeGen/AArch64/inline-asm-clobber-x29-x30.ll b/llvm/test/CodeGen/AArch64/inline-asm-clobber-x29-x30.ll
new file mode 100644
index 0000000000000..cde55522fb19b
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/inline-asm-clobber-x29-x30.ll
@@ -0,0 +1,44 @@
+; RUN: llc -mtriple=aarch64 -verify-machineinstrs < %s | FileCheck %s
+
+; Test that both numeric register names (x29, x30) and their architectural
+; aliases (fp, lr) work correctly as clobbers in inline assembly.
+
+define void @clobber_x29() nounwind {
+; CHECK-LABEL: clobber_x29:
+; CHECK: str x29, [sp
+; CHECK-NEXT: //APP
+; CHECK-NEXT: //NO_APP
+; CHECK-NEXT: ldr x29, [sp
+ tail call void asm sideeffect "", "~{x29}"()
+ ret void
+}
+
+define void @clobber_fp() nounwind {
+; CHECK-LABEL: clobber_fp:
+; CHECK: str x29, [sp
+; CHECK-NEXT: //APP
+; CHECK-NEXT: //NO_APP
+; CHECK-NEXT: ldr x29, [sp
+ tail call void asm sideeffect "", "~{fp}"()
+ ret void
+}
+
+define void @clobber_x30() nounwind {
+; CHECK-LABEL: clobber_x30:
+; CHECK: str x30, [sp
+; CHECK-NEXT: //APP
+; CHECK-NEXT: //NO_APP
+; CHECK-NEXT: ldr x30, [sp
+ tail call void asm sideeffect "", "~{x30}"()
+ ret void
+}
+
+define void @clobber_lr() nounwind {
+; CHECK-LABEL: clobber_lr:
+; CHECK: str x30, [sp
+; CHECK-NEXT: //APP
+; CHECK-NEXT: //NO_APP
+; CHECK-NEXT: ldr x30, [sp
+ tail call void asm sideeffect "", "~{lr}"()
+ ret void
+}
More information about the llvm-commits
mailing list