[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
Fri Jun 12 14:51:58 PDT 2026
https://github.com/dpaoliello updated https://github.com/llvm/llvm-project/pull/203108
>From e2630a6e9b847801dc461ae9d03a7fdbb9d5dc18 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 1e23069c5da71..be940df606e81 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 ---
// Per the V3 spec, an epilog descriptor with NumberOfOps == 0 inherits its
// effective NumberOfOps, FirstOp, IpOffsetOfLastInstruction, and IP offset
@@ -738,24 +790,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);
@@ -1047,6 +1094,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 c6e81feb9165344b2f5a1d89cefe3d32887f25e5 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 | 16 ++++++++++------
llvm/test/MC/COFF/seh-unwindv3-nonmirror.s | 11 ++++++-----
2 files changed, 16 insertions(+), 11 deletions(-)
diff --git a/llvm/test/MC/COFF/seh-unwindv3-inheritance.s b/llvm/test/MC/COFF/seh-unwindv3-inheritance.s
index 1611fb2d95a3f..3e0f06c8f762e 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
// CHECK: Epilog [1] {
-// CHECK: NumberOfOps: 1
+// CHECK: NumberOfOps: 2
// CHECK: FirstOp: 0x0
// CHECK: ALLOC_SMALL Size=0x20
@@ -125,15 +128,16 @@ 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: }
// --- Test 4: two identical LARGE mirror epilogs -> second inherits ---
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