[llvm] [WinEH] Only emit the async-EH nop after EH_LABEL on x86 (PR #219275)
Eli Friedman via llvm-commits
llvm-commits at lists.llvm.org
Wed Sep 16 13:59:20 PDT 2026
https://github.com/efriedma-quic updated https://github.com/llvm/llvm-project/pull/219275
>From 9601c1c0332210eb43303112fa77efff1bbe1dd7 Mon Sep 17 00:00:00 2001
From: Jonas Heinle <jonasheinle at googlemail.com>
Date: Thu, 27 Aug 2026 21:06:53 +0200
Subject: [PATCH 1/3] [AArch64] Count the async-EH nop that follows an EH_LABEL
Under /EHa the AsmPrinter emits a nop after an EH_LABEL when the next
instruction can fault, so the fault lands in the right EH region.
getInstSizeInBytes says EH_LABEL is a meta-instruction and reports 0, so
every block-size estimate on the MIR level is 4 bytes short per label.
That is not cosmetic on AArch64. BranchRelaxation and
AArch64CompressJumpTables both pick an encoding from that estimate, and
both then get contradicted by the assembler:
error: fixup value out of range
error: value evaluated as <N> is out of range.
Neither diagnostic names a source location or an instruction.
Assisted-by: Claude (Anthropic)
---
llvm/lib/Target/AArch64/AArch64InstrInfo.cpp | 10 ++++
.../AArch64/wineh-eha-ehlabel-size.mir | 52 +++++++++++++++++++
2 files changed, 62 insertions(+)
create mode 100644 llvm/test/CodeGen/AArch64/wineh-eha-ehlabel-size.mir
diff --git a/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp b/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
index 7b32461d5164b..df38640d248df 100644
--- a/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
+++ b/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
@@ -221,6 +221,16 @@ unsigned AArch64InstrInfo::getInstSizeInBytes(const MachineInstr &MI) const {
return getInlineAsmLength(MI.getOperand(0).getSymbolName(), MAI);
}
+ // Under async EH the AsmPrinter emits a nop after an EH_LABEL when the next
+ // instruction can fault, so this has to come before the meta check below.
+ if (MI.getOpcode() == TargetOpcode::EH_LABEL &&
+ F.getParent()->getModuleFlag("eh-asynch")) {
+ auto Next = std::next(MI.getIterator());
+ if (Next != MBB.end() &&
+ (Next->mayLoadOrStore() || Next->mayRaiseFPException()))
+ return 4;
+ }
+
// Meta-instructions emit no code.
if (MI.isMetaInstruction())
return 0;
diff --git a/llvm/test/CodeGen/AArch64/wineh-eha-ehlabel-size.mir b/llvm/test/CodeGen/AArch64/wineh-eha-ehlabel-size.mir
new file mode 100644
index 0000000000000..ff7df72e37f9c
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/wineh-eha-ehlabel-size.mir
@@ -0,0 +1,52 @@
+# Under async EH the AsmPrinter emits a nop after an EH_LABEL whose next
+# instruction can fault, so EH_LABEL is not free and BranchRelaxation has to
+# count it. The branch range is narrowed so eight labels decide it: without
+# counting them the TBZ looks in range and is left alone.
+# RUN: llc -mtriple=aarch64-pc-windows-msvc -run-pass branch-relaxation \
+# RUN: -aarch64-tbz-offset-bits=5 %s -o - | FileCheck %s
+
+# CHECK-LABEL: name: eha
+# CHECK: TBNZW renamable $w0, 0, %bb.1
+# CHECK-NEXT: B %bb.2
+
+--- |
+ define void @eha(i32 %x, ptr %p) {
+ ret void
+ }
+
+ !llvm.module.flags = !{!0}
+ !0 = !{i32 2, !"eh-asynch", i32 1}
+...
+---
+name: eha
+tracksRegLiveness: true
+body: |
+ bb.0:
+ successors: %bb.1, %bb.2
+ liveins: $w0, $x1
+ TBZW renamable $w0, 0, %bb.2
+
+ bb.1:
+ successors: %bb.2
+ liveins: $x1
+ EH_LABEL <mcsymbol .Ltmp0>
+ renamable $x2 = LDRXui renamable $x1, 0
+ EH_LABEL <mcsymbol .Ltmp1>
+ renamable $x2 = LDRXui renamable $x1, 1
+ EH_LABEL <mcsymbol .Ltmp2>
+ renamable $x2 = LDRXui renamable $x1, 2
+ EH_LABEL <mcsymbol .Ltmp3>
+ renamable $x2 = LDRXui renamable $x1, 3
+ EH_LABEL <mcsymbol .Ltmp4>
+ renamable $x2 = LDRXui renamable $x1, 4
+ EH_LABEL <mcsymbol .Ltmp5>
+ renamable $x2 = LDRXui renamable $x1, 5
+ EH_LABEL <mcsymbol .Ltmp6>
+ renamable $x2 = LDRXui renamable $x1, 6
+ EH_LABEL <mcsymbol .Ltmp7>
+ renamable $x2 = LDRXui renamable $x1, 7
+ B %bb.2
+
+ bb.2:
+ RET undef $lr
+...
>From 89f40020bb750bdcd5b970bc2443b00b25a07d5f Mon Sep 17 00:00:00 2001
From: Jonas Heinle <jonasheinle at googlemail.com>
Date: Fri, 4 Sep 2026 11:17:39 +0200
Subject: [PATCH 2/3] [AArch64] Switch to not emitting the async-EH nop
Per review: rather than teaching getInstSizeInBytes to count the nop the
AsmPrinter emits after an EH_LABEL, do not emit it on Arm64 at all. The
unwinder there backs the PC up over a call itself, so the padding buys
nothing, and with no nop EH_LABEL really is zero-size -- the size estimate
is then correct by construction instead of by two places agreeing on the
same condition.
Drops the AArch64InstrInfo.cpp change and its MIR test in favour of
CodeGen/WinEH/wineh-eha-nop.ll, which covers both targets in one file.
Assisted-by: Claude (Anthropic)
---
llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp | 7 ++-
llvm/lib/Target/AArch64/AArch64InstrInfo.cpp | 10 ----
.../AArch64/wineh-eha-ehlabel-size.mir | 52 -------------------
llvm/test/CodeGen/WinEH/wineh-eha-nop.ll | 41 +++++++++++++++
4 files changed, 46 insertions(+), 64 deletions(-)
delete mode 100644 llvm/test/CodeGen/AArch64/wineh-eha-ehlabel-size.mir
create mode 100644 llvm/test/CodeGen/WinEH/wineh-eha-nop.ll
diff --git a/llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp b/llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp
index fe034ac8c97c5..a7aa1a17f135a 100644
--- a/llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp
@@ -2097,7 +2097,10 @@ void AsmPrinter::emitFunctionBody() {
// Print out code for the function.
bool HasAnyRealCode = false;
int NumInstsInFunction = 0;
- bool IsEHa = MMI->getModule()->getModuleFlag("eh-asynch");
+ // The AArch64 unwinder backs the PC up over a call itself, so it does not
+ // need the padding the EH_LABEL case below emits for the other targets.
+ bool NeedsEHaNops = MMI->getModule()->getModuleFlag("eh-asynch") &&
+ !TM.getTargetTriple().isAArch64();
const MCSubtargetInfo *STI = nullptr;
if (this->MF)
@@ -2190,7 +2193,7 @@ void AsmPrinter::emitFunctionBody() {
// an EH region as it must be led by at least a Load
{
auto MI2 = std::next(MI.getIterator());
- if (IsEHa && MI2 != MBB.end() &&
+ if (NeedsEHaNops && MI2 != MBB.end() &&
(MI2->mayLoadOrStore() || MI2->mayRaiseFPException()))
emitNops(1);
}
diff --git a/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp b/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
index df38640d248df..7b32461d5164b 100644
--- a/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
+++ b/llvm/lib/Target/AArch64/AArch64InstrInfo.cpp
@@ -221,16 +221,6 @@ unsigned AArch64InstrInfo::getInstSizeInBytes(const MachineInstr &MI) const {
return getInlineAsmLength(MI.getOperand(0).getSymbolName(), MAI);
}
- // Under async EH the AsmPrinter emits a nop after an EH_LABEL when the next
- // instruction can fault, so this has to come before the meta check below.
- if (MI.getOpcode() == TargetOpcode::EH_LABEL &&
- F.getParent()->getModuleFlag("eh-asynch")) {
- auto Next = std::next(MI.getIterator());
- if (Next != MBB.end() &&
- (Next->mayLoadOrStore() || Next->mayRaiseFPException()))
- return 4;
- }
-
// Meta-instructions emit no code.
if (MI.isMetaInstruction())
return 0;
diff --git a/llvm/test/CodeGen/AArch64/wineh-eha-ehlabel-size.mir b/llvm/test/CodeGen/AArch64/wineh-eha-ehlabel-size.mir
deleted file mode 100644
index ff7df72e37f9c..0000000000000
--- a/llvm/test/CodeGen/AArch64/wineh-eha-ehlabel-size.mir
+++ /dev/null
@@ -1,52 +0,0 @@
-# Under async EH the AsmPrinter emits a nop after an EH_LABEL whose next
-# instruction can fault, so EH_LABEL is not free and BranchRelaxation has to
-# count it. The branch range is narrowed so eight labels decide it: without
-# counting them the TBZ looks in range and is left alone.
-# RUN: llc -mtriple=aarch64-pc-windows-msvc -run-pass branch-relaxation \
-# RUN: -aarch64-tbz-offset-bits=5 %s -o - | FileCheck %s
-
-# CHECK-LABEL: name: eha
-# CHECK: TBNZW renamable $w0, 0, %bb.1
-# CHECK-NEXT: B %bb.2
-
---- |
- define void @eha(i32 %x, ptr %p) {
- ret void
- }
-
- !llvm.module.flags = !{!0}
- !0 = !{i32 2, !"eh-asynch", i32 1}
-...
----
-name: eha
-tracksRegLiveness: true
-body: |
- bb.0:
- successors: %bb.1, %bb.2
- liveins: $w0, $x1
- TBZW renamable $w0, 0, %bb.2
-
- bb.1:
- successors: %bb.2
- liveins: $x1
- EH_LABEL <mcsymbol .Ltmp0>
- renamable $x2 = LDRXui renamable $x1, 0
- EH_LABEL <mcsymbol .Ltmp1>
- renamable $x2 = LDRXui renamable $x1, 1
- EH_LABEL <mcsymbol .Ltmp2>
- renamable $x2 = LDRXui renamable $x1, 2
- EH_LABEL <mcsymbol .Ltmp3>
- renamable $x2 = LDRXui renamable $x1, 3
- EH_LABEL <mcsymbol .Ltmp4>
- renamable $x2 = LDRXui renamable $x1, 4
- EH_LABEL <mcsymbol .Ltmp5>
- renamable $x2 = LDRXui renamable $x1, 5
- EH_LABEL <mcsymbol .Ltmp6>
- renamable $x2 = LDRXui renamable $x1, 6
- EH_LABEL <mcsymbol .Ltmp7>
- renamable $x2 = LDRXui renamable $x1, 7
- B %bb.2
-
- bb.2:
- RET undef $lr
-...
diff --git a/llvm/test/CodeGen/WinEH/wineh-eha-nop.ll b/llvm/test/CodeGen/WinEH/wineh-eha-nop.ll
new file mode 100644
index 0000000000000..d221cde28f106
--- /dev/null
+++ b/llvm/test/CodeGen/WinEH/wineh-eha-nop.ll
@@ -0,0 +1,41 @@
+; RUN: llc -mtriple=x86_64-pc-windows-msvc < %s | FileCheck %s --check-prefix=X64
+; RUN: %if aarch64-registered-target %{ llc -mtriple=aarch64-pc-windows-msvc < %s | FileCheck %s --check-prefix=ARM64 %}
+
+; x86 pads a faulting instruction that opens an EH region away from the return
+; of the call before it. The Arm64 unwinder backs the PC up itself, so it does
+; not need that nop.
+
+declare i32 @__CxxFrameHandler3(...)
+declare void @llvm.seh.scope.begin()
+declare void @llvm.seh.scope.end()
+declare void @dtor(ptr)
+
+define void @f(ptr %p) personality ptr @__CxxFrameHandler3 {
+entry:
+ invoke void @llvm.seh.scope.begin()
+ to label %cont unwind label %cleanup
+
+cont:
+ store volatile i32 0, ptr %p, align 4
+ invoke void @llvm.seh.scope.end()
+ to label %done unwind label %cleanup
+
+done:
+ ret void
+
+cleanup:
+ %cp = cleanuppad within none []
+ call void @dtor(ptr %p) [ "funclet"(token %cp) ]
+ cleanupret from %cp unwind to caller
+}
+
+; X64-LABEL: f:
+; X64: # EH_LABEL
+; X64-NEXT: nop
+
+; ARM64-LABEL: f:
+; ARM64: // EH_LABEL
+; ARM64-NOT: nop
+
+!llvm.module.flags = !{!0}
+!0 = !{i32 2, !"eh-asynch", i32 1}
>From 15b39267ac7b1d1fcd1bacf474c68bf7a62143bd Mon Sep 17 00:00:00 2001
From: Jonas Heinle <jonasheinle at googlemail.com>
Date: Wed, 16 Sep 2026 20:16:38 +0200
Subject: [PATCH 3/3] [WinEH] Restrict the async-EH nop to x86
Per review: gate on isX86() rather than !isAArch64(). Triple::isARM() does
not cover thumb, and Windows ARM32 normalises to a thumb triple, so a
negative predicate would keep emitting the nop on armv7-windows.
With no target left that both emits the nop and reports instruction sizes,
the EH_LABEL exception added to getInstSizeVerifyMode() in #223641 goes
too, along with the test that came with it: the condition it checks can no
longer occur.
Assisted-by: Claude (Anthropic)
---
llvm/include/llvm/CodeGen/TargetInstrInfo.h | 3 --
llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp | 5 ++-
.../AArch64/wineh-async-eh-pad-instr-size.ll | 34 -------------------
3 files changed, 2 insertions(+), 40 deletions(-)
delete mode 100644 llvm/test/CodeGen/AArch64/wineh-async-eh-pad-instr-size.ll
diff --git a/llvm/include/llvm/CodeGen/TargetInstrInfo.h b/llvm/include/llvm/CodeGen/TargetInstrInfo.h
index 260cf794c731f..eff16348a8bc0 100644
--- a/llvm/include/llvm/CodeGen/TargetInstrInfo.h
+++ b/llvm/include/llvm/CodeGen/TargetInstrInfo.h
@@ -450,9 +450,6 @@ class LLVM_ABI TargetInstrInfo : public MCInstrInfo {
/// getInstSizeInBytes() should be verified.
virtual InstSizeVerifyMode
getInstSizeVerifyMode(const MachineInstr &MI) const {
- // FIXME: Incorrect size reported if nop inserted for async exceptions.
- if (MI.getOpcode() == TargetOpcode::EH_LABEL)
- return InstSizeVerifyMode::NoVerify;
return InstSizeVerifyMode::AllowOverEstimate;
}
diff --git a/llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp b/llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp
index a7aa1a17f135a..52f8f148f7487 100644
--- a/llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp
+++ b/llvm/lib/CodeGen/AsmPrinter/AsmPrinter.cpp
@@ -2097,10 +2097,9 @@ void AsmPrinter::emitFunctionBody() {
// Print out code for the function.
bool HasAnyRealCode = false;
int NumInstsInFunction = 0;
- // The AArch64 unwinder backs the PC up over a call itself, so it does not
- // need the padding the EH_LABEL case below emits for the other targets.
+ // Only x86 needs this padding; the Arm unwinders back the PC up themselves.
bool NeedsEHaNops = MMI->getModule()->getModuleFlag("eh-asynch") &&
- !TM.getTargetTriple().isAArch64();
+ TM.getTargetTriple().isX86();
const MCSubtargetInfo *STI = nullptr;
if (this->MF)
diff --git a/llvm/test/CodeGen/AArch64/wineh-async-eh-pad-instr-size.ll b/llvm/test/CodeGen/AArch64/wineh-async-eh-pad-instr-size.ll
deleted file mode 100644
index d6eca0b3a6a99..0000000000000
--- a/llvm/test/CodeGen/AArch64/wineh-async-eh-pad-instr-size.ll
+++ /dev/null
@@ -1,34 +0,0 @@
-; RUN: llc -mtriple=aarch64-unknown-windows-msvc -filetype=obj < %s | llvm-objdump -d - | FileCheck %s
-
-; Check that there is no instruction size verification error when a nop has
-; to be inserted for an EH_LABEL.
-
-; CHECK: nop
-define void @test() personality ptr @__C_specific_handler {
-entry:
- %a = alloca i32, align 4
- invoke void @llvm.seh.try.begin()
- to label %invoke.cont unwind label %catch.dispatch
-
-invoke.cont:
- %a.val = load volatile i32, ptr %a, align 4
- invoke void @llvm.seh.try.end()
- to label %exit unwind label %catch.dispatch
-
-catch.dispatch:
- %cs = catchswitch within none [label %__except.ret] unwind to caller
-
-__except.ret:
- %cp = catchpad within %cs [ptr @filt]
- catchret from %cp to label %exit
-
-exit:
- ret void
-}
-
-declare i32 @filt(ptr %exception_pointers, ptr %frame_pointer)
-
-declare i32 @__C_specific_handler(...)
-
-!llvm.module.flags = !{!1}
-!1 = !{i32 2, !"eh-asynch", i32 1}
More information about the llvm-commits
mailing list