[llvm-branch-commits] [llvm] release/23.x: [X86][APX] Fix per-function V3 unwind for EGPR functions on Windows x64 (#212924) (PR #213759)
via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Mon Aug 3 13:54:07 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-backend-x86
Author: llvmbot
<details>
<summary>Changes</summary>
Backport 9466be2c87689a07c9a171f24cb5c9b0c2f0cda8
Requested by: @<!-- -->mikolaj-pirog
---
Full diff: https://github.com/llvm/llvm-project/pull/213759.diff
12 Files Affected:
- (modified) llvm/lib/Target/X86/X86.h (+7)
- (modified) llvm/lib/Target/X86/X86FrameLowering.cpp (+21-9)
- (modified) llvm/lib/Target/X86/X86InstrCompiler.td (+2-1)
- (modified) llvm/lib/Target/X86/X86WinEHUnwindV2.cpp (+6)
- (modified) llvm/lib/Target/X86/X86WinEHUnwindV3.cpp (+30-21)
- (added) llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-funclet.ll (+31)
- (added) llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-no-wincfi.ll (+16)
- (added) llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-per-function.ll (+32)
- (removed) llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-required.ll (-16)
- (added) llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-v2-module.ll (+39)
- (added) llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-version-switch-v1.ll (+53)
- (added) llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-version-switch.ll (+47)
``````````diff
diff --git a/llvm/lib/Target/X86/X86.h b/llvm/lib/Target/X86/X86.h
index 48dedd9d2a758..3478e0e9c0d4b 100644
--- a/llvm/lib/Target/X86/X86.h
+++ b/llvm/lib/Target/X86/X86.h
@@ -27,6 +27,7 @@ namespace llvm {
class FunctionPass;
class InstructionSelector;
+class MachineFunction;
class PassRegistry;
class X86RegisterBankInfo;
class X86Subtarget;
@@ -408,6 +409,12 @@ FunctionPass *createX86LowerAMXIntrinsicsLegacyPass();
/// Capacity check and sub-fragment splitting for Win x64 Unwind V3.
FunctionPass *createX86WinEHUnwindV3Pass();
+/// Returns true when \p MF must use Windows x64 Unwind V3: the module is in V3
+/// mode, or the function needs an unwind table and may use EGPR (R16-R31),
+/// which V1/V2 cannot encode. Shared by frame lowering and the WinEH Unwind
+/// V2/V3 passes so their decisions cannot disagree.
+bool requireWinX64UnwindV3(const MachineFunction &MF);
+
InstructionSelector *createX86InstructionSelector(const X86TargetMachine &TM,
const X86Subtarget &,
const X86RegisterBankInfo &);
diff --git a/llvm/lib/Target/X86/X86FrameLowering.cpp b/llvm/lib/Target/X86/X86FrameLowering.cpp
index a6e1d998c051a..a657c7d3873e5 100644
--- a/llvm/lib/Target/X86/X86FrameLowering.cpp
+++ b/llvm/lib/Target/X86/X86FrameLowering.cpp
@@ -12,6 +12,7 @@
#include "X86FrameLowering.h"
#include "MCTargetDesc/X86MCTargetDesc.h"
+#include "X86.h"
#include "X86InstrBuilder.h"
#include "X86InstrInfo.h"
#include "X86MachineFunctionInfo.h"
@@ -46,6 +47,23 @@ STATISTIC(NumFunctionUsingPush2Pop2, "Number of functions using push2/pop2");
using namespace llvm;
+bool llvm::requireWinX64UnwindV3(const MachineFunction &MF) {
+ const Function &Fn = MF.getFunction();
+
+ // Whole module is in V3 mode.
+ if (Fn.getParent()->getWinX64EHUnwindMode() == WinX64EHUnwindMode::V3)
+ return true;
+
+ // Otherwise promote a function that may use EGPR (R16-R31), which V1/V2
+ // unwind codes cannot encode. The per-function "+egpr" feature is the signal,
+ // so an auto-dispatch APX clone gets V3 while the baseline clone stays on the
+ // module default. We conservatively promote any egpr function rather than
+ // checking for an actual EGPR save, keeping this a cheap query. (PUSH2/POP2
+ // does not need V3: V1/V2 describe a PUSH2 as two SEH_PushReg codes.)
+ return Fn.needsUnwindTableEntry() &&
+ MF.getSubtarget<X86Subtarget>().hasEGPR();
+}
+
static const TargetRegisterClass *
getCalleeSavedSpillRC(MCRegister Reg, const X86Subtarget &STI,
const TargetRegisterInfo &TRI) {
@@ -1624,9 +1642,7 @@ void X86FrameLowering::emitPrologue(MachineFunction &MF,
MF.getFunction().getParent()->getCodeViewFlag();
bool NeedsWinCFI = NeedsWin64CFI || NeedsWinFPO;
bool NeedsDwarfCFI = needsDwarfCFI(MF);
- bool IsWin64UnwindV3 =
- NeedsWin64CFI &&
- Fn.getParent()->getWinX64EHUnwindMode() == WinX64EHUnwindMode::V3;
+ bool IsWin64UnwindV3 = NeedsWin64CFI && requireWinX64UnwindV3(MF);
Register FramePtr = TRI->getFrameRegister(MF);
const Register MachineFramePtr =
STI.isTarget64BitILP32() ? Register(getX86SubSuperRegister(FramePtr, 64))
@@ -2508,9 +2524,7 @@ void X86FrameLowering::emitEpilogue(MachineFunction &MF,
// For V3 unwind, epilog SEH pseudos are emitted inline before each
// unwind-effecting instruction.
bool IsWin64UnwindV3 =
- NeedsWin64CFI && MF.hasWinCFI() &&
- MF.getFunction().getParent()->getWinX64EHUnwindMode() ==
- WinX64EHUnwindMode::V3;
+ NeedsWin64CFI && MF.hasWinCFI() && requireWinX64UnwindV3(MF);
bool IsFunclet = MBBI == MBB.end() ? false : isFuncletReturnInstr(*MBBI);
// Get the number of bytes to allocate from the FrameInfo.
@@ -3292,9 +3306,7 @@ bool X86FrameLowering::restoreCalleeSavedRegisters(
bool NeedsWin64CFI =
isWin64Prologue(MF) && MF.getFunction().needsUnwindTableEntry();
- bool IsWin64UnwindV3 =
- NeedsWin64CFI && MF.getFunction().getParent()->getWinX64EHUnwindMode() ==
- WinX64EHUnwindMode::V3;
+ bool IsWin64UnwindV3 = NeedsWin64CFI && requireWinX64UnwindV3(MF);
// Reload XMMs from stack frame.
for (const CalleeSavedInfo &I : CSI) {
diff --git a/llvm/lib/Target/X86/X86InstrCompiler.td b/llvm/lib/Target/X86/X86InstrCompiler.td
index 5a6597affd51f..d3e8fdbdcd53c 100644
--- a/llvm/lib/Target/X86/X86InstrCompiler.td
+++ b/llvm/lib/Target/X86/X86InstrCompiler.td
@@ -260,7 +260,8 @@ let isPseudo = 1, isMeta = 1, isNotDuplicable = 1, SchedRW = [WriteSystem] in {
"#SEH_PushFrame $mode", []>;
def SEH_EndPrologue : I<0, Pseudo, (outs), (ins),
"#SEH_EndPrologue", []>;
- def SEH_UnwindVersion : I<0, Pseudo, (outs), (ins i1imm:$version),
+ // Widened from i1imm to i8imm to hold V3 (the version can be 1, 2 or 3).
+ def SEH_UnwindVersion : I<0, Pseudo, (outs), (ins i8imm:$version),
"#SEH_UnwindVersion $version", []>;
}
diff --git a/llvm/lib/Target/X86/X86WinEHUnwindV2.cpp b/llvm/lib/Target/X86/X86WinEHUnwindV2.cpp
index 254a91174de7c..79b91206e9d5b 100644
--- a/llvm/lib/Target/X86/X86WinEHUnwindV2.cpp
+++ b/llvm/lib/Target/X86/X86WinEHUnwindV2.cpp
@@ -383,6 +383,12 @@ bool runX86WinEHUnwindV2(MachineFunction &MF) {
Mode != WinX64EHUnwindMode::V2Required)
return false;
+ // A function that requires V3 (see requireWinX64UnwindV3()) is emitted as V3
+ // instead; skip it here so this pass does not stamp V2 pseudos that conflict
+ // with the V3 layout.
+ if (requireWinX64UnwindV3(MF))
+ return false;
+
// Requested changes.
SmallVector<FrameInfo> FrameInfos;
MachineFunction::iterator Iter = MF.begin();
diff --git a/llvm/lib/Target/X86/X86WinEHUnwindV3.cpp b/llvm/lib/Target/X86/X86WinEHUnwindV3.cpp
index 162633e2fddf3..4efedcd796f21 100644
--- a/llvm/lib/Target/X86/X86WinEHUnwindV3.cpp
+++ b/llvm/lib/Target/X86/X86WinEHUnwindV3.cpp
@@ -13,7 +13,10 @@
/// 2. Check V3 capacity limits (<=31 prolog/epilog ops, <=7 epilogs).
/// 3. Insert sub-fragment split points if limits are exceeded.
///
-/// The unwind version is set module-wide, not per-function.
+/// The unwind version is normally module-wide. When only an individual function
+/// needs V3 (see requireWinX64UnwindV3()), this pass stamps each of its frames
+/// -- the entry block and every funclet -- with a per-function
+/// .seh_unwindversion 3, leaving the rest of the module on its default version.
///
/// See https://learn.microsoft.com/en-us/cpp/build/x64-unwind-information-v3
///
@@ -225,30 +228,21 @@ FuncletInfo X86WinEHUnwindV3::analyzeFunclet(MachineFunction &MF,
}
bool X86WinEHUnwindV3::runOnMachineFunction(MachineFunction &MF) {
- WinX64EHUnwindMode Mode =
- MF.getFunction().getParent()->getWinX64EHUnwindMode();
-
Function &F = MF.getFunction();
LLVMContext &Ctx = F.getContext();
- // EGPR (R16-R31) requires V3 unwind info because V1/V2 cannot encode
- // registers beyond R15. Only enforce this for functions that actually
- // emit SEH unwind info — `nounwind` functions and targets that don't
- // require unwind tables (e.g. cross-compilation host defaults) can use
- // EGPR with any unwind mode since no SEH metadata is generated.
- if (Mode != WinX64EHUnwindMode::V3) {
- if (!F.needsUnwindTableEntry())
- return false;
- const auto &STI = MF.getSubtarget<X86Subtarget>();
- if (STI.hasEGPR()) {
- Ctx.diagnose(DiagnosticInfoUnsupported(
- F, "EGPR (R16-R31) requires V3 unwind info on Windows x64"));
- // Stripping the SEH pseudos modifies the function, so report a change.
- suppressWinCFI(MF);
- return true;
- }
+ if (!requireWinX64UnwindV3(MF))
return false;
- }
+
+ // Emit a per-function .seh_unwindversion 3 only when V3 is enabled for this
+ // function alone: in module-wide V3 the AsmPrinter emits it once, so stamping
+ // here would duplicate it. The gate also requires WinCFI -- without a
+ // .seh_proc there is nothing to version, and a lone SEH pseudo would trip an
+ // AsmPrinter assertion. The marker is per .seh_proc, hence stamped on each
+ // funclet in the loop below.
+ bool PerFunctionV3 =
+ MF.hasWinCFI() && MF.getFunction().getParent()->getWinX64EHUnwindMode() !=
+ WinX64EHUnwindMode::V3;
bool Changed = false;
unsigned ApproxBytePos = 0;
@@ -259,6 +253,21 @@ bool X86WinEHUnwindV3::runOnMachineFunction(MachineFunction &MF) {
// Process each funclet (and the main function body) independently.
// Each funclet gets its own UNWIND_INFO, so V3 limits apply per funclet.
while (Iter != MF.end()) {
+ // Iter points at the first block of a frame -- the entry frame on the
+ // first iteration, an EH funclet on later ones. Each frame is its own
+ // .seh_proc, so stamp the version on each here before analyzeFunclet
+ // advances past it.
+ if (PerFunctionV3) {
+ const TargetInstrInfo *TII = MF.getSubtarget().getInstrInfo();
+ MachineBasicBlock &FuncletEntry = *Iter;
+ BuildMI(FuncletEntry, FuncletEntry.begin(),
+ FuncletEntry.findDebugLoc(FuncletEntry.begin()),
+ TII->get(X86::SEH_UnwindVersion))
+ .addImm(3)
+ .setMIFlag(MachineInstr::FrameSetup);
+ Changed = true;
+ }
+
FuncletInfo Info = analyzeFunclet(MF, Iter, ApproxBytePos);
if (Info.PrologOpCount > MaxV3PrologOps) {
diff --git a/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-funclet.ll b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-funclet.ll
new file mode 100644
index 0000000000000..dec4b0364db03
--- /dev/null
+++ b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-funclet.ll
@@ -0,0 +1,31 @@
+; RUN: llc -mtriple=x86_64-unknown-windows-msvc -o - %s | FileCheck %s
+
+; A per-function V3 promotion stamps every WinEH frame -- the entry frame and
+; each EH funclet (each its own .seh_proc) -- with .seh_unwindversion 3.
+
+; Entry frame is V3.
+; CHECK-LABEL: f_eh:
+; CHECK: .seh_proc f_eh
+; CHECK: .seh_unwindversion 3
+; CHECK: .seh_endproc
+
+; The cleanup funclet is a separate .seh_proc and must also be V3.
+; CHECK: .seh_proc "?dtor${{[^"]*}}"
+; CHECK: .seh_unwindversion 3
+; CHECK: .seh_endproc
+
+define dso_local void @f_eh() #0 personality ptr @__C_specific_handler {
+entry:
+ invoke void @c() to label %ok unwind label %cu
+ok:
+ ret void
+cu:
+ %tok = cleanuppad within none []
+ call void @d() [ "funclet"(token %tok) ]
+ cleanupret from %tok unwind to caller
+}
+
+declare void @c()
+declare void @d()
+declare dso_local i32 @__C_specific_handler(...)
+attributes #0 = { uwtable "target-features"="+egpr" }
diff --git a/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-no-wincfi.ll b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-no-wincfi.ll
new file mode 100644
index 0000000000000..722341418e0df
--- /dev/null
+++ b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-no-wincfi.ll
@@ -0,0 +1,16 @@
+; RUN: llc -mtriple=x86_64-unknown-windows-msvc -o - %s | FileCheck %s
+
+; An egpr leaf with no WinCFI (saves nothing) must NOT get a .seh_proc or
+; .seh_unwindversion: the per-function marker is gated on MF.hasWinCFI().
+
+; CHECK-LABEL: leaf:
+; CHECK-NOT: .seh_proc
+; CHECK-NOT: .seh_unwindversion
+; CHECK: retq
+
+define dso_local void @leaf() #0 {
+entry:
+ ret void
+}
+
+attributes #0 = { uwtable "target-features"="+egpr" }
diff --git a/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-per-function.ll b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-per-function.ll
new file mode 100644
index 0000000000000..bdc36804e990a
--- /dev/null
+++ b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-per-function.ll
@@ -0,0 +1,32 @@
+; RUN: llc -mtriple=x86_64-unknown-windows-msvc -o - %s | FileCheck %s
+
+; Models auto-dispatch: in a V1-default module, only the egpr clone
+; is promoted to V3; the baseline clone stays on the module default.
+
+; The baseline function must NOT get a per-function unwind version.
+; CHECK-LABEL: baseline:
+; CHECK: .seh_proc baseline
+; CHECK-NOT: .seh_unwindversion
+; CHECK: .seh_endproc
+
+; The APX clone may use EGPR and is promoted to V3 individually.
+; CHECK-LABEL: apx_clone:
+; CHECK: .seh_proc apx_clone
+; CHECK: .seh_unwindversion 3
+; CHECK: .seh_endproc
+
+define dso_local void @baseline() #1 {
+entry:
+ call void @ext()
+ ret void
+}
+
+define dso_local void @apx_clone() #0 {
+entry:
+ call void @ext()
+ ret void
+}
+
+declare void @ext()
+attributes #0 = { uwtable "target-features"="+egpr" }
+attributes #1 = { uwtable }
diff --git a/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-required.ll b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-required.ll
deleted file mode 100644
index f33893a9e1d88..0000000000000
--- a/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-required.ll
+++ /dev/null
@@ -1,16 +0,0 @@
-; RUN: not llc -mtriple=x86_64-unknown-windows-msvc -mattr=+egpr -o /dev/null %s 2>&1 | FileCheck %s
-; CHECK: error: {{.*}}: in function func {{.*}}: EGPR (R16-R31) requires V3 unwind info on Windows x64
-
-; EGPR enabled without V3 unwind (default V1) should produce a recoverable
-; backend diagnostic (no crash, no stack trace).
-; The uwtable attribute and the call site force a stack frame and SEH unwind
-; info emission so the V3 pass runs regardless of the host platform's default
-; (matters for cross-compilation on Linux/macOS hosts targeting Windows).
-
-declare void @other()
-
-define dso_local void @func() uwtable {
-entry:
- call void @other()
- ret void
-}
diff --git a/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-v2-module.ll b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-v2-module.ll
new file mode 100644
index 0000000000000..f914b20b0e29d
--- /dev/null
+++ b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-v2-module.ll
@@ -0,0 +1,39 @@
+; RUN: llc -mtriple=x86_64-unknown-windows-msvc -o - %s | FileCheck %s
+
+; In a V2-default module, the egpr function is promoted to V3 (with no V2
+; markers) while the non-egpr function stays on V2.
+
+; The non-EGPR function stays on V2 with its V2 epilog markers.
+; CHECK-LABEL: v2_baseline:
+; CHECK: .seh_proc v2_baseline
+; CHECK: .seh_unwindversion 2
+; CHECK-NOT: .seh_unwindversion 3
+; CHECK: .seh_unwindv2start
+; CHECK: .seh_endproc
+
+; The EGPR function is emitted as V3 with no V2 markers.
+; CHECK-LABEL: v2_egpr:
+; CHECK: .seh_proc v2_egpr
+; CHECK: .seh_unwindversion 3
+; CHECK-NOT: .seh_unwindversion 2
+; CHECK-NOT: .seh_unwindv2start
+; CHECK: .seh_endproc
+
+define dso_local void @v2_baseline() #1 {
+entry:
+ call void @ext()
+ ret void
+}
+
+define dso_local void @v2_egpr() #0 {
+entry:
+ call void @ext()
+ ret void
+}
+
+declare void @ext()
+attributes #0 = { uwtable "target-features"="+egpr" }
+attributes #1 = { uwtable }
+
+!llvm.module.flags = !{!0}
+!0 = !{i32 1, !"winx64-eh-unwind", i32 2}
diff --git a/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-version-switch-v1.ll b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-version-switch-v1.ll
new file mode 100644
index 0000000000000..efd707e8072e6
--- /dev/null
+++ b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-version-switch-v1.ll
@@ -0,0 +1,53 @@
+; RUN: llc -mtriple=x86_64-unknown-windows-msvc -o - %s | FileCheck %s
+
+; Like win64-eh-unwindv3-egpr-version-switch.ll but in a V1-default
+; module: each egpr function toggles to V3, each non-egpr function stays on V1
+; (no marker).
+
+; CHECK-LABEL: a_egpr:
+; CHECK: .seh_proc a_egpr
+; CHECK: .seh_unwindversion 3
+; CHECK: .seh_endproc
+
+; CHECK-LABEL: b_base:
+; CHECK: .seh_proc b_base
+; CHECK-NOT: .seh_unwindversion
+; CHECK: .seh_endproc
+
+; CHECK-LABEL: c_egpr:
+; CHECK: .seh_proc c_egpr
+; CHECK: .seh_unwindversion 3
+; CHECK: .seh_endproc
+
+; CHECK-LABEL: d_base:
+; CHECK: .seh_proc d_base
+; CHECK-NOT: .seh_unwindversion
+; CHECK: .seh_endproc
+
+define dso_local void @a_egpr() #0 {
+entry:
+ call void @ext()
+ ret void
+}
+
+define dso_local void @b_base() #1 {
+entry:
+ call void @ext()
+ ret void
+}
+
+define dso_local void @c_egpr() #0 {
+entry:
+ call void @ext()
+ ret void
+}
+
+define dso_local void @d_base() #1 {
+entry:
+ call void @ext()
+ ret void
+}
+
+declare void @ext()
+attributes #0 = { uwtable "target-features"="+egpr" }
+attributes #1 = { uwtable }
diff --git a/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-version-switch.ll b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-version-switch.ll
new file mode 100644
index 0000000000000..6136fb64516e8
--- /dev/null
+++ b/llvm/test/CodeGen/X86/win64-eh-unwindv3-egpr-version-switch.ll
@@ -0,0 +1,47 @@
+; RUN: llc -mtriple=x86_64-unknown-windows-msvc -o - %s | FileCheck %s
+
+; Interleaved egpr / non-egpr functions in a V2 module: the per-function
+; version must alternate both ways, 3, 2, 3, 2.
+
+; CHECK-LABEL: a_egpr:
+; CHECK: .seh_unwindversion 3
+; CHECK-NOT: .seh_unwindversion
+; CHECK-LABEL: b_base:
+; CHECK: .seh_unwindversion 2
+; CHECK-NOT: .seh_unwindversion
+; CHECK-LABEL: c_egpr:
+; CHECK: .seh_unwindversion 3
+; CHECK-NOT: .seh_unwindversion
+; CHECK-LABEL: d_base:
+; CHECK: .seh_unwindversion 2
+
+define dso_local void @a_egpr() #0 {
+entry:
+ call void @ext()
+ ret void
+}
+
+define dso_local void @b_base() #1 {
+entry:
+ call void @ext()
+ ret void
+}
+
+define dso_local void @c_egpr() #0 {
+entry:
+ call void @ext()
+ ret void
+}
+
+define dso_local void @d_base() #1 {
+entry:
+ call void @ext()
+ ret void
+}
+
+declare void @ext()
+attributes #0 = { uwtable "target-features"="+egpr" }
+attributes #1 = { uwtable }
+
+!llvm.module.flags = !{!0}
+!0 = !{i32 1, !"winx64-eh-unwind", i32 2}
``````````
</details>
https://github.com/llvm/llvm-project/pull/213759
More information about the llvm-branch-commits
mailing list