[llvm] [llvm-ml] Add MASM unwind v3 support for x64 exception handling and improve MSVC compat (PR #202809)

Daniel Paoliello via llvm-commits llvm-commits at lists.llvm.org
Thu Jun 11 09:17:26 PDT 2026


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

>From b8e704fa8148665d356b37cad09c5efb04c4da5a Mon Sep 17 00:00:00 2001
From: "Daniel Paoliello (HE HIM)" <danpao at microsoft.com>
Date: Wed, 25 Mar 2026 19:08:32 +0000
Subject: [PATCH 1/3] Merged PR 15128428: [llvm-ml] Add MASM unwind v3 support
 for x64 exception handling and improve MSVC compat

New command-line options:
- `/unwindv3`: Enable V3 unwind information format
- `--mattr`: Enable target-specific CPU features (e.g. `--mattr=+push2pop2`)

New MASM directives:
- `.push2reg` / `.pop2reg`: Push/pop register pairs (PUSH2/POP2)
- `.beginepilog` / `.endepilog`: Delimit epilog unwind regions
- `.popreg`, `.freestack`, `.restorereg`, `.restorexmm128`, `.unsetframe`:
  Epilog counterparts of existing prolog directives
- `.pushframe code`: MASM syntax for interrupt handlers with error codes

New built-in symbol:
- `@UnwindVersion`: Returns the current x64 unwind version being used.

Error diagnostics:
- Prolog directives after `.endprolog` are diagnosed
- Epilog directives outside `.beginepilog`/`.endepilog` are diagnosed
- Nested `.beginepilog` is diagnosed
---
 llvm/docs/ReleaseNotes.md                     |  20 ++
 llvm/include/llvm/MC/MCStreamer.h             |  10 +
 llvm/lib/MC/MCParser/COFFMasmParser.cpp       |  83 ++++++
 llvm/lib/MC/MCParser/MasmParser.cpp           |  12 +
 .../lib/Target/X86/AsmParser/X86AsmParser.cpp |  86 +++++-
 llvm/test/tools/llvm-ml/beginepilog_error.asm |  19 ++
 llvm/test/tools/llvm-ml/builtin_symbols.asm   |   6 +
 .../tools/llvm-ml/epilog_directive_errors.asm |  42 +++
 llvm/test/tools/llvm-ml/epilog_directives.asm | 278 ++++++++++++++++++
 llvm/test/tools/llvm-ml/proc_frame_v3.asm     | 177 +++++++++++
 .../tools/llvm-ml/prolog_directive_errors.asm |  45 +++
 llvm/tools/llvm-ml/Opts.td                    |   5 +
 llvm/tools/llvm-ml/llvm-ml.cpp                |  25 +-
 13 files changed, 794 insertions(+), 14 deletions(-)
 create mode 100644 llvm/test/tools/llvm-ml/beginepilog_error.asm
 create mode 100644 llvm/test/tools/llvm-ml/epilog_directive_errors.asm
 create mode 100644 llvm/test/tools/llvm-ml/epilog_directives.asm
 create mode 100644 llvm/test/tools/llvm-ml/proc_frame_v3.asm
 create mode 100644 llvm/test/tools/llvm-ml/prolog_directive_errors.asm

diff --git a/llvm/docs/ReleaseNotes.md b/llvm/docs/ReleaseNotes.md
index bdfbfc3f70533..ba042577c84ac 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -321,6 +321,26 @@ Makes programs 10x faster by doing Special New Thing.
 
 ### Changes to the LLVM tools
 
+* llvm-ml now supports the `--mattr` flag for enabling target-specific CPU
+  features (e.g. `--mattr=+push2pop2`).
+* llvm-ml now supports the `/unwindv3` flag to enable V3 unwind information
+  format for x64 exception handling.
+* llvm-ml now supports the `@UnwindVersion` built-in symbol, which returns the
+  current unwind version (1 by default, 3 when `/unwindv3` is specified).
+* llvm-ml now supports the `.push2reg`, `.pop2reg`, `.beginepilog`, and
+  `.endepilog` MASM directives for V3 unwind information.
+* llvm-ml now supports the `.popreg`, `.freestack`, `.restorereg`,
+  `.restorexmm128`, and `.unsetframe` MASM epilog directives. These are the
+  epilog counterparts of `.pushreg`, `.allocstack`, `.savereg`,
+  `.savexmm128`, and `.setframe` respectively, and are valid only inside
+  `.beginepilog`/`.endepilog` blocks.
+* llvm-ml now supports `.pushframe code` syntax (without the `@` prefix)
+  for interrupt handlers with error codes.
+* llvm-ml now diagnoses:
+  - Prolog directives (including `.allocstack`) used outside of prologs (after `.endprolog`).
+  - Epilog directives used outside of epilogs (outside of `.beginepilog` + `.endepilog` blocks).
+  - Beginning an epilog (`.beginepilog`) inside another epilog.
+
 * `llvm-profgen` now supports ETM trace decoding using the OpenCSD library for Cortex-M targets. OpenCSD version 1.5.4 or higher is required.
 
 * `llvm-objcopy` no longer corrupts the symbol table when `--update-section` is called for ELF files.
diff --git a/llvm/include/llvm/MC/MCStreamer.h b/llvm/include/llvm/MC/MCStreamer.h
index 750e3269cc0e3..955a48705f15c 100644
--- a/llvm/include/llvm/MC/MCStreamer.h
+++ b/llvm/include/llvm/MC/MCStreamer.h
@@ -366,6 +366,13 @@ class LLVM_ABI MCStreamer {
 
   bool isInEpilogCFI() const { return CurrentWinEpilog; }
 
+  /// Returns true if a WinCFI prolog has been completed (.seh_endprologue)
+  /// in the current frame.
+  bool isWinCFIPrologEnded() const {
+    return CurrentWinFrameInfo && !CurrentWinFrameInfo->End &&
+           CurrentWinFrameInfo->PrologEnd;
+  }
+
   /// \name Assembly File Formatting.
   /// @{
 
@@ -1088,6 +1095,9 @@ class LLVM_ABI MCStreamer {
   void setDefaultWinCFIUnwindVersion(uint8_t V) {
     DefaultWinCFIUnwindVersion = V;
   }
+  uint8_t getDefaultWinCFIUnwindVersion() const {
+    return DefaultWinCFIUnwindVersion;
+  }
   virtual void emitWinEHHandler(const MCSymbol *Sym, bool Unwind, bool Except,
                                 SMLoc Loc = SMLoc());
   virtual void emitWinEHHandlerData(SMLoc Loc = SMLoc());
diff --git a/llvm/lib/MC/MCParser/COFFMasmParser.cpp b/llvm/lib/MC/MCParser/COFFMasmParser.cpp
index db796a3ad2ed8..7ba3aa0256190 100644
--- a/llvm/lib/MC/MCParser/COFFMasmParser.cpp
+++ b/llvm/lib/MC/MCParser/COFFMasmParser.cpp
@@ -49,7 +49,17 @@ class COFFMasmParser : public MCAsmParserExtension {
   bool parseDirectiveAlias(StringRef, SMLoc);
 
   bool parseSEHDirectiveAllocStack(StringRef, SMLoc);
+  bool parseSEHDirectiveFreeStack(StringRef, SMLoc);
   bool parseSEHDirectiveEndProlog(StringRef, SMLoc);
+  bool ParseSEHDirectiveBeginEpilog(StringRef, SMLoc);
+  bool ParseSEHDirectiveEndEpilog(StringRef, SMLoc);
+
+  /// Check that we are inside a PROC FRAME.
+  bool ensureInsideFrame(SMLoc Loc, StringRef Directive);
+  /// Check that we are in the prolog (before .endprolog).
+  bool ensureInProlog(SMLoc Loc, StringRef Directive);
+  /// Check that we are inside a .beginepilog/.endepilog block.
+  bool ensureInEpilog(SMLoc Loc, StringRef Directive);
 
   bool IgnoreDirective(StringRef, SMLoc) {
     while (!getLexer().is(AsmToken::EndOfStatement)) {
@@ -65,8 +75,14 @@ class COFFMasmParser : public MCAsmParserExtension {
     // x64 directives
     addDirectiveHandler<&COFFMasmParser::parseSEHDirectiveAllocStack>(
         ".allocstack");
+    addDirectiveHandler<&COFFMasmParser::parseSEHDirectiveFreeStack>(
+        ".freestack");
     addDirectiveHandler<&COFFMasmParser::parseSEHDirectiveEndProlog>(
         ".endprolog");
+    addDirectiveHandler<&COFFMasmParser::ParseSEHDirectiveBeginEpilog>(
+        ".beginepilog");
+    addDirectiveHandler<&COFFMasmParser::ParseSEHDirectiveEndEpilog>(
+        ".endepilog");
 
     // Code label directives
     // label
@@ -517,12 +533,58 @@ bool COFFMasmParser::parseDirectiveAlias(StringRef Directive, SMLoc Loc) {
   return false;
 }
 
+bool COFFMasmParser::ensureInsideFrame(SMLoc Loc, StringRef Directive) {
+  if (CurrentProceduresFramed.empty() || !CurrentProceduresFramed.back()) {
+    return Error(Loc,
+                 "Missing Frame in proc, no unwind code will be generated.");
+  }
+  return false;
+}
+
+bool COFFMasmParser::ensureInProlog(SMLoc Loc, StringRef Directive) {
+  if (ensureInsideFrame(Loc, Directive))
+    return true;
+  if (getStreamer().isWinCFIPrologEnded()) {
+    return Error(Loc, "prolog directive must be used inside a prolog");
+  }
+  return false;
+}
+
+bool COFFMasmParser::ensureInEpilog(SMLoc Loc, StringRef Directive) {
+  if (ensureInsideFrame(Loc, Directive))
+    return true;
+  if (!getStreamer().isInEpilogCFI()) {
+    return Error(Loc, "epilog directive must be used inside an epilog");
+  }
+  return false;
+}
+
 bool COFFMasmParser::parseSEHDirectiveAllocStack(StringRef Directive,
                                                  SMLoc Loc) {
+  if (ensureInProlog(Loc, Directive))
+    return true;
+  int64_t Size;
+  SMLoc SizeLoc = getTok().getLoc();
+  if (getParser().parseAbsoluteExpression(Size))
+    return Error(SizeLoc, "expected integer size");
+  if (Size < 0)
+    return Error(SizeLoc, "stack size must be non-negative");
+  if (Size % 8 != 0)
+    return Error(SizeLoc, "stack size must be a multiple of 8");
+  getStreamer().emitWinCFIAllocStack(static_cast<unsigned>(Size), Loc);
+  return false;
+}
+
+bool COFFMasmParser::parseSEHDirectiveFreeStack(StringRef Directive,
+                                                SMLoc Loc) {
+  if (ensureInEpilog(Loc, Directive))
+    return true;
   int64_t Size;
   SMLoc SizeLoc = getTok().getLoc();
   if (getParser().parseAbsoluteExpression(Size))
     return Error(SizeLoc, "expected integer size");
+  if (Size < 0)
+    return Error(SizeLoc, "stack size must be non-negative");
   if (Size % 8 != 0)
     return Error(SizeLoc, "stack size must be a multiple of 8");
   getStreamer().emitWinCFIAllocStack(static_cast<unsigned>(Size), Loc);
@@ -531,10 +593,31 @@ bool COFFMasmParser::parseSEHDirectiveAllocStack(StringRef Directive,
 
 bool COFFMasmParser::parseSEHDirectiveEndProlog(StringRef Directive,
                                                 SMLoc Loc) {
+  if (ensureInsideFrame(Loc, Directive))
+    return true;
   getStreamer().emitWinCFIEndProlog(Loc);
   return false;
 }
 
+bool COFFMasmParser::ParseSEHDirectiveBeginEpilog(StringRef Directive,
+                                                  SMLoc Loc) {
+  if (ensureInsideFrame(Loc, Directive))
+    return true;
+  if (getStreamer().isInEpilogCFI()) {
+    return Error(Loc, ".beginepilog must come after .endprolog or .endepilog");
+  }
+  getStreamer().emitWinCFIBeginEpilogue(Loc);
+  return false;
+}
+
+bool COFFMasmParser::ParseSEHDirectiveEndEpilog(StringRef Directive,
+                                                SMLoc Loc) {
+  if (ensureInsideFrame(Loc, Directive))
+    return true;
+  getStreamer().emitWinCFIEndEpilogue(Loc);
+  return false;
+}
+
 MCAsmParserExtension *llvm::createCOFFMasmParser() {
   return new COFFMasmParser;
 }
diff --git a/llvm/lib/MC/MCParser/MasmParser.cpp b/llvm/lib/MC/MCParser/MasmParser.cpp
index 6ec95ef697b29..14443a63dda63 100644
--- a/llvm/lib/MC/MCParser/MasmParser.cpp
+++ b/llvm/lib/MC/MCParser/MasmParser.cpp
@@ -717,6 +717,7 @@ class MasmParser : public MCAsmParser {
     DK_END,
     DK_PUSHFRAME,
     DK_PUSHREG,
+    DK_PUSH2REGS,
     DK_SAVEREG,
     DK_SAVEXMM128,
     DK_SETFRAME,
@@ -749,6 +750,7 @@ class MasmParser : public MCAsmParser {
     BI_DATASIZE,
     BI_MODEL,
     BI_STACK,
+    BI_UNWINDVERSION,
   };
 
   /// Maps builtin name --> BuiltinSymbol enum, for builtins handled by this
@@ -5306,9 +5308,15 @@ void MasmParser::initializeDirectiveKindMap() {
   DirectiveKindMap[".errnz"] = DK_ERRNZ;
   DirectiveKindMap[".pushframe"] = DK_PUSHFRAME;
   DirectiveKindMap[".pushreg"] = DK_PUSHREG;
+  DirectiveKindMap[".push2reg"] = DK_PUSH2REGS;
+  DirectiveKindMap[".pop2reg"] = DK_PUSH2REGS;
+  DirectiveKindMap[".popreg"] = DK_PUSHREG;
   DirectiveKindMap[".savereg"] = DK_SAVEREG;
+  DirectiveKindMap[".restorereg"] = DK_SAVEREG;
   DirectiveKindMap[".savexmm128"] = DK_SAVEXMM128;
+  DirectiveKindMap[".restorexmm128"] = DK_SAVEXMM128;
   DirectiveKindMap[".setframe"] = DK_SETFRAME;
+  DirectiveKindMap[".unsetframe"] = DK_SETFRAME;
   DirectiveKindMap[".radix"] = DK_RADIX;
   DirectiveKindMap["db"] = DK_DB;
   DirectiveKindMap["dd"] = DK_DD;
@@ -6139,6 +6147,7 @@ void MasmParser::initializeBuiltinSymbolMaps() {
   // Numeric built-ins (supported in all versions)
   BuiltinSymbolMap["@version"] = BI_VERSION;
   BuiltinSymbolMap["@line"] = BI_LINE;
+  BuiltinSymbolMap["@unwindversion"] = BI_UNWINDVERSION;
 
   // Text built-ins (supported in all versions)
   BuiltinSymbolMap["@date"] = BI_DATE;
@@ -6186,6 +6195,9 @@ const MCExpr *MasmParser::evaluateBuiltinValue(BuiltinSymbol Symbol,
                                    ActiveMacros.front()->ExitBuffer);
     return MCConstantExpr::create(Line, getContext());
   }
+  case BI_UNWINDVERSION:
+    return MCConstantExpr::create(getStreamer().getDefaultWinCFIUnwindVersion(),
+                                  getContext());
   }
   llvm_unreachable("unhandled built-in symbol");
 }
diff --git a/llvm/lib/Target/X86/AsmParser/X86AsmParser.cpp b/llvm/lib/Target/X86/AsmParser/X86AsmParser.cpp
index 58c6dd2b709a1..ab5c3440901c2 100644
--- a/llvm/lib/Target/X86/AsmParser/X86AsmParser.cpp
+++ b/llvm/lib/Target/X86/AsmParser/X86AsmParser.cpp
@@ -1232,12 +1232,15 @@ class X86AsmParser : public MCTargetAsmParser {
   /// SEH directives.
   bool parseSEHRegisterNumber(unsigned RegClassID, MCRegister &RegNo);
   bool parseDirectiveSEHPushReg(SMLoc);
-  bool parseDirectiveSEHPush2Regs(SMLoc);
+  bool parseDirectiveSEHPush2Regs(SMLoc, bool SwapRegs = false);
   bool parseDirectiveSEHSetFrame(SMLoc);
   bool parseDirectiveSEHSaveReg(SMLoc);
   bool parseDirectiveSEHSaveXMM(SMLoc);
   bool parseDirectiveSEHPushFrame(SMLoc);
 
+  bool ensureMasmEpilogContext(SMLoc Loc, StringRef Directive);
+  bool ensureMasmPrologContext(SMLoc Loc, StringRef Directive);
+
   unsigned checkTargetMatchPredicate(MCInst &Inst) override;
 
   bool validateInstruction(MCInst &Inst, const OperandVector &Ops);
@@ -4837,23 +4840,60 @@ bool X86AsmParser::ParseDirective(AsmToken DirectiveID) {
     return parseDirectiveFPOEndPrologue(DirectiveID.getLoc());
   else if (IDVal == ".cv_fpo_endproc")
     return parseDirectiveFPOEndProc(DirectiveID.getLoc());
-  else if (IDVal == ".seh_pushreg" ||
-           (Parser.isParsingMasm() && IDVal.equals_insensitive(".pushreg")))
+  else if (IDVal == ".seh_pushreg")
     return parseDirectiveSEHPushReg(DirectiveID.getLoc());
   else if (IDVal == ".seh_push2regs")
     return parseDirectiveSEHPush2Regs(DirectiveID.getLoc());
-  else if (IDVal == ".seh_setframe" ||
-           (Parser.isParsingMasm() && IDVal.equals_insensitive(".setframe")))
+  else if (IDVal == ".seh_setframe")
     return parseDirectiveSEHSetFrame(DirectiveID.getLoc());
-  else if (IDVal == ".seh_savereg" ||
-           (Parser.isParsingMasm() && IDVal.equals_insensitive(".savereg")))
+  else if (IDVal == ".seh_savereg")
     return parseDirectiveSEHSaveReg(DirectiveID.getLoc());
-  else if (IDVal == ".seh_savexmm" ||
-           (Parser.isParsingMasm() && IDVal.equals_insensitive(".savexmm128")))
+  else if (IDVal == ".seh_savexmm")
     return parseDirectiveSEHSaveXMM(DirectiveID.getLoc());
-  else if (IDVal == ".seh_pushframe" ||
-           (Parser.isParsingMasm() && IDVal.equals_insensitive(".pushframe")))
+  else if (IDVal == ".seh_pushframe")
     return parseDirectiveSEHPushFrame(DirectiveID.getLoc());
+  else if (Parser.isParsingMasm()) {
+    // MASM prolog directives.
+    if (IDVal.equals_insensitive(".pushreg")) {
+      return ensureMasmPrologContext(DirectiveID.getLoc(), IDVal) ||
+             parseDirectiveSEHPushReg(DirectiveID.getLoc());
+    } else if (IDVal.equals_insensitive(".push2reg")) {
+      return ensureMasmPrologContext(DirectiveID.getLoc(), IDVal) ||
+             parseDirectiveSEHPush2Regs(DirectiveID.getLoc());
+    } else if (IDVal.equals_insensitive(".setframe")) {
+      return ensureMasmPrologContext(DirectiveID.getLoc(), IDVal) ||
+             parseDirectiveSEHSetFrame(DirectiveID.getLoc());
+    } else if (IDVal.equals_insensitive(".savereg")) {
+      return ensureMasmPrologContext(DirectiveID.getLoc(), IDVal) ||
+             parseDirectiveSEHSaveReg(DirectiveID.getLoc());
+    } else if (IDVal.equals_insensitive(".savexmm128")) {
+      return ensureMasmPrologContext(DirectiveID.getLoc(), IDVal) ||
+             parseDirectiveSEHSaveXMM(DirectiveID.getLoc());
+    } else if (IDVal.equals_insensitive(".pushframe")) {
+      return ensureMasmPrologContext(DirectiveID.getLoc(), IDVal) ||
+             parseDirectiveSEHPushFrame(DirectiveID.getLoc());
+    }
+    // MASM epilog directives
+    if (IDVal.equals_insensitive(".popreg")) {
+      return ensureMasmEpilogContext(DirectiveID.getLoc(), IDVal) ||
+             parseDirectiveSEHPushReg(DirectiveID.getLoc());
+    } else if (IDVal.equals_insensitive(".pop2reg")) {
+      // .pop2reg args are in the order they are popped, so reverse them to get
+      // the order they were pushed.
+      return ensureMasmEpilogContext(DirectiveID.getLoc(), IDVal) ||
+             parseDirectiveSEHPush2Regs(DirectiveID.getLoc(),
+                                        /*SwapRegs=*/true);
+    } else if (IDVal.equals_insensitive(".unsetframe")) {
+      return ensureMasmEpilogContext(DirectiveID.getLoc(), IDVal) ||
+             parseDirectiveSEHSetFrame(DirectiveID.getLoc());
+    } else if (IDVal.equals_insensitive(".restorereg")) {
+      return ensureMasmEpilogContext(DirectiveID.getLoc(), IDVal) ||
+             parseDirectiveSEHSaveReg(DirectiveID.getLoc());
+    } else if (IDVal.equals_insensitive(".restorexmm128")) {
+      return ensureMasmEpilogContext(DirectiveID.getLoc(), IDVal) ||
+             parseDirectiveSEHSaveXMM(DirectiveID.getLoc());
+    }
+  }
 
   return true;
 }
@@ -5078,7 +5118,7 @@ bool X86AsmParser::parseDirectiveSEHPushReg(SMLoc Loc) {
   return false;
 }
 
-bool X86AsmParser::parseDirectiveSEHPush2Regs(SMLoc Loc) {
+bool X86AsmParser::parseDirectiveSEHPush2Regs(SMLoc Loc, bool SwapRegs) {
   MCRegister Reg1;
   if (parseSEHRegisterNumber(X86::GR64RegClassID, Reg1))
     return true;
@@ -5095,6 +5135,9 @@ bool X86AsmParser::parseDirectiveSEHPush2Regs(SMLoc Loc) {
     return TokError("expected end of directive");
 
   getParser().Lex();
+  // Swap regs to go from pop order to push order.
+  if (SwapRegs)
+    std::swap(Reg1, Reg2);
   getStreamer().emitWinCFIPush2Regs(Reg1, Reg2, Loc);
   return false;
 }
@@ -5159,6 +5202,20 @@ bool X86AsmParser::parseDirectiveSEHSaveXMM(SMLoc Loc) {
   return false;
 }
 
+bool X86AsmParser::ensureMasmPrologContext(SMLoc Loc, StringRef Directive) {
+  if (getStreamer().isWinCFIPrologEnded()) {
+    return Error(Loc, "prolog directive must be used inside a prolog");
+  }
+  return false;
+}
+
+bool X86AsmParser::ensureMasmEpilogContext(SMLoc Loc, StringRef Directive) {
+  if (!getStreamer().isInEpilogCFI()) {
+    return Error(Loc, "epilog directive must be used inside an epilog");
+  }
+  return false;
+}
+
 bool X86AsmParser::parseDirectiveSEHPushFrame(SMLoc Loc) {
   bool Code = false;
   StringRef CodeID;
@@ -5170,6 +5227,11 @@ bool X86AsmParser::parseDirectiveSEHPushFrame(SMLoc Loc) {
         return Error(startLoc, "expected @code");
       Code = true;
     }
+  } else if (getParser().isParsingMasm() &&
+             getLexer().is(AsmToken::Identifier) &&
+             getTok().getString().equals_insensitive("code")) {
+    getParser().Lex();
+    Code = true;
   }
 
   if (getLexer().isNot(AsmToken::EndOfStatement))
diff --git a/llvm/test/tools/llvm-ml/beginepilog_error.asm b/llvm/test/tools/llvm-ml/beginepilog_error.asm
new file mode 100644
index 0000000000000..c6bd86bd960e0
--- /dev/null
+++ b/llvm/test/tools/llvm-ml/beginepilog_error.asm
@@ -0,0 +1,19 @@
+; RUN: not llvm-ml64 -filetype=s /unwindv3 %s /Fo - 2>&1 | FileCheck %s
+
+.code
+
+; Test A2265: .beginepilog inside .beginepilog (before .endepilog)
+t1 PROC FRAME
+  push r10
+  .pushreg r10
+  .endprolog
+  .beginepilog
+  .popreg r10
+  pop r10
+; CHECK: .beginepilog must come after .endprolog or .endepilog
+  .beginepilog
+  .endepilog
+  ret
+t1 ENDP
+
+END
diff --git a/llvm/test/tools/llvm-ml/builtin_symbols.asm b/llvm/test/tools/llvm-ml/builtin_symbols.asm
index 81442c49d7ec7..e06f5a21efbbe 100644
--- a/llvm/test/tools/llvm-ml/builtin_symbols.asm
+++ b/llvm/test/tools/llvm-ml/builtin_symbols.asm
@@ -56,4 +56,10 @@ ECHO t6:
 ; CHECK-FIXEDTIME: Time = 00:00:00
 ; CHECK-NOT: {{[[:digit:]]}}
 
+ECHO t7:
+unwind_val TEXTEQU %@UnwindVersion
+%ECHO @UnwindVersion = unwind_val
+; CHECK-LABEL: t7:
+; CHECK-NEXT: @UnwindVersion = 1
+
 end
diff --git a/llvm/test/tools/llvm-ml/epilog_directive_errors.asm b/llvm/test/tools/llvm-ml/epilog_directive_errors.asm
new file mode 100644
index 0000000000000..3ef71e25b534e
--- /dev/null
+++ b/llvm/test/tools/llvm-ml/epilog_directive_errors.asm
@@ -0,0 +1,42 @@
+; RUN: not llvm-ml64 -filetype=s /unwindv3 %s /Fo - 2>&1 | FileCheck %s
+
+.code
+
+; Test A2255: epilog directives used outside .beginepilog/.endepilog
+t1 PROC FRAME
+  push r12
+  .pushreg r12
+  sub rsp, 64
+  .allocstack 64
+  lea rbp, [rsp]
+  .setframe rbp, 0
+  movaps [rsp+16], xmm6
+  .savexmm128 xmm6, 16
+  mov [rsp+32], rbx
+  .savereg rbx, 32
+  .endprolog
+
+  mov rax, 0
+
+; CHECK: epilog directive must be used inside an epilog
+  .popreg r12
+
+; CHECK: epilog directive must be used inside an epilog
+  .pop2reg r12, r13
+
+; CHECK: epilog directive must be used inside an epilog
+  .freestack 64
+
+; CHECK: epilog directive must be used inside an epilog
+  .restorereg rbx, 32
+
+; CHECK: epilog directive must be used inside an epilog
+  .restorexmm128 xmm6, 16
+
+; CHECK: epilog directive must be used inside an epilog
+  .unsetframe rbp, 0
+
+  ret
+t1 ENDP
+
+END
diff --git a/llvm/test/tools/llvm-ml/epilog_directives.asm b/llvm/test/tools/llvm-ml/epilog_directives.asm
new file mode 100644
index 0000000000000..1f465a23a199a
--- /dev/null
+++ b/llvm/test/tools/llvm-ml/epilog_directives.asm
@@ -0,0 +1,278 @@
+; RUN: llvm-ml64 -filetype=s /unwindv3 %s /Fo - | FileCheck %s
+
+.code
+
+; Test .pushreg / .popreg
+t1 PROC FRAME
+  push r12
+  .pushreg r12
+  push r13
+  .pushreg r13
+  push rsi
+  .pushreg rsi
+  push rdi
+  .pushreg rdi
+  .endprolog
+  mov rax, 0
+  .beginepilog
+  .popreg rdi
+  pop rdi
+  .popreg rsi
+  pop rsi
+  .popreg r13
+  pop r13
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+t1 ENDP
+
+; CHECK: .seh_proc t1
+; CHECK: push r12
+; CHECK: .seh_pushreg r12
+; CHECK: push r13
+; CHECK: .seh_pushreg r13
+; CHECK: push rsi
+; CHECK: .seh_pushreg rsi
+; CHECK: push rdi
+; CHECK: .seh_pushreg rdi
+; CHECK: .seh_endprologue
+; CHECK: .seh_startepilogue
+; CHECK: .seh_pushreg rdi
+; CHECK: pop rdi
+; CHECK: .seh_pushreg rsi
+; CHECK: pop rsi
+; CHECK: .seh_pushreg r13
+; CHECK: pop r13
+; CHECK: .seh_pushreg r12
+; CHECK: pop r12
+; CHECK: .seh_endepilogue
+; CHECK: .seh_endproc
+
+; Test .allocstack / .freestack
+t2 PROC FRAME
+  push r12
+  .pushreg r12
+  sub rsp, 48
+  .allocstack 48
+  .endprolog
+  mov rax, 0
+  .beginepilog
+  .freestack 48
+  add rsp, 48
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+t2 ENDP
+
+; CHECK: .seh_proc t2
+; CHECK: .seh_pushreg r12
+; CHECK: .seh_stackalloc 48
+; CHECK: .seh_endprologue
+; CHECK: .seh_startepilogue
+; CHECK: .seh_stackalloc 48
+; CHECK: .seh_pushreg r12
+; CHECK: .seh_endepilogue
+; CHECK: .seh_endproc
+
+; Test .savereg / .restorereg
+t3 PROC FRAME
+  push r12
+  .pushreg r12
+  sub rsp, 48
+  .allocstack 48
+  mov [rsp], rbx
+  .savereg rbx, 0
+  mov [rsp+8], rsi
+  .savereg rsi, 8
+  .endprolog
+  mov rax, 0
+  .beginepilog
+  .restorereg rsi, 8
+  mov rsi, [rsp+8]
+  .restorereg rbx, 0
+  mov rbx, [rsp]
+  .freestack 48
+  add rsp, 48
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+t3 ENDP
+
+; CHECK: .seh_proc t3
+; CHECK: .seh_savereg rbx, 0
+; CHECK: .seh_savereg rsi, 8
+; CHECK: .seh_endprologue
+; CHECK: .seh_startepilogue
+; CHECK: .seh_savereg rsi, 8
+; CHECK: .seh_savereg rbx, 0
+; CHECK: .seh_stackalloc 48
+; CHECK: .seh_pushreg r12
+; CHECK: .seh_endepilogue
+; CHECK: .seh_endproc
+
+; Test .savexmm128 / .restorexmm128
+t4 PROC FRAME
+  push r12
+  .pushreg r12
+  sub rsp, 48
+  .allocstack 48
+  movaps [rsp], xmm6
+  .savexmm128 xmm6, 0
+  movaps [rsp+16], xmm7
+  .savexmm128 xmm7, 16
+  .endprolog
+  mov rax, 0
+  .beginepilog
+  .restorexmm128 xmm7, 16
+  movaps xmm7, [rsp+16]
+  .restorexmm128 xmm6, 0
+  movaps xmm6, [rsp]
+  .freestack 48
+  add rsp, 48
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+t4 ENDP
+
+; CHECK: .seh_proc t4
+; CHECK: .seh_savexmm xmm6, 0
+; CHECK: .seh_savexmm xmm7, 16
+; CHECK: .seh_endprologue
+; CHECK: .seh_startepilogue
+; CHECK: .seh_savexmm xmm7, 16
+; CHECK: .seh_savexmm xmm6, 0
+; CHECK: .seh_stackalloc 48
+; CHECK: .seh_pushreg r12
+; CHECK: .seh_endepilogue
+; CHECK: .seh_endproc
+
+; Test .setframe / .unsetframe
+t5 PROC FRAME
+  push r12
+  .pushreg r12
+  sub rsp, 32
+  .allocstack 32
+  lea rbp, [rsp+16]
+  .setframe rbp, 16
+  .endprolog
+  mov rax, 0
+  .beginepilog
+  .unsetframe rbp, 16
+  lea rsp, [rbp-16]
+  .freestack 32
+  add rsp, 32
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+t5 ENDP
+
+; CHECK: .seh_proc t5
+; CHECK: .seh_setframe rbp, 16
+; CHECK: .seh_endprologue
+; CHECK: .seh_startepilogue
+; CHECK: .seh_setframe rbp, 16
+; CHECK: .seh_stackalloc 32
+; CHECK: .seh_pushreg r12
+; CHECK: .seh_endepilogue
+; CHECK: .seh_endproc
+
+; Test .pushframe (interrupt handler)
+t6 PROC FRAME
+  .pushframe
+  push r12
+  .pushreg r12
+  sub rsp, 32
+  .allocstack 32
+  .endprolog
+  mov rax, 0
+  .beginepilog
+  .freestack 32
+  add rsp, 32
+  .popreg r12
+  pop r12
+  .endepilog
+  iretq
+t6 ENDP
+
+; CHECK: .seh_proc t6
+; CHECK: .seh_pushframe
+; CHECK: .seh_pushreg r12
+; CHECK: .seh_stackalloc 32
+; CHECK: .seh_endprologue
+; CHECK: .seh_startepilogue
+; CHECK: .seh_stackalloc 32
+; CHECK: .seh_pushreg r12
+; CHECK: .seh_endepilogue
+; CHECK: .seh_endproc
+
+; Test .pushframe code (interrupt handler with error code)
+t7 PROC FRAME
+  .pushframe code
+  push r12
+  .pushreg r12
+  sub rsp, 32
+  .allocstack 32
+  .endprolog
+  mov rax, 0
+  .beginepilog
+  .freestack 32
+  add rsp, 32
+  .popreg r12
+  pop r12
+  .endepilog
+  iretq
+t7 ENDP
+
+; CHECK: .seh_proc t7
+; CHECK: .seh_pushframe @code
+; CHECK: .seh_endproc
+
+; Test two epilogs
+t8 PROC FRAME
+  push r12
+  .pushreg r12
+  push rdi
+  .pushreg rdi
+  .endprolog
+  mov rax, 0
+  cmp rcx, 0
+  je epilog2
+  .beginepilog
+  .popreg rdi
+  pop rdi
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+epilog2:
+  .beginepilog
+  .popreg rdi
+  pop rdi
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+t8 ENDP
+
+; CHECK: .seh_proc t8
+; CHECK: .seh_pushreg r12
+; CHECK: .seh_pushreg rdi
+; CHECK: .seh_endprologue
+; CHECK: .seh_startepilogue
+; CHECK: .seh_pushreg rdi
+; CHECK: .seh_pushreg r12
+; CHECK: .seh_endepilogue
+; CHECK: ret
+; CHECK: .seh_startepilogue
+; CHECK: .seh_pushreg rdi
+; CHECK: .seh_pushreg r12
+; CHECK: .seh_endepilogue
+; CHECK: ret
+; CHECK: .seh_endproc
+
+END
diff --git a/llvm/test/tools/llvm-ml/proc_frame_v3.asm b/llvm/test/tools/llvm-ml/proc_frame_v3.asm
new file mode 100644
index 0000000000000..1e9c57cd39c3d
--- /dev/null
+++ b/llvm/test/tools/llvm-ml/proc_frame_v3.asm
@@ -0,0 +1,177 @@
+; RUN: llvm-ml64 -filetype=s --mattr=+push2pop2 /unwindv3 %s /Fo - | FileCheck %s
+
+.code
+
+; Verify @UnwindVersion reflects the /unwindv3 flag.
+if @UnwindVersion ne 3
+  .err <@UnwindVersion should be 3 when /unwindv3 is used>
+endif
+
+t1 PROC FRAME
+  push2 r12, r13
+  .push2reg r12, r13
+  sub rsp, 32
+  .allocstack 32
+  .endprolog
+  nop
+  .beginepilog
+  .freestack 32
+  add rsp, 32
+  .pop2reg r12, r13
+  pop2 r13, r12
+  .endepilog
+  ret
+t1 ENDP
+
+; CHECK: .seh_proc t1
+
+; CHECK: t1:
+; CHECK: push2 r12, r13
+; CHECK: .seh_push2regs r12, r13
+; CHECK: sub rsp, 32
+; CHECK: .seh_stackalloc 32
+; CHECK: .seh_endprologue
+; CHECK: nop
+; CHECK: .seh_startepilogue
+; CHECK: .seh_stackalloc 32
+; CHECK: add rsp, 32
+; CHECK: .seh_push2regs r13, r12
+; CHECK: pop2 r13, r12
+; CHECK: .seh_endepilogue
+; CHECK: ret
+; CHECK: .seh_endproc
+
+; Test .popreg
+t2 PROC FRAME
+  push r12
+  .pushreg r12
+  .endprolog
+  nop
+  .beginepilog
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+t2 ENDP
+
+; CHECK: .seh_proc t2
+; CHECK: .seh_pushreg r12
+; CHECK: .seh_endprologue
+; CHECK: .seh_startepilogue
+; CHECK: .seh_pushreg r12
+; CHECK: .seh_endepilogue
+; CHECK: .seh_endproc
+
+; Test .freestack
+t3 PROC FRAME
+  push r12
+  .pushreg r12
+  sub rsp, 32
+  .allocstack 32
+  .endprolog
+  nop
+  .beginepilog
+  .freestack 32
+  add rsp, 32
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+t3 ENDP
+
+; CHECK: .seh_proc t3
+; CHECK: .seh_stackalloc 32
+; CHECK: .seh_endprologue
+; CHECK: .seh_startepilogue
+; CHECK: .seh_stackalloc 32
+; CHECK: .seh_endepilogue
+; CHECK: .seh_endproc
+
+; Test .restorereg
+t4 PROC FRAME
+  push r12
+  .pushreg r12
+  sub rsp, 32
+  .allocstack 32
+  mov [rsp], rbx
+  .savereg rbx, 0
+  .endprolog
+  nop
+  .beginepilog
+  .restorereg rbx, 0
+  mov rbx, [rsp]
+  .freestack 32
+  add rsp, 32
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+t4 ENDP
+
+; CHECK: .seh_proc t4
+; CHECK: .seh_savereg rbx, 0
+; CHECK: .seh_endprologue
+; CHECK: .seh_startepilogue
+; CHECK: .seh_savereg rbx, 0
+; CHECK: .seh_endepilogue
+; CHECK: .seh_endproc
+
+; Test .restorexmm128
+t5 PROC FRAME
+  push r12
+  .pushreg r12
+  sub rsp, 32
+  .allocstack 32
+  movaps [rsp], xmm6
+  .savexmm128 xmm6, 0
+  .endprolog
+  nop
+  .beginepilog
+  .restorexmm128 xmm6, 0
+  movaps xmm6, [rsp]
+  .freestack 32
+  add rsp, 32
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+t5 ENDP
+
+; CHECK: .seh_proc t5
+; CHECK: .seh_savexmm xmm6, 0
+; CHECK: .seh_endprologue
+; CHECK: .seh_startepilogue
+; CHECK: .seh_savexmm xmm6, 0
+; CHECK: .seh_endepilogue
+; CHECK: .seh_endproc
+
+; Test .unsetframe
+t6 PROC FRAME
+  push r12
+  .pushreg r12
+  sub rsp, 32
+  .allocstack 32
+  lea rbp, [rsp+16]
+  .setframe rbp, 16
+  .endprolog
+  nop
+  .beginepilog
+  .unsetframe rbp, 16
+  lea rsp, [rbp-16]
+  .freestack 32
+  add rsp, 32
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+t6 ENDP
+
+; CHECK: .seh_proc t6
+; CHECK: .seh_setframe rbp, 16
+; CHECK: .seh_endprologue
+; CHECK: .seh_startepilogue
+; CHECK: .seh_setframe rbp, 16
+; CHECK: .seh_endepilogue
+; CHECK: .seh_endproc
+
+END
diff --git a/llvm/test/tools/llvm-ml/prolog_directive_errors.asm b/llvm/test/tools/llvm-ml/prolog_directive_errors.asm
new file mode 100644
index 0000000000000..dcdeaa94f7899
--- /dev/null
+++ b/llvm/test/tools/llvm-ml/prolog_directive_errors.asm
@@ -0,0 +1,45 @@
+; RUN: not llvm-ml64 -filetype=s /unwindv3 %s /Fo - 2>&1 | FileCheck %s
+
+.code
+
+; Test A2256: prolog directives used after .endprolog
+t1 PROC FRAME
+  push r12
+  .pushreg r12
+  sub rsp, 32
+  .allocstack 32
+  .endprolog
+
+  mov rax, 0
+
+; CHECK: prolog directive must be used inside a prolog
+  .pushreg r13
+
+; CHECK: prolog directive must be used inside a prolog
+  .push2reg r14, r15
+
+; CHECK: prolog directive must be used inside a prolog
+  .pushframe
+
+; CHECK: prolog directive must be used inside a prolog
+  .setframe rbp, 0
+
+; CHECK: prolog directive must be used inside a prolog
+  .allocstack 16
+
+; CHECK: prolog directive must be used inside a prolog
+  .savereg rbx, 0
+
+; CHECK: prolog directive must be used inside a prolog
+  .savexmm128 xmm6, 0
+
+  .beginepilog
+  .freestack 32
+  add rsp, 32
+  .popreg r12
+  pop r12
+  .endepilog
+  ret
+t1 ENDP
+
+END
diff --git a/llvm/tools/llvm-ml/Opts.td b/llvm/tools/llvm-ml/Opts.td
index 8b748bb64b278..121c8cfeef566 100644
--- a/llvm/tools/llvm-ml/Opts.td
+++ b/llvm/tools/llvm-ml/Opts.td
@@ -41,6 +41,8 @@ def fatal_warnings : LLVMFlag<"fatal-warnings">,
                      HelpText<"Treat warnings as errors">;
 def filetype : LLVMJoined<"filetype=">, Values<"obj,s,null">,
                HelpText<"Emit a file with the given type">;
+def mattr : LLVMCommaJoined<"mattr=">,
+            HelpText<"Target specific attributes (--mattr=help for details)">;
 def output_att_asm : LLVMFlag<"output-att-asm">,
                      HelpText<"Use ATT syntax for output assembly">;
 def show_encoding : LLVMFlag<"show-encoding">,
@@ -81,6 +83,9 @@ def safeseh : MLFlag<"safeseh">,
                        "exception handlers or containing exception handlers "
                        "that are all declared with .SAFESEH. Only available in "
                        "32-bit.">;
+def unwindv3 : MLFlag<"unwindv3">,
+               HelpText<"Use V3 unwind information format for x64 exception "
+                        "handling.">;
 def assembly_file : MLJoinedOrSeparate<"Ta">,
                     HelpText<"Assemble source file with the given name. Used "
                              "if the filename begins with a forward slash.">;
diff --git a/llvm/tools/llvm-ml/llvm-ml.cpp b/llvm/tools/llvm-ml/llvm-ml.cpp
index 6ecc1bf86ef50..b5670e6bf1de6 100644
--- a/llvm/tools/llvm-ml/llvm-ml.cpp
+++ b/llvm/tools/llvm-ml/llvm-ml.cpp
@@ -10,6 +10,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "llvm/ADT/StringExtras.h"
 #include "llvm/ADT/StringSwitch.h"
 #include "llvm/MC/MCAsmBackend.h"
 #include "llvm/MC/MCAsmInfo.h"
@@ -44,6 +45,7 @@
 #include "llvm/Support/VirtualFileSystem.h"
 #include "llvm/Support/WithColor.h"
 #include "llvm/TargetParser/Host.h"
+#include "llvm/TargetParser/SubtargetFeature.h"
 #include <ctime>
 #include <optional>
 
@@ -285,6 +287,13 @@ int llvm_ml_main(int Argc, char **Argv, const llvm::ToolContext &) {
     SafeSEH = false;
   }
 
+  bool UnwindV3 = InputArgs.hasArg(OPT_unwindv3);
+  if (UnwindV3 && !TheTriple.isArch64Bit()) {
+    WithColor::warning()
+        << "/unwindv3 applies only to 64-bit X86 platforms; ignoring.\n";
+    UnwindV3 = false;
+  }
+
   ErrorOr<std::unique_ptr<MemoryBuffer>> BufferPtr =
       MemoryBuffer::getFileOrSTDIN(InputFilename);
   if (std::error_code EC = BufferPtr.getError()) {
@@ -325,8 +334,17 @@ int llvm_ml_main(int Argc, char **Argv, const llvm::ToolContext &) {
 
   MAI->setPreserveAsmComments(InputArgs.hasArg(OPT_preserve_comments));
 
-  std::unique_ptr<MCSubtargetInfo> STI(
-      TheTarget->createMCSubtargetInfo(TheTriple, /*CPU=*/"", /*Features=*/""));
+  std::string FeaturesStr;
+  if (InputArgs.hasArg(OPT_mattr)) {
+    SubtargetFeatures Features;
+    for (auto *A : InputArgs.filtered(OPT_mattr))
+      for (StringRef F : llvm::split(A->getValue(), ','))
+        Features.AddFeature(F);
+    FeaturesStr = Features.getString();
+  }
+
+  std::unique_ptr<MCSubtargetInfo> STI(TheTarget->createMCSubtargetInfo(
+      TheTriple, /*CPU=*/"", /*Features=*/FeaturesStr));
   if (!STI) {
     WithColor::error(errs(), ProgName) << "unable to create subtarget info\n";
     exit(1);
@@ -430,6 +448,9 @@ int llvm_ml_main(int Argc, char **Argv, const llvm::ToolContext &) {
     Str->emitAssignment(Feat00Sym, MCConstantExpr::create(Feat00Flags, Ctx));
   }
 
+  if (UnwindV3)
+    Str->setDefaultWinCFIUnwindVersion(3);
+
   int Res = 1;
   if (InputArgs.hasArg(OPT_as_lex)) {
     // -as-lex; Lex only, and output a stream of tokens

>From 3c7fe9bc6731c50c6ab1cc50d2f68cc377bd2de1 Mon Sep 17 00:00:00 2001
From: Daniel Paoliello <danpao at microsoft.com>
Date: Wed, 10 Jun 2026 11:20:23 -0700
Subject: [PATCH 2/3] Addressed PR feedback: remove -mattr, errors for
 push2pop2 or epgr without unwindv3

---
 llvm/docs/ReleaseNotes.md                     |  2 -
 llvm/lib/MC/MCStreamer.cpp                    | 29 +++++++++
 llvm/test/MC/COFF/seh-unwindv3-error.s        | 36 +++++++++++
 llvm/test/tools/llvm-ml/proc_frame_v3.asm     |  2 +-
 .../llvm-ml/unwindv3_required_errors.asm      | 61 +++++++++++++++++++
 llvm/tools/llvm-ml/Opts.td                    |  2 -
 llvm/tools/llvm-ml/llvm-ml.cpp                | 15 +----
 7 files changed, 129 insertions(+), 18 deletions(-)
 create mode 100644 llvm/test/tools/llvm-ml/unwindv3_required_errors.asm

diff --git a/llvm/docs/ReleaseNotes.md b/llvm/docs/ReleaseNotes.md
index ba042577c84ac..68075fd71030d 100644
--- a/llvm/docs/ReleaseNotes.md
+++ b/llvm/docs/ReleaseNotes.md
@@ -321,8 +321,6 @@ Makes programs 10x faster by doing Special New Thing.
 
 ### Changes to the LLVM tools
 
-* llvm-ml now supports the `--mattr` flag for enabling target-specific CPU
-  features (e.g. `--mattr=+push2pop2`).
 * llvm-ml now supports the `/unwindv3` flag to enable V3 unwind information
   format for x64 exception handling.
 * llvm-ml now supports the `@UnwindVersion` built-in symbol, which returns the
diff --git a/llvm/lib/MC/MCStreamer.cpp b/llvm/lib/MC/MCStreamer.cpp
index a02ba1fe31f8c..d6542ef24a9e4 100644
--- a/llvm/lib/MC/MCStreamer.cpp
+++ b/llvm/lib/MC/MCStreamer.cpp
@@ -955,6 +955,23 @@ static unsigned encodeSEHRegNum(MCContext &Ctx, MCRegister Reg) {
   return Ctx.getRegisterInfo()->getSEHRegNum(Reg);
 }
 
+// Unwind formats before v3 store the register operand of an unwind code in a
+// 4-bit field, so extended registers (r16-r31 / xmm16-xmm31, i.e. SEH register
+// numbers greater than 15) cannot be represented. Report an error rather than
+// silently truncating the register number to a different register. Returns true
+// if an error was reported.
+static bool checkUnwindV3ExtendedReg(MCContext &Ctx,
+                                     const WinEH::Instruction &Inst,
+                                     uint8_t Version, SMLoc Loc,
+                                     StringRef Directive) {
+  if (Version < 3 && Inst.Register > 15) {
+    Ctx.reportError(Loc, Directive +
+                             " with an extended register requires unwind v3");
+    return true;
+  }
+  return false;
+}
+
 void MCStreamer::emitWinCFIPushReg(MCRegister Register, SMLoc Loc) {
   WinEH::FrameInfo *CurFrame = EnsureValidWinFrameInfo(Loc);
   if (!CurFrame)
@@ -970,6 +987,9 @@ void MCStreamer::emitWinCFIPushReg(MCRegister Register, SMLoc Loc) {
           Loc, ".seh_pushreg inside epilog requires unwind v3");
     CurrentWinEpilog->Instructions.push_back(Inst);
   } else {
+    if (checkUnwindV3ExtendedReg(getContext(), Inst, CurFrame->Version, Loc,
+                                 ".seh_pushreg"))
+      return;
     CurFrame->Instructions.push_back(Inst);
   }
 }
@@ -1019,6 +1039,9 @@ void MCStreamer::emitWinCFISetFrame(MCRegister Register, unsigned Offset,
           Loc, ".seh_setframe inside epilog requires unwind v3");
     CurrentWinEpilog->Instructions.push_back(Inst);
   } else {
+    if (checkUnwindV3ExtendedReg(getContext(), Inst, CurFrame->Version, Loc,
+                                 ".seh_setframe"))
+      return;
     CurFrame->LastFrameInst = CurFrame->Instructions.size();
     CurFrame->Instructions.push_back(Inst);
   }
@@ -1068,6 +1091,9 @@ void MCStreamer::emitWinCFISaveReg(MCRegister Register, unsigned Offset,
           Loc, ".seh_savereg inside epilog requires unwind v3");
     CurrentWinEpilog->Instructions.push_back(Inst);
   } else {
+    if (checkUnwindV3ExtendedReg(getContext(), Inst, CurFrame->Version, Loc,
+                                 ".seh_savereg"))
+      return;
     CurFrame->Instructions.push_back(Inst);
   }
 }
@@ -1090,6 +1116,9 @@ void MCStreamer::emitWinCFISaveXMM(MCRegister Register, unsigned Offset,
           Loc, ".seh_savexmm inside epilog requires unwind v3");
     CurrentWinEpilog->Instructions.push_back(Inst);
   } else {
+    if (checkUnwindV3ExtendedReg(getContext(), Inst, CurFrame->Version, Loc,
+                                 ".seh_savexmm"))
+      return;
     CurFrame->Instructions.push_back(Inst);
   }
 }
diff --git a/llvm/test/MC/COFF/seh-unwindv3-error.s b/llvm/test/MC/COFF/seh-unwindv3-error.s
index 0cc0e13b172bc..a8bd775870c6c 100644
--- a/llvm/test/MC/COFF/seh-unwindv3-error.s
+++ b/llvm/test/MC/COFF/seh-unwindv3-error.s
@@ -49,6 +49,41 @@ push2_trailing_junk:
     retq
     .seh_endproc
 
+// Test: extended registers (r16-r31 / xmm16-xmm31) in prolog unwind codes
+// cannot be encoded in V1/V2 (the register field is only 4 bits), so they must
+// be diagnosed rather than silently truncated to a low register.
+pushreg_egpr_v1:
+    .seh_proc pushreg_egpr_v1
+    .seh_pushreg %r16
+// CHECK: error: .seh_pushreg with an extended register requires unwind v3
+    .seh_endprologue
+    retq
+    .seh_endproc
+
+savereg_egpr_v1:
+    .seh_proc savereg_egpr_v1
+    .seh_savereg %r16, 0
+// CHECK: error: .seh_savereg with an extended register requires unwind v3
+    .seh_endprologue
+    retq
+    .seh_endproc
+
+savexmm_egpr_v1:
+    .seh_proc savexmm_egpr_v1
+    .seh_savexmm %xmm16, 0
+// CHECK: error: .seh_savexmm with an extended register requires unwind v3
+    .seh_endprologue
+    retq
+    .seh_endproc
+
+setframe_egpr_v1:
+    .seh_proc setframe_egpr_v1
+    .seh_setframe %r16, 0
+// CHECK: error: .seh_setframe with an extended register requires unwind v3
+    .seh_endprologue
+    retq
+    .seh_endproc
+
 // Test: UOP_Push2 recorded under V3 then frame downgraded to V2 — the
 // directive-level check passes (since the frame is already V3), so the
 // error must come from the unwind-info emitter as a recoverable diagnostic
@@ -70,3 +105,4 @@ push2_downgrade_v2:
     retq
     .seh_endproc
 // CHECK: error: UOP_Push2 (PUSH2 with two registers) requires V3 unwind info
+
diff --git a/llvm/test/tools/llvm-ml/proc_frame_v3.asm b/llvm/test/tools/llvm-ml/proc_frame_v3.asm
index 1e9c57cd39c3d..d3e2112455e89 100644
--- a/llvm/test/tools/llvm-ml/proc_frame_v3.asm
+++ b/llvm/test/tools/llvm-ml/proc_frame_v3.asm
@@ -1,4 +1,4 @@
-; RUN: llvm-ml64 -filetype=s --mattr=+push2pop2 /unwindv3 %s /Fo - | FileCheck %s
+; RUN: llvm-ml64 -filetype=s /unwindv3 %s /Fo - | FileCheck %s
 
 .code
 
diff --git a/llvm/test/tools/llvm-ml/unwindv3_required_errors.asm b/llvm/test/tools/llvm-ml/unwindv3_required_errors.asm
new file mode 100644
index 0000000000000..54ebe8fd7007b
--- /dev/null
+++ b/llvm/test/tools/llvm-ml/unwindv3_required_errors.asm
@@ -0,0 +1,61 @@
+; RUN: not llvm-ml64 -filetype=s %s /Fo - 2>&1 | FileCheck %s
+
+; These directives require unwind v3, but this file is assembled WITHOUT the
+; /unwindv3 flag (so the default unwind version is 1). Each use must be
+; diagnosed rather than silently mis-encoded.
+
+.code
+
+; .push2reg / .pop2reg map to UOP_Push2, which only exists in v3.
+t1 PROC FRAME
+  push2 r12, r13
+  .push2reg r12, r13
+; CHECK: error: .seh_push2regs is only supported for unwind v3
+  .endprolog
+  ret
+t1 ENDP
+
+; Epilog unwind codes only exist in v3.
+t2 PROC FRAME
+  push r12
+  .pushreg r12
+  sub rsp, 32
+  .allocstack 32
+  .endprolog
+  nop
+  .beginepilog
+  .freestack 32
+; CHECK: error: .seh_stackalloc inside epilog requires unwind v3
+  add rsp, 32
+  .popreg r12
+; CHECK: error: .seh_pushreg inside epilog requires unwind v3
+  pop r12
+  .endepilog
+  ret
+t2 ENDP
+
+; Extended registers (r16-r31 / xmm16-xmm31) do not fit in the 4-bit register
+; field of a v1/v2 unwind code, so they must be diagnosed rather than truncated
+; to a low register.
+t3 PROC FRAME
+  push r16
+  .pushreg r16
+; CHECK: error: .seh_pushreg with an extended register requires unwind v3
+  .endprolog
+  ret
+t3 ENDP
+
+t4 PROC FRAME
+  sub rsp, 32
+  .allocstack 32
+  .savereg r16, 0
+; CHECK: error: .seh_savereg with an extended register requires unwind v3
+  .savexmm128 xmm16, 16
+; CHECK: error: .seh_savexmm with an extended register requires unwind v3
+  .setframe r17, 0
+; CHECK: error: .seh_setframe with an extended register requires unwind v3
+  .endprolog
+  ret
+t4 ENDP
+
+END
diff --git a/llvm/tools/llvm-ml/Opts.td b/llvm/tools/llvm-ml/Opts.td
index 121c8cfeef566..fb08f4033c6b6 100644
--- a/llvm/tools/llvm-ml/Opts.td
+++ b/llvm/tools/llvm-ml/Opts.td
@@ -41,8 +41,6 @@ def fatal_warnings : LLVMFlag<"fatal-warnings">,
                      HelpText<"Treat warnings as errors">;
 def filetype : LLVMJoined<"filetype=">, Values<"obj,s,null">,
                HelpText<"Emit a file with the given type">;
-def mattr : LLVMCommaJoined<"mattr=">,
-            HelpText<"Target specific attributes (--mattr=help for details)">;
 def output_att_asm : LLVMFlag<"output-att-asm">,
                      HelpText<"Use ATT syntax for output assembly">;
 def show_encoding : LLVMFlag<"show-encoding">,
diff --git a/llvm/tools/llvm-ml/llvm-ml.cpp b/llvm/tools/llvm-ml/llvm-ml.cpp
index b5670e6bf1de6..6f181a6c5d6bb 100644
--- a/llvm/tools/llvm-ml/llvm-ml.cpp
+++ b/llvm/tools/llvm-ml/llvm-ml.cpp
@@ -10,7 +10,6 @@
 //
 //===----------------------------------------------------------------------===//
 
-#include "llvm/ADT/StringExtras.h"
 #include "llvm/ADT/StringSwitch.h"
 #include "llvm/MC/MCAsmBackend.h"
 #include "llvm/MC/MCAsmInfo.h"
@@ -45,7 +44,6 @@
 #include "llvm/Support/VirtualFileSystem.h"
 #include "llvm/Support/WithColor.h"
 #include "llvm/TargetParser/Host.h"
-#include "llvm/TargetParser/SubtargetFeature.h"
 #include <ctime>
 #include <optional>
 
@@ -334,17 +332,8 @@ int llvm_ml_main(int Argc, char **Argv, const llvm::ToolContext &) {
 
   MAI->setPreserveAsmComments(InputArgs.hasArg(OPT_preserve_comments));
 
-  std::string FeaturesStr;
-  if (InputArgs.hasArg(OPT_mattr)) {
-    SubtargetFeatures Features;
-    for (auto *A : InputArgs.filtered(OPT_mattr))
-      for (StringRef F : llvm::split(A->getValue(), ','))
-        Features.AddFeature(F);
-    FeaturesStr = Features.getString();
-  }
-
-  std::unique_ptr<MCSubtargetInfo> STI(TheTarget->createMCSubtargetInfo(
-      TheTriple, /*CPU=*/"", /*Features=*/FeaturesStr));
+  std::unique_ptr<MCSubtargetInfo> STI(
+      TheTarget->createMCSubtargetInfo(TheTriple, /*CPU=*/"", /*Features=*/""));
   if (!STI) {
     WithColor::error(errs(), ProgName) << "unable to create subtarget info\n";
     exit(1);

>From a6a47ca025b7ab59bab8b74d8aaf94afca5110b4 Mon Sep 17 00:00:00 2001
From: Daniel Paoliello <danpao at microsoft.com>
Date: Thu, 11 Jun 2026 09:16:38 -0700
Subject: [PATCH 3/3] Addressed PR feedback

---
 llvm/lib/MC/MCParser/COFFMasmParser.cpp       | 44 +++++++++----------
 .../lib/Target/X86/AsmParser/X86AsmParser.cpp | 30 ++++++-------
 llvm/test/tools/llvm-ml/proc_frame_v3.asm     |  4 +-
 3 files changed, 39 insertions(+), 39 deletions(-)

diff --git a/llvm/lib/MC/MCParser/COFFMasmParser.cpp b/llvm/lib/MC/MCParser/COFFMasmParser.cpp
index 7ba3aa0256190..4e899f980a535 100644
--- a/llvm/lib/MC/MCParser/COFFMasmParser.cpp
+++ b/llvm/lib/MC/MCParser/COFFMasmParser.cpp
@@ -51,15 +51,15 @@ class COFFMasmParser : public MCAsmParserExtension {
   bool parseSEHDirectiveAllocStack(StringRef, SMLoc);
   bool parseSEHDirectiveFreeStack(StringRef, SMLoc);
   bool parseSEHDirectiveEndProlog(StringRef, SMLoc);
-  bool ParseSEHDirectiveBeginEpilog(StringRef, SMLoc);
-  bool ParseSEHDirectiveEndEpilog(StringRef, SMLoc);
+  bool parseSEHDirectiveBeginEpilog(StringRef, SMLoc);
+  bool parseSEHDirectiveEndEpilog(StringRef, SMLoc);
 
   /// Check that we are inside a PROC FRAME.
-  bool ensureInsideFrame(SMLoc Loc, StringRef Directive);
+  bool ensureInsideFrame(SMLoc Loc);
   /// Check that we are in the prolog (before .endprolog).
-  bool ensureInProlog(SMLoc Loc, StringRef Directive);
+  bool ensureInProlog(SMLoc Loc);
   /// Check that we are inside a .beginepilog/.endepilog block.
-  bool ensureInEpilog(SMLoc Loc, StringRef Directive);
+  bool ensureInEpilog(SMLoc Loc);
 
   bool IgnoreDirective(StringRef, SMLoc) {
     while (!getLexer().is(AsmToken::EndOfStatement)) {
@@ -79,9 +79,9 @@ class COFFMasmParser : public MCAsmParserExtension {
         ".freestack");
     addDirectiveHandler<&COFFMasmParser::parseSEHDirectiveEndProlog>(
         ".endprolog");
-    addDirectiveHandler<&COFFMasmParser::ParseSEHDirectiveBeginEpilog>(
+    addDirectiveHandler<&COFFMasmParser::parseSEHDirectiveBeginEpilog>(
         ".beginepilog");
-    addDirectiveHandler<&COFFMasmParser::ParseSEHDirectiveEndEpilog>(
+    addDirectiveHandler<&COFFMasmParser::parseSEHDirectiveEndEpilog>(
         ".endepilog");
 
     // Code label directives
@@ -533,7 +533,7 @@ bool COFFMasmParser::parseDirectiveAlias(StringRef Directive, SMLoc Loc) {
   return false;
 }
 
-bool COFFMasmParser::ensureInsideFrame(SMLoc Loc, StringRef Directive) {
+bool COFFMasmParser::ensureInsideFrame(SMLoc Loc) {
   if (CurrentProceduresFramed.empty() || !CurrentProceduresFramed.back()) {
     return Error(Loc,
                  "Missing Frame in proc, no unwind code will be generated.");
@@ -541,8 +541,8 @@ bool COFFMasmParser::ensureInsideFrame(SMLoc Loc, StringRef Directive) {
   return false;
 }
 
-bool COFFMasmParser::ensureInProlog(SMLoc Loc, StringRef Directive) {
-  if (ensureInsideFrame(Loc, Directive))
+bool COFFMasmParser::ensureInProlog(SMLoc Loc) {
+  if (ensureInsideFrame(Loc))
     return true;
   if (getStreamer().isWinCFIPrologEnded()) {
     return Error(Loc, "prolog directive must be used inside a prolog");
@@ -550,8 +550,8 @@ bool COFFMasmParser::ensureInProlog(SMLoc Loc, StringRef Directive) {
   return false;
 }
 
-bool COFFMasmParser::ensureInEpilog(SMLoc Loc, StringRef Directive) {
-  if (ensureInsideFrame(Loc, Directive))
+bool COFFMasmParser::ensureInEpilog(SMLoc Loc) {
+  if (ensureInsideFrame(Loc))
     return true;
   if (!getStreamer().isInEpilogCFI()) {
     return Error(Loc, "epilog directive must be used inside an epilog");
@@ -559,9 +559,9 @@ bool COFFMasmParser::ensureInEpilog(SMLoc Loc, StringRef Directive) {
   return false;
 }
 
-bool COFFMasmParser::parseSEHDirectiveAllocStack(StringRef Directive,
+bool COFFMasmParser::parseSEHDirectiveAllocStack(StringRef /*Directive*/,
                                                  SMLoc Loc) {
-  if (ensureInProlog(Loc, Directive))
+  if (ensureInProlog(Loc))
     return true;
   int64_t Size;
   SMLoc SizeLoc = getTok().getLoc();
@@ -575,9 +575,9 @@ bool COFFMasmParser::parseSEHDirectiveAllocStack(StringRef Directive,
   return false;
 }
 
-bool COFFMasmParser::parseSEHDirectiveFreeStack(StringRef Directive,
+bool COFFMasmParser::parseSEHDirectiveFreeStack(StringRef /*Directive*/,
                                                 SMLoc Loc) {
-  if (ensureInEpilog(Loc, Directive))
+  if (ensureInEpilog(Loc))
     return true;
   int64_t Size;
   SMLoc SizeLoc = getTok().getLoc();
@@ -591,17 +591,17 @@ bool COFFMasmParser::parseSEHDirectiveFreeStack(StringRef Directive,
   return false;
 }
 
-bool COFFMasmParser::parseSEHDirectiveEndProlog(StringRef Directive,
+bool COFFMasmParser::parseSEHDirectiveEndProlog(StringRef /*Directive*/,
                                                 SMLoc Loc) {
-  if (ensureInsideFrame(Loc, Directive))
+  if (ensureInsideFrame(Loc))
     return true;
   getStreamer().emitWinCFIEndProlog(Loc);
   return false;
 }
 
-bool COFFMasmParser::ParseSEHDirectiveBeginEpilog(StringRef Directive,
+bool COFFMasmParser::parseSEHDirectiveBeginEpilog(StringRef /*Directive*/,
                                                   SMLoc Loc) {
-  if (ensureInsideFrame(Loc, Directive))
+  if (ensureInsideFrame(Loc))
     return true;
   if (getStreamer().isInEpilogCFI()) {
     return Error(Loc, ".beginepilog must come after .endprolog or .endepilog");
@@ -610,9 +610,9 @@ bool COFFMasmParser::ParseSEHDirectiveBeginEpilog(StringRef Directive,
   return false;
 }
 
-bool COFFMasmParser::ParseSEHDirectiveEndEpilog(StringRef Directive,
+bool COFFMasmParser::parseSEHDirectiveEndEpilog(StringRef /*Directive*/,
                                                 SMLoc Loc) {
-  if (ensureInsideFrame(Loc, Directive))
+  if (ensureInsideFrame(Loc))
     return true;
   getStreamer().emitWinCFIEndEpilogue(Loc);
   return false;
diff --git a/llvm/lib/Target/X86/AsmParser/X86AsmParser.cpp b/llvm/lib/Target/X86/AsmParser/X86AsmParser.cpp
index ab5c3440901c2..33ce25425b29e 100644
--- a/llvm/lib/Target/X86/AsmParser/X86AsmParser.cpp
+++ b/llvm/lib/Target/X86/AsmParser/X86AsmParser.cpp
@@ -1238,8 +1238,8 @@ class X86AsmParser : public MCTargetAsmParser {
   bool parseDirectiveSEHSaveXMM(SMLoc);
   bool parseDirectiveSEHPushFrame(SMLoc);
 
-  bool ensureMasmEpilogContext(SMLoc Loc, StringRef Directive);
-  bool ensureMasmPrologContext(SMLoc Loc, StringRef Directive);
+  bool ensureMasmEpilogContext(SMLoc Loc);
+  bool ensureMasmPrologContext(SMLoc Loc);
 
   unsigned checkTargetMatchPredicate(MCInst &Inst) override;
 
@@ -4855,42 +4855,42 @@ bool X86AsmParser::ParseDirective(AsmToken DirectiveID) {
   else if (Parser.isParsingMasm()) {
     // MASM prolog directives.
     if (IDVal.equals_insensitive(".pushreg")) {
-      return ensureMasmPrologContext(DirectiveID.getLoc(), IDVal) ||
+      return ensureMasmPrologContext(DirectiveID.getLoc()) ||
              parseDirectiveSEHPushReg(DirectiveID.getLoc());
     } else if (IDVal.equals_insensitive(".push2reg")) {
-      return ensureMasmPrologContext(DirectiveID.getLoc(), IDVal) ||
+      return ensureMasmPrologContext(DirectiveID.getLoc()) ||
              parseDirectiveSEHPush2Regs(DirectiveID.getLoc());
     } else if (IDVal.equals_insensitive(".setframe")) {
-      return ensureMasmPrologContext(DirectiveID.getLoc(), IDVal) ||
+      return ensureMasmPrologContext(DirectiveID.getLoc()) ||
              parseDirectiveSEHSetFrame(DirectiveID.getLoc());
     } else if (IDVal.equals_insensitive(".savereg")) {
-      return ensureMasmPrologContext(DirectiveID.getLoc(), IDVal) ||
+      return ensureMasmPrologContext(DirectiveID.getLoc()) ||
              parseDirectiveSEHSaveReg(DirectiveID.getLoc());
     } else if (IDVal.equals_insensitive(".savexmm128")) {
-      return ensureMasmPrologContext(DirectiveID.getLoc(), IDVal) ||
+      return ensureMasmPrologContext(DirectiveID.getLoc()) ||
              parseDirectiveSEHSaveXMM(DirectiveID.getLoc());
     } else if (IDVal.equals_insensitive(".pushframe")) {
-      return ensureMasmPrologContext(DirectiveID.getLoc(), IDVal) ||
+      return ensureMasmPrologContext(DirectiveID.getLoc()) ||
              parseDirectiveSEHPushFrame(DirectiveID.getLoc());
     }
     // MASM epilog directives
     if (IDVal.equals_insensitive(".popreg")) {
-      return ensureMasmEpilogContext(DirectiveID.getLoc(), IDVal) ||
+      return ensureMasmEpilogContext(DirectiveID.getLoc()) ||
              parseDirectiveSEHPushReg(DirectiveID.getLoc());
     } else if (IDVal.equals_insensitive(".pop2reg")) {
       // .pop2reg args are in the order they are popped, so reverse them to get
       // the order they were pushed.
-      return ensureMasmEpilogContext(DirectiveID.getLoc(), IDVal) ||
+      return ensureMasmEpilogContext(DirectiveID.getLoc()) ||
              parseDirectiveSEHPush2Regs(DirectiveID.getLoc(),
                                         /*SwapRegs=*/true);
     } else if (IDVal.equals_insensitive(".unsetframe")) {
-      return ensureMasmEpilogContext(DirectiveID.getLoc(), IDVal) ||
+      return ensureMasmEpilogContext(DirectiveID.getLoc()) ||
              parseDirectiveSEHSetFrame(DirectiveID.getLoc());
     } else if (IDVal.equals_insensitive(".restorereg")) {
-      return ensureMasmEpilogContext(DirectiveID.getLoc(), IDVal) ||
+      return ensureMasmEpilogContext(DirectiveID.getLoc()) ||
              parseDirectiveSEHSaveReg(DirectiveID.getLoc());
     } else if (IDVal.equals_insensitive(".restorexmm128")) {
-      return ensureMasmEpilogContext(DirectiveID.getLoc(), IDVal) ||
+      return ensureMasmEpilogContext(DirectiveID.getLoc()) ||
              parseDirectiveSEHSaveXMM(DirectiveID.getLoc());
     }
   }
@@ -5202,14 +5202,14 @@ bool X86AsmParser::parseDirectiveSEHSaveXMM(SMLoc Loc) {
   return false;
 }
 
-bool X86AsmParser::ensureMasmPrologContext(SMLoc Loc, StringRef Directive) {
+bool X86AsmParser::ensureMasmPrologContext(SMLoc Loc) {
   if (getStreamer().isWinCFIPrologEnded()) {
     return Error(Loc, "prolog directive must be used inside a prolog");
   }
   return false;
 }
 
-bool X86AsmParser::ensureMasmEpilogContext(SMLoc Loc, StringRef Directive) {
+bool X86AsmParser::ensureMasmEpilogContext(SMLoc Loc) {
   if (!getStreamer().isInEpilogCFI()) {
     return Error(Loc, "epilog directive must be used inside an epilog");
   }
diff --git a/llvm/test/tools/llvm-ml/proc_frame_v3.asm b/llvm/test/tools/llvm-ml/proc_frame_v3.asm
index d3e2112455e89..367e08a8b1030 100644
--- a/llvm/test/tools/llvm-ml/proc_frame_v3.asm
+++ b/llvm/test/tools/llvm-ml/proc_frame_v3.asm
@@ -17,7 +17,7 @@ t1 PROC FRAME
   .beginepilog
   .freestack 32
   add rsp, 32
-  .pop2reg r12, r13
+  .pop2reg r13, r12
   pop2 r13, r12
   .endepilog
   ret
@@ -35,7 +35,7 @@ t1 ENDP
 ; CHECK: .seh_startepilogue
 ; CHECK: .seh_stackalloc 32
 ; CHECK: add rsp, 32
-; CHECK: .seh_push2regs r13, r12
+; CHECK: .seh_push2regs r12, r13
 ; CHECK: pop2 r13, r12
 ; CHECK: .seh_endepilogue
 ; CHECK: ret



More information about the llvm-commits mailing list