[llvm] [x64][win] Windows x64 unwind v3: Use tail-relative epilog offsets and add size-based splitting (PR #203108)

Daniel Paoliello via llvm-commits llvm-commits at lists.llvm.org
Thu Jun 11 08:47:46 PDT 2026


https://github.com/dpaoliello updated https://github.com/llvm/llvm-project/pull/203108

>From 583eaa4136d0d19cb945fe476dd8e3eafa72b544 Mon Sep 17 00:00:00 2001
From: Daniel Paoliello <danpao at microsoft.com>
Date: Wed, 10 Jun 2026 14:30:44 -0700
Subject: [PATCH 1/2] [x64][win] Windows x64 unwind v3: Use tail-relative
 epilog offsets and add size-based splitting

Win64 Unwind v3 encodes each epilog's EpilogOffset as a signed 16-bit field.
The encoder previously measured the first epilog offset from the fragment
start, which overflowed for large functions and produced a cryptic
"<unknown>:0: value too large for field" error (and, on the early
.seh_handlerdata path, an assertion failure).

Two changes:

- MCWin64EH.cpp: Always emit epilog offsets tail-relative. The first epilog
  descriptor is measured from the fragment end and subsequent ones as deltas
  from the previous epilog, so descriptors are emitted in descending address
  order (all non-positive, per spec). A new lazy MCUnwindV3EpilogOffsetTargetExpr
  resolves the fragment-end-relative value at layout time (it may not have a
  symbol yet when emitted via .seh_handlerdata) and reports a clean,
  function-named diagnostic on genuine overflow.

- X86WinEHUnwindV3.cpp: Add Unwind-v2-style size-based sub-fragment splitting.
  In addition to the existing >7-epilog split, the pass now splits a funclet
  into chained fragments when an epilog is too far from its fragment tail
  (using the V2 approximate-instruction-count heuristic, tunable via
  -x86-wineh-unwindv3-epilog-distance-threshold). Splits use
  SEH_SplitChainedAtEndOfBlock so the split lands after the epilog, keeping
  every epilog close to its tail; a long trailing region becomes its own
  epilog-free chained fragment.

Updates seh-unwindv3-large.s expectations to the new tail-relative values and
adds win64-eh-unwindv3-split-large.ll covering size-based splitting.
---
 llvm/lib/MC/MCWin64EH.cpp                     | 113 ++++++++++++++---
 llvm/lib/Target/X86/X86WinEHUnwindV3.cpp      | 115 ++++++++++++++----
 .../X86/win64-eh-unwindv3-split-large.ll      |  62 ++++++++++
 llvm/test/MC/COFF/seh-unwindv3-large.s        |   8 +-
 4 files changed, 253 insertions(+), 45 deletions(-)
 create mode 100644 llvm/test/CodeGen/X86/win64-eh-unwindv3-split-large.ll

diff --git a/llvm/lib/MC/MCWin64EH.cpp b/llvm/lib/MC/MCWin64EH.cpp
index edc54235d8e9e..1e37b3b7d56a6 100644
--- a/llvm/lib/MC/MCWin64EH.cpp
+++ b/llvm/lib/MC/MCWin64EH.cpp
@@ -66,6 +66,49 @@ class MCUnwindV2EpilogTargetExpr final : public MCTargetExpr {
     return UnwindV2Start->getFragment();
   }
 };
