[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