+
+/// MCExpr representing a V3 epilog's tail-relative EpilogOffset field. The
+/// first epilog descriptor is encoded relative to the fragment end, and each
+/// subsequent descriptor relative to the previous epilog's start. The fragment
+/// end may not have a symbol yet when the unwind info is emitted (e.g. via
+/// .seh_handlerdata), so the value is resolved lazily through the FrameInfo
+/// reference.
+class MCUnwindV3EpilogOffsetTargetExpr final : public MCTargetExpr {
+  const WinEH::FrameInfo &FrameInfo;
+  const MCSymbol *EpilogStart;
+  const MCSymbol *PrevEpilogStart;
+  SMLoc Loc;
+
+  MCUnwindV3EpilogOffsetTargetExpr(const WinEH::FrameInfo &FrameInfo,
+                                   const MCSymbol *EpilogStart,
+                                   const MCSymbol *PrevEpilogStart, SMLoc Loc)
+      : FrameInfo(FrameInfo), EpilogStart(EpilogStart),
+        PrevEpilogStart(PrevEpilogStart), Loc(Loc) {}
+
+public:
+  static MCUnwindV3EpilogOffsetTargetExpr *
+  create(const WinEH::FrameInfo &FrameInfo, const MCSymbol *EpilogStart,
+         const MCSymbol *PrevEpilogStart, SMLoc Loc, MCContext &Ctx) {
+    return new (Ctx) MCUnwindV3EpilogOffsetTargetExpr(FrameInfo, EpilogStart,
+                                                      PrevEpilogStart, Loc);
+  }
+
+  void printImpl(raw_ostream &OS, const MCAsmInfo *MAI) const override {
+    OS << ":epilogoffset:";
+    EpilogStart->print(OS, MAI);
+  }
+
+  bool evaluateAsRelocatableImpl(MCValue &Res,
+                                 const MCAssembler *Asm) const override;
+
+  void visitUsedExpr(MCStreamer &Streamer) const override {
+    // Contains no sub-expressions.
+  }
+
+  MCFragment *findAssociatedFragment() const override {
+    return EpilogStart->getFragment();
+  }
+};
 } // namespace
 
 // NOTE: All relocations generated here are 4-byte image-relative.
@@ -578,6 +621,15 @@ static void EmitUnwindInfoV3(MCStreamer &Streamer, WinEH::FrameInfo *Info) {
                           " This function has " +
                           Twine(EpilogInfos.size()));
 
+  // V3 epilog offsets are always encoded tail-relative: the first descriptor
+  // holds a negative byte offset from the fragment end, and each subsequent
+  // descriptor a (negative) delta from the previous epilog's start. The spec
+  // requires every descriptor in a fragment to use the same sign, so the
+  // descriptors must be listed in descending address order ("descending from
+  // tail"). EpilogMap is in ascending address order, so reverse it here before
+  // computing inheritance and emitting descriptors.
+  std::reverse(EpilogInfos.begin(), EpilogInfos.end());
+
   // --- Inheritance decisions ---
   // An epilog can use the inherited (3-byte) descriptor when FirstOp,
   // NumberOfOps, NeedsLarge, and relative IP offsets all match the previous
@@ -713,24 +765,19 @@ static void EmitUnwindInfoV3(MCStreamer &Streamer, WinEH::FrameInfo *Info) {
     uint8_t EpiNumOps = EI.Inherited ? 0 : EI.NumberOfOps;
     Streamer.emitInt8((EpiNumOps << 3) | EpiFlags);
 
-    // EpilogOffset: signed 16-bit.
-    // For the first epilog: byte offset from fragment start to epilog start.
-    // For subsequent epilogs: delta from the previous epilog's start position.
-    // Emit as a fixup since we may not know the exact distance yet.
+    // EpilogOffset: signed 16-bit, always tail-relative.
+    // The first epilog descriptor (closest to the fragment tail) holds the
+    // negative byte offset from the fragment end to the epilog start; each
+    // subsequent descriptor holds the delta from the previous epilog's start.
+    // Because descriptors are emitted in descending address order, every offset
+    // is non-positive, satisfying the V3 "all epilogs use the same sign" rule.
+    // Measuring from the tail keeps the magnitude small (epilogs sit near the
+    // end of the function), avoiding overflow of the signed 16-bit field for
+    // large functions. The fragment end may not have a symbol yet (e.g. when
+    // emitted via .seh_handlerdata), so the value is resolved lazily.
     {
-      const MCSymbol *Base = PrevEpilogStart ? PrevEpilogStart : Info->Begin;
-      const MCExpr *EpilogOffsetExpr = MCBinaryExpr::createSub(
-          MCSymbolRefExpr::create(Epilog.Start, Context),
-          MCSymbolRefExpr::create(Base, Context), Context);
-      // Validate the epilog offset fits in a signed 16-bit field if we can
-      // evaluate it now.
-      int64_t OffsetValue;
-      if (EpilogOffsetExpr->evaluateAsAbsolute(OffsetValue,
-                                               OS->getAssembler())) {
-        if (OffsetValue < INT16_MIN || OffsetValue > INT16_MAX)
-          reportFatalUsageError(
-              "Epilog offset out of signed 16-bit range for V3 encoding");
-      }
+      const MCExpr *EpilogOffsetExpr = MCUnwindV3EpilogOffsetTargetExpr::create(
+          *Info, Epilog.Start, PrevEpilogStart, Epilog.Loc, Context);
       OS->ensureHeadroom(2);
       OS->addFixup(EpilogOffsetExpr, FK_Data_2);
       OS->appendContents(2, 0);
@@ -1022,6 +1069,38 @@ bool MCUnwindV2EpilogTargetExpr::evaluateAsRelocatableImpl(
   return true;
 }
 
+bool MCUnwindV3EpilogOffsetTargetExpr::evaluateAsRelocatableImpl(
+    MCValue &Res, const MCAssembler *Asm) const {
+  // The first epilog descriptor is encoded relative to the fragment tail (the
+  // first byte past the end of the fragment); subsequent descriptors are
+  // encoded relative to the previous epilog's start. Both bases yield a
+  // non-positive offset, as required by the V3 "same sign" rule.
+  const MCSymbol *Base = PrevEpilogStart ? PrevEpilogStart : FrameInfo.End;
+  if (!Base) {
+    Asm->getContext().reportError(
+        Loc, "Missing fragment end for V3 epilog offset in " +
+                 FrameInfo.Function->getName());
+    return false;
+  }
+
+  auto Offset = GetOptionalAbsDifference(*Asm, EpilogStart, Base);
+  if (!Offset) {
+    Asm->getContext().reportError(
+        Loc, "Failed to evaluate epilog offset for V3 unwind info in " +
+                 FrameInfo.Function->getName());
+    return false;
+  }
+  if (*Offset < INT16_MIN || *Offset > INT16_MAX) {
+    Asm->getContext().reportError(
+        Loc, "Epilog offset out of signed 16-bit range for V3 encoding in " +
+                 FrameInfo.Function->getName());
+    return false;
+  }
+
+  Res = MCValue::get(*Offset);
+  return true;
+}
+
 void llvm::Win64EH::UnwindEmitter::Emit(MCStreamer &Streamer) const {
   // Emit the unwind info structs first.
   for (const auto &CFI : Streamer.getWinFrameInfos()) {
diff --git a/llvm/lib/Target/X86/X86WinEHUnwindV3.cpp b/llvm/lib/Target/X86/X86WinEHUnwindV3.cpp
index 66d43d2bad217..108c8699d3805 100644
--- a/llvm/lib/Target/X86/X86WinEHUnwindV3.cpp
+++ b/llvm/lib/Target/X86/X86WinEHUnwindV3.cpp
@@ -32,6 +32,7 @@
 #include "llvm/IR/DiagnosticInfo.h"
 #include "llvm/IR/LLVMContext.h"
 #include "llvm/IR/Module.h"
+#include "llvm/Support/CommandLine.h"
 
 using namespace llvm;
 
@@ -47,6 +48,20 @@ static constexpr unsigned MaxV3PrologOps = 31;
 static constexpr unsigned MaxV3Epilogs = 7;
 static constexpr unsigned MaxV3EpilogOps = 31;
 
+/// Approximate-instruction-count distance between an epilog and its fragment
+/// tail beyond which the funclet is split into a new chained sub-fragment.
+/// The V3 EpilogOffset field is a signed 16-bit byte offset measured from the
+/// fragment tail, so each fragment must span less than 32 KiB of code. The
+/// exact byte offsets aren't known until MC layout, so (like the V2 pass) the
+/// approximate instruction count is used as a proxy, with margin for the
+/// average emitted instruction size.
+static cl::opt<unsigned> EpilogDistanceThreshold(
+    "x86-wineh-unwindv3-epilog-distance-threshold", cl::Hidden,
+    cl::desc("Maximum approximate instruction distance between an epilog and "
+             "its fragment tail before splitting into a new chained unwind "
+             "info for Unwind v3."),
+    cl::init(4000));
+
 /// After reporting a recoverable error for `MF`, erase all SEH pseudo-
 /// instructions and clear the WinCFI flag so the AsmPrinter doesn't try to
 /// emit (potentially malformed) unwind information. The LLVMContext
@@ -81,13 +96,23 @@ static void suppressWinCFI(MachineFunction &MF) {
 
 namespace {
 
+/// A V3 epilog and the approximate instruction position where it begins, used
+/// as a candidate sub-fragment split point.
+struct EpilogSplitPoint {
+  MachineInstr *BeginEpilog;
+  unsigned ApproxInstrPos;
+};
+
 /// Per-funclet analysis results.
 struct FuncletInfo {
   unsigned PrologOpCount = 0;
-  unsigned EpilogCount = 0;
   unsigned MaxEpilogOpCount = 0;
-  /// SEH_BeginEpilogue instructions, used as insertion points for splitting.
-  SmallVector<MachineInstr *, 8> EpilogBegins;
+  /// Approximate instruction position at the end of the funclet, used as the
+  /// initial fragment tail reference for size-based splitting.
+  unsigned EndInstrPos = 0;
+  /// SEH_BeginEpilogue instructions (with approximate positions), used as
+  /// candidate insertion points for sub-fragment splitting.
+  SmallVector<EpilogSplitPoint, 8> Epilogs;
 };
 
 class X86WinEHUnwindV3 : public MachineFunctionPass {
@@ -105,9 +130,12 @@ class X86WinEHUnwindV3 : public MachineFunctionPass {
 private:
   /// Analyze one funclet (or the main function body) starting at Iter.
   /// Advances Iter past the analyzed region, stopping at the next funclet
-  /// entry or the end of the function.
+  /// entry or the end of the function. ApproxInstrPos is a running count of
+  /// emitted instructions across the whole function, used to estimate the
+  /// byte distance between epilogs and their fragment tail.
   static FuncletInfo analyzeFunclet(MachineFunction &MF,
-                                    MachineFunction::iterator &Iter);
+                                    MachineFunction::iterator &Iter,
+                                    unsigned &ApproxInstrPos);
 };
 
 } // end anonymous namespace
@@ -123,7 +151,8 @@ FunctionPass *llvm::createX86WinEHUnwindV3Pass() {
 }
 
 FuncletInfo X86WinEHUnwindV3::analyzeFunclet(MachineFunction &MF,
-                                             MachineFunction::iterator &Iter) {
+                                             MachineFunction::iterator &Iter,
+                                             unsigned &ApproxInstrPos) {
   FuncletInfo Info;
   bool InEpilog = false;
   bool SeenProlog = false;
@@ -138,6 +167,12 @@ FuncletInfo X86WinEHUnwindV3::analyzeFunclet(MachineFunction &MF,
       break;
 
     for (MachineInstr &MI : MBB) {
+      // Approximate the number of emitted instructions, mirroring the V2 pass.
+      // This estimates how far each epilog sits from its fragment tail; the
+      // exact byte offsets aren't available until MC layout.
+      if (!MI.isPseudo() && !MI.isMetaInstruction())
+        ApproxInstrPos++;
+
       switch (MI.getOpcode()) {
       case X86::SEH_PushReg:
       case X86::SEH_Push2Regs:
@@ -157,8 +192,7 @@ FuncletInfo X86WinEHUnwindV3::analyzeFunclet(MachineFunction &MF,
       case X86::SEH_BeginEpilogue:
         InEpilog = true;
         CurrentEpilogOpCount = 0;
-        Info.EpilogCount++;
-        Info.EpilogBegins.push_back(&MI);
+        Info.Epilogs.push_back({&MI, ApproxInstrPos});
         break;
       case X86::SEH_EndEpilogue:
         InEpilog = false;
@@ -171,6 +205,7 @@ FuncletInfo X86WinEHUnwindV3::analyzeFunclet(MachineFunction &MF,
     }
   }
 
+  Info.EndInstrPos = ApproxInstrPos;
   return Info;
 }
 
@@ -201,12 +236,13 @@ bool X86WinEHUnwindV3::runOnMachineFunction(MachineFunction &MF) {
   }
 
   bool Changed = false;
+  unsigned ApproxInstrPos = 0;
   MachineFunction::iterator Iter = MF.begin();
 
   // 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()) {
-    FuncletInfo Info = analyzeFunclet(MF, Iter);
+    FuncletInfo Info = analyzeFunclet(MF, Iter, ApproxInstrPos);
 
     if (Info.PrologOpCount > MaxV3PrologOps) {
       Ctx.diagnose(DiagnosticInfoResourceLimit(
@@ -232,23 +268,54 @@ bool X86WinEHUnwindV3::runOnMachineFunction(MachineFunction &MF) {
       return true;
     }
 
-    if (Info.EpilogCount > MaxV3Epilogs) {
-      const TargetInstrInfo *TII = MF.getSubtarget().getInstrInfo();
-      unsigned Count = 0;
-      for (MachineInstr *BeginEpilog : Info.EpilogBegins) {
-        Count++;
-        if (Count > MaxV3Epilogs) {
-          MachineBasicBlock *MBB = BeginEpilog->getParent();
-          BuildMI(*MBB, BeginEpilog, BeginEpilog->getDebugLoc(),
-                  TII->get(X86::SEH_SplitChained));
-          BuildMI(*MBB, BeginEpilog, BeginEpilog->getDebugLoc(),
-                  TII->get(X86::SEH_EndPrologue));
-          SubFragmentSplits++;
-          Count = 1;
-        }
-      }
+    // Split the funclet into chained sub-fragments so that each fragment's
+    // UNWIND_INFO stays within the V3 capacity limits:
+    //   * at most 7 epilogs per fragment, and
+    //   * every epilog close enough to its fragment tail that the tail-relative
+    //     EpilogOffset fits in the signed 16-bit field.
+    // The exact byte offsets aren't known until MC layout, so (like the V2
+    // pass) the distance bound uses an approximate instruction count as a
+    // proxy. A SEH_SplitChainedAtEndOfBlock is inserted at the start of an
+    // epilog's block; the AsmPrinter emits the actual .seh_splitchained at the
+    // *end* of that block, so the epilog becomes the last epilog of the
+    // earlier fragment, immediately followed by the new chained fragment. This
+    // keeps every epilog close to its tail even when a single epilog is
+    // followed by a large amount of code (a long "tail" after the last
+    // epilog is pushed into its own epilog-free chained fragment).
+    const TargetInstrInfo *TII = MF.getSubtarget().getInstrInfo();
+    auto SplitAfter = [&](const EpilogSplitPoint &Epilog) {
+      MachineBasicBlock *MBB = Epilog.BeginEpilog->getParent();
+      BuildMI(*MBB, MBB->begin(), Epilog.BeginEpilog->getDebugLoc(),
+              TII->get(X86::SEH_SplitChainedAtEndOfBlock));
+      SubFragmentSplits++;
       Changed = true;
+    };
+
+    unsigned FragmentFirstPos = 0;
+    unsigned EpilogsInFragment = 0;
+    const EpilogSplitPoint *LastEpilog = nullptr;
+    for (const EpilogSplitPoint &Epilog : Info.Epilogs) {
+      // If adding this epilog would exceed a fragment limit, end the current
+      // fragment after the previous epilog and start a new one here.
+      if (EpilogsInFragment > 0 && (EpilogsInFragment >= MaxV3Epilogs ||
+                                    Epilog.ApproxInstrPos - FragmentFirstPos >=
+                                        EpilogDistanceThreshold)) {
+        SplitAfter(*LastEpilog);
+        EpilogsInFragment = 0;
+      }
+      if (EpilogsInFragment == 0)
+        FragmentFirstPos = Epilog.ApproxInstrPos;
+      EpilogsInFragment++;
+      LastEpilog = &Epilog;
     }
+
+    // If the last fragment's first epilog is too far from the funclet end,
+    // split after the last epilog so the trailing code becomes its own
+    // epilog-free chained fragment, keeping the last fragment's epilogs close
+    // to their tail.
+    if (LastEpilog &&
+        Info.EndInstrPos - FragmentFirstPos >= EpilogDistanceThreshold)
+      SplitAfter(*LastEpilog);
   }
 
   if (Changed)
diff --git a/llvm/test/CodeGen/X86/win64-eh-unwindv3-split-large.ll b/llvm/test/CodeGen/X86/win64-eh-unwindv3-split-large.ll
new file mode 100644
index 0000000000000..1ea323fca2896
--- /dev/null
+++ b/llvm/test/CodeGen/X86/win64-eh-unwindv3-split-large.ll
@@ -0,0 +1,62 @@
+; RUN: llc -mtriple=x86_64-unknown-windows-msvc \
+; RUN:   -x86-wineh-unwindv3-epilog-distance-threshold=1 -o - %s | FileCheck %s
+; RUN: llc -mtriple=x86_64-unknown-windows-msvc \
+; RUN:   -x86-wineh-unwindv3-epilog-distance-threshold=1 -filetype=obj %s -o - \
+; RUN:   | llvm-readobj --unwind - | FileCheck %s --check-prefix=OBJ
+
+; Test V3 *size-based* sub-fragment splitting (the "Unwind v2 style" heuristic).
+; With a very small distance threshold, every epilog is forced into its own
+; chained sub-fragment, even though the function has far fewer than 8 epilogs.
+; This exercises the path that keeps each tail-relative EpilogOffset within the
+; signed 16-bit field for large functions.
+
+declare i32 @c(i32) local_unnamed_addr
+
+; CHECK-LABEL: three_epilogs:
+; CHECK:         .seh_endprologue
+; First epilog stays in the main fragment.
+; CHECK:         .seh_startepilogue
+; CHECK:         .seh_endepilogue
+; A size-based split is inserted after each epilog.
+; CHECK:         .seh_splitchained
+; CHECK-NEXT:    .seh_endprologue
+; CHECK:         .seh_startepilogue
+; CHECK:         .seh_endepilogue
+; CHECK:         .seh_splitchained
+; CHECK-NEXT:    .seh_endprologue
+; CHECK:         .seh_startepilogue
+; CHECK:         .seh_endepilogue
+; CHECK:         .seh_endproc
+
+; Each fragment carries a single epilog with a small, in-range, tail-relative
+; (negative) EpilogOffset, and every chained fragment is marked CHAININFO.
+; OBJ:        Version: 3
+; OBJ:        NumberOfEpilogs: 1
+; OBJ:          EpilogOffset: -0x5
+; OBJ:      RuntimeFunction {
+; OBJ:        Version: 3
+; OBJ:          ChainInfo (0x4)
+; OBJ:        NumberOfEpilogs: 1
+; OBJ:          EpilogOffset: -0x5
+
+define dso_local i32 @three_epilogs(i32 %x) #0 {
+entry:
+  switch i32 %x, label %sw.default [
+    i32 0, label %sw.0
+    i32 1, label %sw.1
+  ]
+sw.0:
+  %r0 = call i32 @c(i32 0)
+  ret i32 %r0
+sw.1:
+  %r1 = call i32 @c(i32 1)
+  ret i32 %r1
+sw.default:
+  %rd = call i32 @c(i32 7)
+  ret i32 %rd
+}
+
+attributes #0 = { optnone noinline }
+
+!llvm.module.flags = !{!0}
+!0 = !{i32 1, !"winx64-eh-unwind", i32 3}
diff --git a/llvm/test/MC/COFF/seh-unwindv3-large.s b/llvm/test/MC/COFF/seh-unwindv3-large.s
index c0f35b0ba0e5f..694f156cfe277 100644
--- a/llvm/test/MC/COFF/seh-unwindv3-large.s
+++ b/llvm/test/MC/COFF/seh-unwindv3-large.s
@@ -45,7 +45,7 @@ large_prolog_known:
 // CHECK-NEXT:       [0] IP +0x0105: ALLOC_SMALL Size=0x20
 // CHECK-NEXT:       [1] IP +0x0000: PUSH Reg=RBX
 // CHECK:          Epilog [0] {
-// CHECK:            EpilogOffset: +0x10A
+// CHECK:            EpilogOffset: -0x6
 // CHECK-NEXT:       NumberOfOps: 2
 // CHECK-NEXT:       FirstOp: 0x0
 // CHECK-NEXT:       IpOffsetOfLastInstruction: 0x5
@@ -93,7 +93,7 @@ large_prolog_unevaluatable:
 // CHECK-NEXT:       [0] IP +0x00F0: ALLOC_SMALL Size=0x20
 // CHECK-NEXT:       [1] IP +0x0000: PUSH Reg=RBX
 // CHECK:          Epilog [0] {
-// CHECK:            EpilogOffset: +0xF5
+// CHECK:            EpilogOffset: -0x6
 // CHECK-NEXT:       NumberOfOps: 2
 // CHECK-NEXT:       FirstOp: 0x0
 // CHECK-NEXT:       IpOffsetOfLastInstruction: 0x5
@@ -143,7 +143,7 @@ large_epilog_known:
 // CHECK-NEXT:       Flags [ (0x2)
 // CHECK-NEXT:         Large (0x2)
 // CHECK-NEXT:       ]
-// CHECK-NEXT:       EpilogOffset: +0x6
+// CHECK-NEXT:       EpilogOffset: -0x10A
 // CHECK-NEXT:       NumberOfOps: 2
 // CHECK-NEXT:       FirstOp: 0x0
 // CHECK-NEXT:       IpOffsetOfLastInstruction: 0x109
@@ -189,7 +189,7 @@ large_epilog_unevaluatable:
 // CHECK-NEXT:       Flags [ (0x2)
 // CHECK-NEXT:         Large (0x2)
 // CHECK-NEXT:       ]
-// CHECK-NEXT:       EpilogOffset: +0x6
+// CHECK-NEXT:       EpilogOffset: -0xE1
 // CHECK-NEXT:       NumberOfOps: 2
 // CHECK-NEXT:       FirstOp: 0x0
 // CHECK-NEXT:       IpOffsetOfLastInstruction: 0xE0

>From 7f69650574cee7e9b0194af42978a2fb1e8013a7 Mon Sep 17 00:00:00 2001
From: Daniel Paoliello <danpao at microsoft.com>
Date: Wed, 10 Jun 2026 16:08:15 -0700
Subject: [PATCH 2/2] Fix tests

---
 llvm/test/MC/COFF/seh-unwindv3-inheritance.s | 18 +++++++++++-------
 llvm/test/MC/COFF/seh-unwindv3-nonmirror.s   | 11 ++++++-----
 2 files changed, 17 insertions(+), 12 deletions(-)

diff --git a/llvm/test/MC/COFF/seh-unwindv3-inheritance.s b/llvm/test/MC/COFF/seh-unwindv3-inheritance.s
index 4c768e275f3ce..9d47cea5932c4 100644
--- a/llvm/test/MC/COFF/seh-unwindv3-inheritance.s
+++ b/llvm/test/MC/COFF/seh-unwindv3-inheritance.s
@@ -83,13 +83,16 @@ different_numops:
 // CHECK-LABEL:  StartAddress: different_numops
 // CHECK:        NumberOfOps: 2
 // CHECK:        NumberOfEpilogs: 2
+// V3 emits epilog descriptors in descending address order (tail-relative
+// offsets), so the later partial (1-op) epilog is Epilog [0].
 // CHECK:        Epilog [0] {
-// CHECK:          NumberOfOps: 2
+// CHECK:          NumberOfOps: 1
 // CHECK:          FirstOp: 0x0
+// CHECK:          ALLOC_SMALL Size=0x20
 // CHECK:        }
-// Epilog 1: different NumberOfOps ΓÇö gets its own full descriptor, not inherited
+// Epilog 1: different NumberOfOps — gets its own full descriptor, not inherited
 // CHECK:        Epilog [1] {
-// CHECK:          NumberOfOps: 1
+// CHECK:          NumberOfOps: 2
 // CHECK:          FirstOp: 0x0
 // CHECK:          ALLOC_SMALL Size=0x20
 
@@ -125,13 +128,14 @@ different_wods:
 // CHECK-LABEL:  StartAddress: different_wods
 // CHECK:        NumberOfOps: 3
 // CHECK:        NumberOfEpilogs: 2
-// Epilog 0: PUSH RDI, found in prolog pool
+// Descriptors are in descending address order, so the later epilog
+// (PUSH RBX) is Epilog [0].
 // CHECK:        Epilog [0] {
 // CHECK:          NumberOfOps: 1
-// CHECK:          PUSH Reg=RDI
+// CHECK:          PUSH Reg=RBX
 // CHECK:        }
-// Epilog 1: PUSH RBX ΓÇö different FirstOp, so NOT inherited
+// Epilog 1: PUSH RDI — different FirstOp, so NOT inherited
 // CHECK:        Epilog [1] {
 // CHECK:          NumberOfOps: 1
-// CHECK:          PUSH Reg=RBX
+// CHECK:          PUSH Reg=RDI
 // CHECK:       ]
diff --git a/llvm/test/MC/COFF/seh-unwindv3-nonmirror.s b/llvm/test/MC/COFF/seh-unwindv3-nonmirror.s
index 9c9d59e7e4e6d..568539b1a9539 100644
--- a/llvm/test/MC/COFF/seh-unwindv3-nonmirror.s
+++ b/llvm/test/MC/COFF/seh-unwindv3-nonmirror.s
@@ -143,19 +143,20 @@ mixed_epilogs:
 // CHECK:            ALLOC_SMALL Size=0x20
 // CHECK:            PUSH Reg=RDI
 // CHECK:            PUSH Reg=RBX
-// Mirror epilog (FirstOp=0, NumberOfOps=3)
+// V3 emits epilog descriptors in descending address order (tail-relative
+// offsets), so the later partial epilog (2 ops) is Epilog [0].
 // CHECK:          Epilog [0] {
-// CHECK:            NumberOfOps: 3
+// CHECK:            NumberOfOps: 2
 // CHECK:            FirstOp: 0x0
 // CHECK:            ALLOC_SMALL Size=0x20
 // CHECK:            PUSH Reg=RDI
-// CHECK:            PUSH Reg=RBX
-// Partial epilog — different NumberOfOps, so NOT inherited.
+// Mirror epilog (FirstOp=0, NumberOfOps=3).
 // CHECK:          Epilog [1] {
-// CHECK:            NumberOfOps: 2
+// CHECK:            NumberOfOps: 3
 // CHECK:            FirstOp: 0x0
 // CHECK:            ALLOC_SMALL Size=0x20
 // CHECK:            PUSH Reg=RDI
+// CHECK:            PUSH Reg=RBX
 
 // --- Test 4: reordered epilog ---
 // Prolog: push rbx, push rdi, sub rsp, 32



More information about the llvm-commits mailing list