[libunwind] [libunwind] Support extended amd64 segment registers (PR #229176)

Minsoo Choo via cfe-commits cfe-commits at lists.llvm.org
Sat Oct 24 17:26:56 PDT 2026


https://github.com/mchoo7 updated https://github.com/llvm/llvm-project/pull/229176

>From 525000350d3b51899ec20d907803321722d47958 Mon Sep 17 00:00:00 2001
From: Minsoo Choo <minsoo at minsoo.io>
Date: Mon, 5 Oct 2026 12:36:23 -0400
Subject: [PATCH 1/2] [libunwind] Support extended amd64 segment registers

Raise the amd64 DWARF register limit to 59 and add RFLAGS, all six
segment selectors, FS.base, and GS.base to the register model.
Capture the extended state and restore it when resuming, using
setcontext on FreeBSD to restore selectors and bases together.
Pack the state into the existing context buffers without changing
their public sizes or the Windows XMM offsets.

Add tests for register access, signal-frame CFI recovery, register
bounds, and resume behavior.
---
 libunwind/include/__libunwind_config.h        |   2 +-
 libunwind/include/libunwind.h                 |   9 +
 libunwind/src/AddressSpace.hpp                |   4 +-
 libunwind/src/Registers.hpp                   |  99 ++++++++++-
 libunwind/src/UnwindCursor.hpp                |  25 +++
 libunwind/src/UnwindRegistersRestore.S        |  67 ++++++-
 libunwind/src/UnwindRegistersSave.S           |  60 ++++++-
 libunwind/src/assembly.h                      |  18 ++
 libunwind/src/libunwind.cpp                   |  49 ++++++
 .../test/x86_64_dwarf_registers.pass.cpp      |  59 +++++++
 .../test/x86_64_extended_registers.pass.cpp   | 163 ++++++++++++++++++
 .../x86_64_getcontext_preserves_rbx.pass.cpp  |  80 +++++++++
 .../x86_64_resume_extended_registers.pass.cpp | 158 +++++++++++++++++
 13 files changed, 771 insertions(+), 22 deletions(-)
 create mode 100644 libunwind/test/x86_64_dwarf_registers.pass.cpp
 create mode 100644 libunwind/test/x86_64_extended_registers.pass.cpp
 create mode 100644 libunwind/test/x86_64_getcontext_preserves_rbx.pass.cpp
 create mode 100644 libunwind/test/x86_64_resume_extended_registers.pass.cpp

diff --git a/libunwind/include/__libunwind_config.h b/libunwind/include/__libunwind_config.h
index 0c7aed13375a14..674a6461b935a1 100644
--- a/libunwind/include/__libunwind_config.h
+++ b/libunwind/include/__libunwind_config.h
@@ -17,7 +17,7 @@
 #endif
 
 #define _LIBUNWIND_HIGHEST_DWARF_REGISTER_X86       8
-#define _LIBUNWIND_HIGHEST_DWARF_REGISTER_X86_64    32
+#define _LIBUNWIND_HIGHEST_DWARF_REGISTER_X86_64    59
 #define _LIBUNWIND_HIGHEST_DWARF_REGISTER_PPC       112
 #define _LIBUNWIND_HIGHEST_DWARF_REGISTER_PPC64     116
 #define _LIBUNWIND_HIGHEST_DWARF_REGISTER_ARM64     95
diff --git a/libunwind/include/libunwind.h b/libunwind/include/libunwind.h
index 6c2dfe58e53bf1..7c2f611e208923 100644
--- a/libunwind/include/libunwind.h
+++ b/libunwind/include/libunwind.h
@@ -295,6 +295,15 @@ enum {
   UNW_X86_64_XMM13 = 30,
   UNW_X86_64_XMM14 = 31,
   UNW_X86_64_XMM15 = 32,
+  UNW_X86_64_RFLAGS = 49,
+  UNW_X86_64_ES = 50,
+  UNW_X86_64_CS = 51,
+  UNW_X86_64_SS = 52,
+  UNW_X86_64_DS = 53,
+  UNW_X86_64_FS = 54,
+  UNW_X86_64_GS = 55,
+  UNW_X86_64_FS_BASE = 58,
+  UNW_X86_64_GS_BASE = 59,
 };
 
 
diff --git a/libunwind/src/AddressSpace.hpp b/libunwind/src/AddressSpace.hpp
index 3711189f57b1d1..d446f6abd9578f 100644
--- a/libunwind/src/AddressSpace.hpp
+++ b/libunwind/src/AddressSpace.hpp
@@ -453,8 +453,8 @@ static bool checkForUnwindInfoSegment(const Elf_Phdr *phdr, size_t image_base,
 #endif
 }
 
-static int findUnwindSectionsByPhdr(struct dl_phdr_info *pinfo,
-                                    size_t pinfo_size, void *data) {
+static inline int findUnwindSectionsByPhdr(struct dl_phdr_info *pinfo,
+                                           size_t pinfo_size, void *data) {
   auto cbdata = static_cast<dl_iterate_cb_data *>(data);
   if (pinfo->dlpi_phnum == 0 || cbdata->targetAddr < pinfo->dlpi_addr)
     return 0;
diff --git a/libunwind/src/Registers.hpp b/libunwind/src/Registers.hpp
index 4870e315962e09..7b76708510beba 100644
--- a/libunwind/src/Registers.hpp
+++ b/libunwind/src/Registers.hpp
@@ -347,14 +347,26 @@ class _LIBUNWIND_HIDDEN Registers_x86_64 {
     uint64_t __r14;
     uint64_t __r15;
     uint64_t __rip;
-    uint64_t __rflags;
-    uint64_t __cs;
-    uint64_t __fs;
-    uint64_t __gs;
+    // RFLAGS bits 32-63 are reserved. Pack the selectors with the low flags
+    // word so the complete segment state fits the existing context ABI.
+    uint32_t __rflags;
+    uint16_t __cs;
+    uint16_t __ss;
+    uint16_t __ds;
+    uint16_t __es;
+    uint16_t __fs;
+    uint16_t __gs;
+    uint64_t __fs_base;
+    uint64_t __gs_base;
 #if defined(_WIN64)
     uint64_t __padding; // 16-byte align
 #endif
   };
+  static_assert(offsetof(GPRs, __rflags) == 136 &&
+                    offsetof(GPRs, __cs) == 140 &&
+                    offsetof(GPRs, __fs_base) == 152 &&
+                    offsetof(GPRs, __gs_base) == 160,
+                "x86_64 segment state must match the assembly offsets");
   GPRs _registers;
 #if defined(_WIN64)
   v128 _xmm[16];
@@ -378,9 +390,19 @@ inline bool Registers_x86_64::validRegister(int regNum) const {
     return true;
   if (regNum < 0)
     return false;
-  if (regNum > 16)
-    return false;
-  return true;
+  if (regNum <= UNW_X86_64_RIP)
+    return true;
+  if (regNum >= UNW_X86_64_RFLAGS && regNum <= UNW_X86_64_GS)
+    return true;
+  // Native base capture and restore need an OS interface. Cross unwinding
+  // can still represent bases recovered from DWARF on any host.
+#if (!defined(_LIBUNWIND_IS_BAREMETAL) &&                                      \
+     (defined(__linux__) || defined(__FreeBSD__))) ||                          \
+    !defined(_LIBUNWIND_IS_NATIVE_ONLY)
+  if (regNum == UNW_X86_64_FS_BASE || regNum == UNW_X86_64_GS_BASE)
+    return true;
+#endif
+  return false;
 }
 
 inline uint64_t Registers_x86_64::getRegister(int regNum) const {
@@ -422,6 +444,24 @@ inline uint64_t Registers_x86_64::getRegister(int regNum) const {
     return _registers.__r14;
   case UNW_X86_64_R15:
     return _registers.__r15;
+  case UNW_X86_64_RFLAGS:
+    return _registers.__rflags;
+  case UNW_X86_64_ES:
+    return _registers.__es;
+  case UNW_X86_64_CS:
+    return _registers.__cs;
+  case UNW_X86_64_SS:
+    return _registers.__ss;
+  case UNW_X86_64_DS:
+    return _registers.__ds;
+  case UNW_X86_64_FS:
+    return _registers.__fs;
+  case UNW_X86_64_GS:
+    return _registers.__gs;
+  case UNW_X86_64_FS_BASE:
+    return _registers.__fs_base;
+  case UNW_X86_64_GS_BASE:
+    return _registers.__gs_base;
   }
   _LIBUNWIND_ABORT("unsupported x86_64 register");
 }
@@ -483,6 +523,33 @@ inline void Registers_x86_64::setRegister(int regNum, uint64_t value) {
   case UNW_X86_64_R15:
     _registers.__r15 = value;
     return;
+  case UNW_X86_64_RFLAGS:
+    _registers.__rflags = static_cast<uint32_t>(value);
+    return;
+  case UNW_X86_64_ES:
+    _registers.__es = static_cast<uint16_t>(value);
+    return;
+  case UNW_X86_64_CS:
+    _registers.__cs = static_cast<uint16_t>(value);
+    return;
+  case UNW_X86_64_SS:
+    _registers.__ss = static_cast<uint16_t>(value);
+    return;
+  case UNW_X86_64_DS:
+    _registers.__ds = static_cast<uint16_t>(value);
+    return;
+  case UNW_X86_64_FS:
+    _registers.__fs = static_cast<uint16_t>(value);
+    return;
+  case UNW_X86_64_GS:
+    _registers.__gs = static_cast<uint16_t>(value);
+    return;
+  case UNW_X86_64_FS_BASE:
+    _registers.__fs_base = value;
+    return;
+  case UNW_X86_64_GS_BASE:
+    _registers.__gs_base = value;
+    return;
   }
   _LIBUNWIND_ABORT("unsupported x86_64 register");
 }
@@ -526,6 +593,24 @@ inline const char *Registers_x86_64::getRegisterName(int regNum) {
     return "r14";
   case UNW_X86_64_R15:
     return "r15";
+  case UNW_X86_64_RFLAGS:
+    return "rflags";
+  case UNW_X86_64_ES:
+    return "es";
+  case UNW_X86_64_CS:
+    return "cs";
+  case UNW_X86_64_SS:
+    return "ss";
+  case UNW_X86_64_DS:
+    return "ds";
+  case UNW_X86_64_FS:
+    return "fs";
+  case UNW_X86_64_GS:
+    return "gs";
+  case UNW_X86_64_FS_BASE:
+    return "fs.base";
+  case UNW_X86_64_GS_BASE:
+    return "gs.base";
   case UNW_X86_64_XMM0:
     return "xmm0";
   case UNW_X86_64_XMM1:
diff --git a/libunwind/src/UnwindCursor.hpp b/libunwind/src/UnwindCursor.hpp
index f307190addadac..89fa854c826d6e 100644
--- a/libunwind/src/UnwindCursor.hpp
+++ b/libunwind/src/UnwindCursor.hpp
@@ -621,6 +621,7 @@ UnwindCursor<A, R>::UnwindCursor(unw_context_t *context, A &as)
   RtlCaptureContext(&_msContext);
   _msContext.ContextFlags = CONTEXT_CONTROL|CONTEXT_INTEGER|CONTEXT_FLOATING_POINT;
 #if defined(_LIBUNWIND_TARGET_X86_64)
+  _msContext.ContextFlags |= CONTEXT_SEGMENTS;
   _msContext.Rax = r.getRegister(UNW_X86_64_RAX);
   _msContext.Rcx = r.getRegister(UNW_X86_64_RCX);
   _msContext.Rdx = r.getRegister(UNW_X86_64_RDX);
@@ -638,6 +639,15 @@ UnwindCursor<A, R>::UnwindCursor(unw_context_t *context, A &as)
   _msContext.R14 = r.getRegister(UNW_X86_64_R14);
   _msContext.R15 = r.getRegister(UNW_X86_64_R15);
   _msContext.Rip = r.getRegister(UNW_REG_IP);
+#if !defined(__arm64ec__)
+  _msContext.EFlags = static_cast<DWORD>(r.getRegister(UNW_X86_64_RFLAGS));
+  _msContext.SegEs = static_cast<WORD>(r.getRegister(UNW_X86_64_ES));
+  _msContext.SegCs = static_cast<WORD>(r.getRegister(UNW_X86_64_CS));
+  _msContext.SegSs = static_cast<WORD>(r.getRegister(UNW_X86_64_SS));
+  _msContext.SegDs = static_cast<WORD>(r.getRegister(UNW_X86_64_DS));
+  _msContext.SegFs = static_cast<WORD>(r.getRegister(UNW_X86_64_FS));
+  _msContext.SegGs = static_cast<WORD>(r.getRegister(UNW_X86_64_GS));
+#endif
   union {
     v128 v;
     M128A m;
@@ -728,6 +738,7 @@ bool UnwindCursor<A, R>::validReg(int regNum) {
   if (regNum == UNW_REG_IP || regNum == UNW_REG_SP) return true;
 #if defined(_LIBUNWIND_TARGET_X86_64)
   if (regNum >= UNW_X86_64_RAX && regNum <= UNW_X86_64_RIP) return true;
+  if (regNum >= UNW_X86_64_RFLAGS && regNum <= UNW_X86_64_GS) return true;
 #elif defined(_LIBUNWIND_TARGET_ARM)
   if ((regNum >= UNW_ARM_R0 && regNum <= UNW_ARM_R15) ||
       regNum == UNW_ARM_RA_AUTH_CODE)
@@ -761,6 +772,13 @@ unw_word_t UnwindCursor<A, R>::getReg(int regNum) {
   case UNW_X86_64_R13: return _msContext.R13;
   case UNW_X86_64_R14: return _msContext.R14;
   case UNW_X86_64_R15: return _msContext.R15;
+  case UNW_X86_64_RFLAGS: return _msContext.EFlags;
+  case UNW_X86_64_ES: return _msContext.SegEs;
+  case UNW_X86_64_CS: return _msContext.SegCs;
+  case UNW_X86_64_SS: return _msContext.SegSs;
+  case UNW_X86_64_DS: return _msContext.SegDs;
+  case UNW_X86_64_FS: return _msContext.SegFs;
+  case UNW_X86_64_GS: return _msContext.SegGs;
 #elif defined(_LIBUNWIND_TARGET_ARM)
   case UNW_ARM_R0: return _msContext.R0;
   case UNW_ARM_R1: return _msContext.R1;
@@ -812,6 +830,13 @@ void UnwindCursor<A, R>::setReg(int regNum, unw_word_t value) {
   case UNW_X86_64_R13: _msContext.R13 = value; break;
   case UNW_X86_64_R14: _msContext.R14 = value; break;
   case UNW_X86_64_R15: _msContext.R15 = value; break;
+  case UNW_X86_64_RFLAGS: _msContext.EFlags = static_cast<DWORD>(value); break;
+  case UNW_X86_64_ES: _msContext.SegEs = static_cast<WORD>(value); break;
+  case UNW_X86_64_CS: _msContext.SegCs = static_cast<WORD>(value); break;
+  case UNW_X86_64_SS: _msContext.SegSs = static_cast<WORD>(value); break;
+  case UNW_X86_64_DS: _msContext.SegDs = static_cast<WORD>(value); break;
+  case UNW_X86_64_FS: _msContext.SegFs = static_cast<WORD>(value); break;
+  case UNW_X86_64_GS: _msContext.SegGs = static_cast<WORD>(value); break;
 #elif defined(_LIBUNWIND_TARGET_ARM)
   case UNW_ARM_R0: _msContext.R0 = value; break;
   case UNW_ARM_R1: _msContext.R1 = value; break;
diff --git a/libunwind/src/UnwindRegistersRestore.S b/libunwind/src/UnwindRegistersRestore.S
index b299bcd0a75949..defb1bafa6d42d 100644
--- a/libunwind/src/UnwindRegistersRestore.S
+++ b/libunwind/src/UnwindRegistersRestore.S
@@ -72,7 +72,8 @@ DEFINE_LIBUNWIND_FUNCTION(__libunwind_Registers_x86_jumpto)
   # skip fs
   # skip gs
 
-#elif defined(__x86_64__) && !defined(__arm64ec__)
+#elif defined(__x86_64__) && !defined(__arm64ec__) && \
+    (!defined(__FreeBSD__) || defined(_LIBUNWIND_IS_BAREMETAL))
 .att_syntax
 
 DEFINE_LIBUNWIND_FUNCTION(__libunwind_Registers_x86_64_jumpto)
@@ -98,6 +99,57 @@ DEFINE_LIBUNWIND_FUNCTION(__libunwind_Registers_x86_64_jumpto)
   movq  %rbx, 0(%rax)
   movq  128(%rdi), %rbx # store new rip on new stack
   movq  %rbx, 8(%rax)
+  # Segment selectors are 16 bits. In particular, do not reload unchanged
+  # FS/GS selectors: doing so can clear their hidden base addresses.
+  movw  %fs,%ax
+  cmpw  148(%rdi),%ax
+  je    1f
+  movw  148(%rdi),%fs
+1:
+  movw  %gs,%ax
+  cmpw  150(%rdi),%ax
+  je    2f
+  movw  150(%rdi),%gs
+2:
+  movw  146(%rdi),%es
+  movw  142(%rdi),%ss
+  movw  144(%rdi),%ds
+  movw  %cs,%ax
+  cmpw  140(%rdi),%ax
+  je    3f
+  # An indirect far jump changes CS without consuming a shadow-stack entry.
+  movzwq 140(%rdi),%rax
+  pushq %rax
+  leaq  3f(%rip),%rax
+  pushq %rax
+  rex64 ljmp *(%rsp)
+3:
+  _LIBUNWIND_CET_ENDBR
+  # The far-jump path used a temporary 16-byte far pointer on the old stack.
+  # The old stack is discarded below.
+
+#if defined(X86_64_BASE_SYSCALL)
+  movq  %rdi,%r8
+  movq  $X86_64_BASE_SYSCALL,%rax
+  movq  $X86_64_SET_FSBASE,%rdi
+  movq  152(%r8),%rsi
+  syscall
+  testq %rax,%rax
+  js    9f
+  movq  $X86_64_BASE_SYSCALL,%rax
+  movq  $X86_64_SET_GSBASE,%rdi
+  movq  160(%r8),%rsi
+  syscall
+  testq %rax,%rax
+  js    9f
+  movq  %r8,%rdi
+#endif
+
+  # Restore flags after all arithmetic, comparisons, and syscalls. The upper
+  # half of the packed flags word contains selectors, so zero-extend it first.
+  movl  136(%rdi),%eax
+  pushq %rax
+  popfq
   # restore all registers
   movq    0(%rdi), %rax
   movq    8(%rdi), %rbx
@@ -115,10 +167,6 @@ DEFINE_LIBUNWIND_FUNCTION(__libunwind_Registers_x86_64_jumpto)
   movq  104(%rdi), %r13
   movq  112(%rdi), %r14
   movq  120(%rdi), %r15
-  # skip rflags
-  # skip cs
-  # skip fs
-  # skip gs
 
 #if defined(_WIN64)
   movdqu 176(%rdi),%xmm0
@@ -140,8 +188,13 @@ DEFINE_LIBUNWIND_FUNCTION(__libunwind_Registers_x86_64_jumpto)
 #endif
   movq  56(%rdi), %rsp  # cut back rsp to new location
   pop    %rdi      # rdi was saved here earlier
-  pop    %rcx
-  jmpq   *%rcx
+  leaq   8(%rsp),%rsp
+  jmpq   *-8(%rsp)
+#if defined(X86_64_BASE_SYSCALL)
+9:
+  # A failed base restore cannot safely resume with a different TLS base.
+  ud2
+#endif
 
 
 #elif defined(__powerpc64__)
diff --git a/libunwind/src/UnwindRegistersSave.S b/libunwind/src/UnwindRegistersSave.S
index c51b5b8d176646..0f87cccb34108a 100644
--- a/libunwind/src/UnwindRegistersSave.S
+++ b/libunwind/src/UnwindRegistersSave.S
@@ -91,6 +91,8 @@ DEFINE_LIBUNWIND_FUNCTION("#__unw_getcontext")
   stp    x19,x20, [x0, #0x060]  // r12, r13
   stp    x21,x22, [x0, #0x070]  // r14, r15
   str    x30,     [x0, #0x080]  // store return address as pc
+  stp    xzr,xzr, [x0, #0x088]  // flags and segment selectors
+  stp    xzr,xzr, [x0, #0x098]  // fs.base, gs.base
   stp    q0, q1,  [x0, #0x0b0]  // xmm0, xmm1
   stp    q2, q3,  [x0, #0x0d0]  // xmm2, xmm3
   stp    q4, q5,  [x0, #0x0f0]  // xmm4, xmm5
@@ -124,9 +126,11 @@ DEFINE_LIBUNWIND_FUNCTION(__unw_getcontext)
 #if defined(_WIN64)
 #define PTR %rcx
 #define TMP %rdx
+#define TMP32 %edx
 #else
 #define PTR %rdi
 #define TMP %rsi
+#define TMP32 %esi
 #endif
 
   _LIBUNWIND_CET_ENDBR
@@ -138,7 +142,6 @@ DEFINE_LIBUNWIND_FUNCTION(__unw_getcontext)
   movq  %rsi, 40(PTR)
   movq  %rbp, 48(PTR)
   movq  %rsp, 56(PTR)
-  addq  $8,   56(PTR)
   movq  %r8,  64(PTR)
   movq  %r9,  72(PTR)
   movq  %r10, 80(PTR)
@@ -149,10 +152,51 @@ DEFINE_LIBUNWIND_FUNCTION(__unw_getcontext)
   movq  %r15,120(PTR)
   movq  (%rsp),TMP
   movq  TMP,128(PTR) # store return address as rip
-  # skip rflags
-  # skip cs
-  # skip fs
-  # skip gs
+  # Capture flags before any instruction that changes them.
+  pushfq
+  popq  TMP
+  movl  TMP32,136(PTR)
+  leaq  8(%rsp),TMP
+  movq  TMP,56(PTR) # store what sp was at call site as rsp
+  movw  %cs,140(PTR)
+  movw  %fs,148(PTR)
+  movw  %gs,150(PTR)
+  movw  %es,146(PTR)
+  movw  %ss,142(PTR)
+  movw  %ds,144(PTR)
+  movq  $0,152(PTR)
+  movq  $0,160(PTR)
+
+#if defined(X86_64_BASE_SYSCALL)
+  # Use syscalls rather than FSGSBASE instructions, which require both CPU
+  # support and OS enablement. All syscall-clobbered registers are saved above.
+  # FreeBSD may clear r8-r10 on syscall return. Keep the context pointer in
+  # the callee-saved rbx, preserving its value for our caller as well.
+  pushq %rbx
+  movq  PTR,%rbx
+  movq  $X86_64_BASE_SYSCALL,%rax
+  movq  $X86_64_GET_FSBASE,%rdi
+  leaq  152(%rbx),%rsi
+  syscall
+#if defined(__FreeBSD__)
+  jc    9f
+#else
+  testq %rax,%rax
+  js    9f
+#endif
+  movq  $X86_64_BASE_SYSCALL,%rax
+  movq  $X86_64_GET_GSBASE,%rdi
+  leaq  160(%rbx),%rsi
+  syscall
+#if defined(__FreeBSD__)
+  jc    9f
+#else
+  testq %rax,%rax
+  js    9f
+#endif
+  movq  %rbx,PTR
+  popq  %rbx
+#endif
 
 #if defined(_WIN64)
   movdqu %xmm0,176(PTR)
@@ -174,6 +218,12 @@ DEFINE_LIBUNWIND_FUNCTION(__unw_getcontext)
 #endif
   xorl  %eax, %eax    # return UNW_ESUCCESS
   ret
+#if defined(X86_64_BASE_SYSCALL)
+9:
+  popq %rbx
+  movl $-6540,%eax # UNW_EUNSPEC: segment base capture failed
+  ret
+#endif
 
 #elif defined(__mips__) && defined(_ABIO32) && _MIPS_SIM == _ABIO32
 
diff --git a/libunwind/src/assembly.h b/libunwind/src/assembly.h
index 84c9d526f1d759..a36d2669eaa500 100644
--- a/libunwind/src/assembly.h
+++ b/libunwind/src/assembly.h
@@ -15,6 +15,24 @@
 #ifndef UNWIND_ASSEMBLY_H
 #define UNWIND_ASSEMBLY_H
 
+#if defined(__x86_64__) && !defined(_LIBUNWIND_IS_BAREMETAL) &&                \
+    (defined(__linux__) || defined(__FreeBSD__))
+#include <sys/syscall.h>
+#if defined(__linux__)
+#include <asm/prctl.h>
+#define X86_64_GET_FSBASE ARCH_GET_FS
+#define X86_64_GET_GSBASE ARCH_GET_GS
+#define X86_64_SET_FSBASE ARCH_SET_FS
+#define X86_64_SET_GSBASE ARCH_SET_GS
+#define X86_64_BASE_SYSCALL SYS_arch_prctl
+#else
+// <machine/sysarch.h> contains C declarations and cannot be included here.
+#define X86_64_GET_FSBASE 128 // AMD64_GET_FSBASE
+#define X86_64_GET_GSBASE 130 // AMD64_GET_GSBASE
+#define X86_64_BASE_SYSCALL SYS_sysarch
+#endif
+#endif
+
 #if defined(__CET__)
 #include <cet.h>
 #define _LIBUNWIND_CET_ENDBR _CET_ENDBR
diff --git a/libunwind/src/libunwind.cpp b/libunwind/src/libunwind.cpp
index 0b9d56ec805bca..430f7fa43cd692 100644
--- a/libunwind/src/libunwind.cpp
+++ b/libunwind/src/libunwind.cpp
@@ -16,6 +16,11 @@
 
 #include <stdlib.h>
 
+#if defined(__FreeBSD__) && defined(__x86_64__) &&                             \
+    !defined(_LIBUNWIND_IS_BAREMETAL)
+#include <ucontext.h>
+#endif
+
 // Define the __has_feature extension for compilers that do not support it so
 // that we can later check for the presence of ASan in a compiler-neutral way.
 #if !defined(__has_feature)
@@ -32,6 +37,50 @@
 
 using namespace libunwind;
 
+#if defined(__FreeBSD__) && defined(__x86_64__) &&                             \
+    !defined(_LIBUNWIND_IS_BAREMETAL)
+// Changing FS/GS selectors can clear their bases, while sysarch base setters
+// replace the selectors. Let sigreturn restore the complete state atomically.
+extern "C" _LIBUNWIND_HIDDEN void
+__libunwind_Registers_x86_64_jumpto(Registers_x86_64 *registers) {
+  ucontext_t context;
+  if (getcontext(&context) != 0)
+    _LIBUNWIND_ABORT("getcontext failed");
+  mcontext_t &mc = context.uc_mcontext;
+#define RESTORE(reg, field) mc.field = registers->getRegister(UNW_X86_64_##reg)
+  RESTORE(RAX, mc_rax);
+  RESTORE(RBX, mc_rbx);
+  RESTORE(RCX, mc_rcx);
+  RESTORE(RDX, mc_rdx);
+  RESTORE(RDI, mc_rdi);
+  RESTORE(RSI, mc_rsi);
+  RESTORE(RBP, mc_rbp);
+  RESTORE(RSP, mc_rsp);
+  RESTORE(R8, mc_r8);
+  RESTORE(R9, mc_r9);
+  RESTORE(R10, mc_r10);
+  RESTORE(R11, mc_r11);
+  RESTORE(R12, mc_r12);
+  RESTORE(R13, mc_r13);
+  RESTORE(R14, mc_r14);
+  RESTORE(R15, mc_r15);
+  RESTORE(RIP, mc_rip);
+  RESTORE(RFLAGS, mc_rflags);
+  RESTORE(CS, mc_cs);
+  RESTORE(SS, mc_ss);
+  RESTORE(DS, mc_ds);
+  RESTORE(ES, mc_es);
+  RESTORE(FS, mc_fs);
+  RESTORE(GS, mc_gs);
+  RESTORE(FS_BASE, mc_fsbase);
+  RESTORE(GS_BASE, mc_gsbase);
+#undef RESTORE
+  mc.mc_flags |= _MC_HASSEGS | _MC_HASBASES;
+  setcontext(&context);
+  _LIBUNWIND_ABORT("setcontext failed");
+}
+#endif
+
 /// internal object to represent this process's address space
 LocalAddressSpace LocalAddressSpace::sThisAddressSpace;
 
diff --git a/libunwind/test/x86_64_dwarf_registers.pass.cpp b/libunwind/test/x86_64_dwarf_registers.pass.cpp
new file mode 100644
index 00000000000000..23ad6aad2d5721
--- /dev/null
+++ b/libunwind/test/x86_64_dwarf_registers.pass.cpp
@@ -0,0 +1,59 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: target={{x86_64-.+}}
+// UNSUPPORTED: target={{.*-windows.*}}
+// ADDITIONAL_COMPILE_FLAGS: -D_LIBUNWIND_IS_NATIVE_ONLY
+
+// Disable internal tracing, which depends on private library symbols.
+#ifndef NDEBUG
+#define NDEBUG
+#endif
+#include "../src/AddressSpace.hpp"
+#include "../src/DwarfParser.hpp"
+#undef NDEBUG
+#include <assert.h>
+
+using namespace libunwind;
+
+int main(int, char **) {
+  using Parser = CFI_Parser<LocalAddressSpace>;
+  LocalAddressSpace addressSpace;
+  Parser::CIE_Info cie = {};
+  cie.codeAlignFactor = 1;
+  cie.dataAlignFactor = -8;
+  Parser::FDE_Info fde = {};
+  const uint8_t instructions[] = {
+      DW_CFA_offset_extended, 49, 1, DW_CFA_offset_extended, 50, 2,
+      DW_CFA_offset_extended, 51, 3, DW_CFA_offset_extended, 52, 4,
+      DW_CFA_offset_extended, 53, 5, DW_CFA_offset_extended, 54, 6,
+      DW_CFA_offset_extended, 55, 7, DW_CFA_offset_extended, 58, 8,
+      DW_CFA_offset_extended, 59, 9};
+  fde.fdeInstructions = reinterpret_cast<uintptr_t>(instructions);
+  fde.fdeStart = fde.fdeInstructions;
+  fde.fdeLength = sizeof(instructions);
+  Parser::PrologInfo prolog;
+  assert(Parser::parseFDEInstructions<Registers_x86_64>(
+      addressSpace, fde, cie, 1, REGISTERS_X86_64, &prolog));
+  const int regs[] = {49, 50, 51, 52, 53, 54, 55, 58, 59};
+  Registers_x86_64 registers;
+  for (unsigned i = 0; i != sizeof(regs) / sizeof(regs[0]); ++i) {
+    assert(prolog.savedRegisters[regs[i]].location == Parser::kRegisterInCFA);
+    assert(prolog.savedRegisters[regs[i]].value == -8 * int(i + 1));
+    // Also exercise the base fields on hosts without native base access.
+    registers.setRegister(regs[i], i + 1);
+    assert(registers.getRegister(regs[i]) == i + 1);
+  }
+  const uint8_t invalid[] = {DW_CFA_offset_extended, 60, 1};
+  fde.fdeInstructions = reinterpret_cast<uintptr_t>(invalid);
+  fde.fdeStart = fde.fdeInstructions;
+  fde.fdeLength = sizeof(invalid);
+  assert(!Parser::parseFDEInstructions<Registers_x86_64>(
+      addressSpace, fde, cie, 1, REGISTERS_X86_64, &prolog));
+  return 0;
+}
diff --git a/libunwind/test/x86_64_extended_registers.pass.cpp b/libunwind/test/x86_64_extended_registers.pass.cpp
new file mode 100644
index 00000000000000..416de21b40520e
--- /dev/null
+++ b/libunwind/test/x86_64_extended_registers.pass.cpp
@@ -0,0 +1,163 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: target={{x86_64-.+}}
+// UNSUPPORTED: target={{.*-windows.*}}, msan
+
+#undef NDEBUG
+#include <assert.h>
+#include <libunwind.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#if defined(__FreeBSD__)
+#include <machine/sysarch.h>
+#elif defined(__linux__)
+#include <asm/prctl.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+#endif
+
+#define STRINGIFY_IMPL(x) #x
+#define STRINGIFY(x) STRINGIFY_IMPL(x)
+#define SYMBOL(x) STRINGIFY(__USER_LABEL_PREFIX__) #x
+
+static const int selectors[] = {UNW_X86_64_ES, UNW_X86_64_CS, UNW_X86_64_SS,
+                                UNW_X86_64_DS, UNW_X86_64_FS, UNW_X86_64_GS};
+static const char *names[] = {"es", "cs", "ss", "ds", "fs", "gs"};
+
+static unw_word_t get(unw_cursor_t &cursor, int reg) {
+  unw_word_t value;
+  assert(unw_get_reg(&cursor, reg, &value) == UNW_ESUCCESS);
+  return value;
+}
+
+// A tail call preserves the flags and stack pointer of our caller. In
+// particular, getcontext must capture CF before adjusting its saved SP.
+extern "C" __attribute__((naked)) int capture(unw_context_t *) {
+  __asm__("stc\n"
+          "jmp " SYMBOL(unw_getcontext) "\n");
+}
+
+static void test_capture_and_access() {
+  unw_context_t context;
+  // Resolve the shared-library entry point before testing CF: lazy binding
+  // can change flags between capture's tail call and getcontext's entry.
+  assert(unw_getcontext(&context) == UNW_ESUCCESS);
+  memset(&context, 0xa5, sizeof(context));
+  assert(capture(&context) == UNW_ESUCCESS);
+  unw_cursor_t cursor;
+  assert(unw_init_local(&cursor, &context) == UNW_ESUCCESS);
+  assert(get(cursor, UNW_X86_64_RFLAGS) & 1);
+  assert(strcmp(unw_regname(&cursor, UNW_X86_64_RFLAGS), "rflags") == 0);
+
+  uint16_t actual[6];
+  __asm__ volatile("movw %%es, %0\n"
+                   "movw %%cs, %1\n"
+                   "movw %%ss, %2\n"
+                   "movw %%ds, %3\n"
+                   "movw %%fs, %4\n"
+                   "movw %%gs, %5\n"
+                   : "=m"(actual[0]), "=m"(actual[1]), "=m"(actual[2]),
+                     "=m"(actual[3]), "=m"(actual[4]), "=m"(actual[5]));
+  for (unsigned i = 0; i != 6; ++i) {
+    assert(get(cursor, selectors[i]) == actual[i]);
+    assert(strcmp(unw_regname(&cursor, selectors[i]), names[i]) == 0);
+    assert(unw_set_reg(&cursor, selectors[i], 0x12345678) == UNW_ESUCCESS);
+    assert(get(cursor, selectors[i]) == 0x5678);
+  }
+
+#if defined(__FreeBSD__) || defined(__linux__)
+  uintptr_t fs, gs;
+#if defined(__FreeBSD__)
+  assert(sysarch(AMD64_GET_FSBASE, &fs) == 0);
+  assert(sysarch(AMD64_GET_GSBASE, &gs) == 0);
+#else
+  assert(syscall(SYS_arch_prctl, ARCH_GET_FS, &fs) == 0);
+  assert(syscall(SYS_arch_prctl, ARCH_GET_GS, &gs) == 0);
+#endif
+  assert(get(cursor, UNW_X86_64_FS_BASE) == fs);
+  assert(get(cursor, UNW_X86_64_GS_BASE) == gs);
+  assert(strcmp(unw_regname(&cursor, UNW_X86_64_FS_BASE), "fs.base") == 0);
+  assert(strcmp(unw_regname(&cursor, UNW_X86_64_GS_BASE), "gs.base") == 0);
+  assert(unw_set_reg(&cursor, UNW_X86_64_FS_BASE, 0x12345678) == UNW_ESUCCESS);
+  assert(get(cursor, UNW_X86_64_FS_BASE) == 0x12345678);
+  assert(unw_set_reg(&cursor, UNW_X86_64_GS_BASE, 0x87654321) == UNW_ESUCCESS);
+  assert(get(cursor, UNW_X86_64_GS_BASE) == 0x87654321);
+#endif
+
+  unw_word_t value;
+  assert(unw_get_reg(&cursor, 56, &value) == UNW_EBADREG);
+  assert(unw_get_reg(&cursor, 57, &value) == UNW_EBADREG);
+  assert(unw_get_reg(&cursor, 60, &value) == UNW_EBADREG);
+}
+
+extern "C" void check_cfi() {
+  unw_context_t context;
+  unw_cursor_t cursor;
+  assert(unw_getcontext(&context) == UNW_ESUCCESS);
+  assert(unw_init_local(&cursor, &context) == UNW_ESUCCESS);
+  assert(unw_step(&cursor) > 0); // to the synthetic trampoline
+  assert(unw_step(&cursor) > 0); // recover its saved registers
+  assert(get(cursor, UNW_X86_64_RFLAGS) == 0x203);
+  for (unsigned i = 0; i != 6; ++i)
+    assert(get(cursor, selectors[i]) == unsigned(selectors[i]));
+#if defined(__FreeBSD__) || defined(__linux__)
+  assert(get(cursor, UNW_X86_64_FS_BASE) == 0x12345678);
+  assert(get(cursor, UNW_X86_64_GS_BASE) == 0x87654321);
+#endif
+}
+
+// Model the previously disabled FreeBSD sigtramp CFI, including the packed
+// 16-bit selectors whose adjacent data must not become part of their values.
+__attribute__((naked)) void trampoline() {
+  __asm__(".cfi_signal_frame\n"
+          "pushq %rbp\n"
+          ".cfi_def_cfa_offset 16\n"
+          ".cfi_offset 6, -16\n"
+          "movq %rsp, %rbp\n"
+          ".cfi_def_cfa_register 6\n"
+          "subq $80, %rsp\n"
+          "movq $0x203, -8(%rbp)\n"
+          ".cfi_offset 49, -24\n"
+          "movq $50, -16(%rbp)\n"
+          "movw $0xabcd, -14(%rbp)\n"
+          ".cfi_offset 50, -32\n"
+          "movq $51, -24(%rbp)\n"
+          ".cfi_offset 51, -40\n"
+          "movq $52, -32(%rbp)\n"
+          ".cfi_offset 52, -48\n"
+          "movq $53, -40(%rbp)\n"
+          "movw $0xabcd, -38(%rbp)\n"
+          ".cfi_offset 53, -56\n"
+          "movq $54, -48(%rbp)\n"
+          "movw $0xabcd, -46(%rbp)\n"
+          ".cfi_offset 54, -64\n"
+          "movq $55, -56(%rbp)\n"
+          "movw $0xabcd, -54(%rbp)\n"
+          ".cfi_offset 55, -72\n"
+#if defined(__FreeBSD__) || defined(__linux__)
+          "movq $0x12345678, -64(%rbp)\n"
+          ".cfi_offset 58, -80\n"
+          "movl $0x87654321, -72(%rbp)\n"
+          "movl $0, -68(%rbp)\n"
+          ".cfi_offset 59, -88\n"
+#endif
+          "call " SYMBOL(check_cfi) "\n"
+                                    "leave\n"
+                                    ".cfi_def_cfa 7, 8\n"
+                                    ".cfi_restore 6\n"
+                                    "ret\n");
+}
+
+int main(int, char **) {
+  test_capture_and_access();
+  trampoline();
+  return 0;
+}
diff --git a/libunwind/test/x86_64_getcontext_preserves_rbx.pass.cpp b/libunwind/test/x86_64_getcontext_preserves_rbx.pass.cpp
new file mode 100644
index 00000000000000..bd32883c7efafb
--- /dev/null
+++ b/libunwind/test/x86_64_getcontext_preserves_rbx.pass.cpp
@@ -0,0 +1,80 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: target={{x86_64-.+}}
+// UNSUPPORTED: target={{.*-windows.*}}, msan
+
+#undef NDEBUG
+#include <assert.h>
+#include <libunwind.h>
+#include <stdint.h>
+
+#if defined(__FreeBSD__)
+#include <machine/sysarch.h>
+#elif defined(__linux__)
+#include <asm/prctl.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+#endif
+
+#define STRINGIFY_IMPL(x) #x
+#define STRINGIFY(x) STRINGIFY_IMPL(x)
+#define SYMBOL(x) STRINGIFY(__USER_LABEL_PREFIX__) #x
+
+// FreeBSD may clear r8-r10 on syscall return. Context capture must retain its
+// destination across both base queries and preserve any callee-saved register
+// used for that purpose. Set RBX in assembly so the compiler cannot choose a
+// different register or hide a clobber by reloading its value.
+extern "C" __attribute__((naked)) int capture_with_rbx(unw_context_t *,
+                                                       uint64_t *, uint64_t) {
+  // clang-format off
+  __asm__("pushq %rbx\n"
+          "pushq %r12\n"
+          "subq $8, %rsp\n" // Align the stack before calling unw_getcontext.
+          "movq %rsi, %r12\n"
+          "movq %rdx, %rbx\n"
+          "call " SYMBOL(unw_getcontext) "\n"
+          "movq %rbx, (%r12)\n"
+          "addq $8, %rsp\n"
+          "popq %r12\n"
+          "popq %rbx\n"
+          "ret\n");
+  // clang-format on
+}
+
+static unw_word_t get(unw_cursor_t &cursor, int reg) {
+  unw_word_t value;
+  assert(unw_get_reg(&cursor, reg, &value) == UNW_ESUCCESS);
+  return value;
+}
+
+int main(int, char **) {
+  const uint64_t expected = UINT64_C(0x123456789abcdef0);
+  unw_context_t context;
+  uint64_t actual = 0;
+  assert(capture_with_rbx(&context, &actual, expected) == UNW_ESUCCESS);
+  assert(actual == expected);
+
+  unw_cursor_t cursor;
+  assert(unw_init_local(&cursor, &context) == UNW_ESUCCESS);
+  assert(get(cursor, UNW_X86_64_RBX) == expected);
+
+#if defined(__FreeBSD__) || defined(__linux__)
+  uintptr_t fs, gs;
+#if defined(__FreeBSD__)
+  assert(sysarch(AMD64_GET_FSBASE, &fs) == 0);
+  assert(sysarch(AMD64_GET_GSBASE, &gs) == 0);
+#else
+  assert(syscall(SYS_arch_prctl, ARCH_GET_FS, &fs) == 0);
+  assert(syscall(SYS_arch_prctl, ARCH_GET_GS, &gs) == 0);
+#endif
+  assert(get(cursor, UNW_X86_64_FS_BASE) == fs);
+  assert(get(cursor, UNW_X86_64_GS_BASE) == gs);
+#endif
+  return 0;
+}
diff --git a/libunwind/test/x86_64_resume_extended_registers.pass.cpp b/libunwind/test/x86_64_resume_extended_registers.pass.cpp
new file mode 100644
index 00000000000000..ea8419419cb535
--- /dev/null
+++ b/libunwind/test/x86_64_resume_extended_registers.pass.cpp
@@ -0,0 +1,158 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: target={{x86_64-.+}}
+// UNSUPPORTED: target={{.*-windows.*}}, msan
+
+#undef NDEBUG
+#include <assert.h>
+#include <libunwind.h>
+#include <stdint.h>
+#include <stdlib.h>
+
+#if defined(__FreeBSD__)
+#include <machine/sysarch.h>
+#include <sys/syscall.h>
+#elif defined(__linux__)
+#include <asm/prctl.h>
+#include <sys/syscall.h>
+#endif
+
+#define STRINGIFY_IMPL(x) #x
+#define STRINGIFY(x) STRINGIFY_IMPL(x)
+#define SYMBOL(x) STRINGIFY(__USER_LABEL_PREFIX__) #x
+
+struct Snapshot {
+  uint64_t flags;
+  uint16_t selectors[6];
+  uint64_t fsWord, gsWord;
+  uint64_t oldFS, oldGS;
+  uint64_t rcx;
+};
+static Snapshot snapshot;
+static uint16_t expectedSelectors[6];
+#if defined(__FreeBSD__) || defined(__linux__)
+static uint64_t fsWord = 0x123456789abcdef0;
+static uint64_t gsWord = 0xfedcba9876543210;
+#endif
+alignas(16) static char resumeStack[16384];
+
+extern "C" void verify_resume() {
+  assert(snapshot.flags & 1);
+  for (unsigned i = 0; i != 6; ++i)
+    assert(snapshot.selectors[i] == expectedSelectors[i]);
+  assert(snapshot.rcx == 0xabcdef);
+#if defined(__FreeBSD__) || defined(__linux__)
+  assert(snapshot.fsWord == fsWord);
+  assert(snapshot.gsWord == gsWord);
+#endif
+  _Exit(0);
+}
+
+// Record state before any flag-changing instruction or C call. Restore the
+// original TLS bases with raw syscalls before entering C again.
+extern "C" __attribute__((naked)) void resumed() {
+  __asm__(
+      "pushfq\n"
+      "popq %rax\n"
+      "movq %rax, 0(%r12)\n"
+      "movw %es, 8(%r12)\n"
+      "movw %cs, 10(%r12)\n"
+      "movw %ss, 12(%r12)\n"
+      "movw %ds, 14(%r12)\n"
+      "movw %fs, 16(%r12)\n"
+      "movw %gs, 18(%r12)\n"
+      "movq %rcx, 56(%r12)\n"
+#if defined(__FreeBSD__) || defined(__linux__)
+      "movq %fs:0, %rax\n"
+      "movq %rax, 24(%r12)\n"
+      "movq %gs:0, %rax\n"
+      "movq %rax, 32(%r12)\n"
+#if defined(__FreeBSD__)
+      "movq $" STRINGIFY(
+          SYS_sysarch) ", %rax\n"
+                       "movq $" STRINGIFY(
+                           AMD64_SET_FSBASE) ", %rdi\n"
+                                             "leaq 40(%r12), %rsi\n"
+                                             "syscall\n"
+                                             "jc 1f\n"
+                                             "movq $" STRINGIFY(
+                                                 SYS_sysarch) ", %rax\n"
+                                                              "movq "
+                                                              "$" STRINGIFY(
+                                                                  AMD64_SET_GSBASE) ", %rdi\n"
+                                                                                    "leaq 48(%r12), %rsi\n"
+                                                                                    "syscall\n"
+                                                                                    "jc 1f\n"
+#else
+      "movq $" STRINGIFY(
+          SYS_arch_prctl) ", %rax\n"
+                          "movq $" STRINGIFY(
+                              ARCH_SET_FS) ", %rdi\n"
+                                           "movq 40(%r12), %rsi\n"
+                                           "syscall\n"
+                                           "testq %rax, %rax\n"
+                                           "js 1f\n"
+                                           "movq $" STRINGIFY(
+                                               SYS_arch_prctl) ", %rax\n"
+                                                               "movq "
+                                                               "$" STRINGIFY(
+                                                                   ARCH_SET_GS) ", %rdi\n"
+                                                                                "movq 48(%r12), %rsi\n"
+                                                                                "syscall\n"
+                                                                                "testq %rax, %rax\n"
+                                                                                "js 1f\n"
+#endif
+#endif
+                                                                                    "jmp " SYMBOL(
+                                                                                        verify_resume) "\n"
+                                                                                                       "1: ud2\n");
+}
+
+static unw_word_t get(unw_cursor_t &cursor, int reg) {
+  unw_word_t value;
+  assert(unw_get_reg(&cursor, reg, &value) == UNW_ESUCCESS);
+  return value;
+}
+
+int main(int, char **) {
+  unw_context_t context;
+  unw_cursor_t cursor;
+  assert(unw_getcontext(&context) == UNW_ESUCCESS);
+  assert(unw_init_local(&cursor, &context) == UNW_ESUCCESS);
+  const int selectors[] = {UNW_X86_64_ES, UNW_X86_64_CS, UNW_X86_64_SS,
+                           UNW_X86_64_DS, UNW_X86_64_FS, UNW_X86_64_GS};
+  for (unsigned i = 0; i != 6; ++i)
+    expectedSelectors[i] = get(cursor, selectors[i]);
+  // SS names a writable data segment and can also be loaded into DS and ES.
+  expectedSelectors[0] = expectedSelectors[3] = expectedSelectors[2];
+  assert(unw_set_reg(&cursor, UNW_X86_64_ES, expectedSelectors[0]) ==
+         UNW_ESUCCESS);
+  assert(unw_set_reg(&cursor, UNW_X86_64_DS, expectedSelectors[3]) ==
+         UNW_ESUCCESS);
+  assert(unw_set_reg(&cursor, UNW_X86_64_RFLAGS,
+                     (get(cursor, UNW_X86_64_RFLAGS) & ~unw_word_t(0x400)) |
+                         1) == UNW_ESUCCESS);
+#if defined(__FreeBSD__) || defined(__linux__)
+  snapshot.oldFS = get(cursor, UNW_X86_64_FS_BASE);
+  snapshot.oldGS = get(cursor, UNW_X86_64_GS_BASE);
+  assert(unw_set_reg(&cursor, UNW_X86_64_FS_BASE, uintptr_t(&fsWord)) ==
+         UNW_ESUCCESS);
+  assert(unw_set_reg(&cursor, UNW_X86_64_GS_BASE, uintptr_t(&gsWord)) ==
+         UNW_ESUCCESS);
+#endif
+  assert(unw_set_reg(&cursor, UNW_X86_64_R12, uintptr_t(&snapshot)) ==
+         UNW_ESUCCESS);
+  assert(unw_set_reg(&cursor, UNW_X86_64_RCX, 0xabcdef) == UNW_ESUCCESS);
+  assert(unw_set_reg(&cursor, UNW_REG_SP,
+                     uintptr_t(resumeStack + sizeof(resumeStack) - 8)) ==
+         UNW_ESUCCESS);
+  assert(unw_set_reg(&cursor, UNW_REG_IP, uintptr_t(&resumed)) == UNW_ESUCCESS);
+  unw_resume(&cursor);
+  abort();
+}

>From bd7edb7a32d97112b57a7f17201988978f6811ed Mon Sep 17 00:00:00 2001
From: Minsoo Choo <minsoo at minsoo.io>
Date: Sat, 24 Oct 2026 19:15:43 -0400
Subject: [PATCH 2/2] fixup! Support extended amd64 segment registers

---
 libunwind/src/UnwindCursor.hpp                |  2 +
 libunwind/src/UnwindRegistersRestore.S        |  2 +
 libunwind/src/UnwindRegistersSave.S           |  2 +
 libunwind/src/libunwind.cpp                   |  7 ++-
 .../x86_64_resume_extended_registers.pass.cpp | 62 ++++++++-----------
 5 files changed, 38 insertions(+), 37 deletions(-)

diff --git a/libunwind/src/UnwindCursor.hpp b/libunwind/src/UnwindCursor.hpp
index 89fa854c826d6e..2fc554b390efc2 100644
--- a/libunwind/src/UnwindCursor.hpp
+++ b/libunwind/src/UnwindCursor.hpp
@@ -639,6 +639,8 @@ UnwindCursor<A, R>::UnwindCursor(unw_context_t *context, A &as)
   _msContext.R14 = r.getRegister(UNW_X86_64_R14);
   _msContext.R15 = r.getRegister(UNW_X86_64_R15);
   _msContext.Rip = r.getRegister(UNW_REG_IP);
+  // ARM64EC getcontext leaves these fields zero. Keep the values supplied by
+  // RtlCaptureContext instead of replacing them with uncaptured x86 state.
 #if !defined(__arm64ec__)
   _msContext.EFlags = static_cast<DWORD>(r.getRegister(UNW_X86_64_RFLAGS));
   _msContext.SegEs = static_cast<WORD>(r.getRegister(UNW_X86_64_ES));
diff --git a/libunwind/src/UnwindRegistersRestore.S b/libunwind/src/UnwindRegistersRestore.S
index defb1bafa6d42d..70fb44cf396a2b 100644
--- a/libunwind/src/UnwindRegistersRestore.S
+++ b/libunwind/src/UnwindRegistersRestore.S
@@ -72,6 +72,8 @@ DEFINE_LIBUNWIND_FUNCTION(__libunwind_Registers_x86_jumpto)
   # skip fs
   # skip gs
 
+// Native FreeBSD restores selectors and bases together through sigreturn in
+// libunwind.cpp; its sysarch base setters also replace the segment selectors.
 #elif defined(__x86_64__) && !defined(__arm64ec__) && \
     (!defined(__FreeBSD__) || defined(_LIBUNWIND_IS_BAREMETAL))
 .att_syntax
diff --git a/libunwind/src/UnwindRegistersSave.S b/libunwind/src/UnwindRegistersSave.S
index 0f87cccb34108a..7c582d85766547 100644
--- a/libunwind/src/UnwindRegistersSave.S
+++ b/libunwind/src/UnwindRegistersSave.S
@@ -178,6 +178,8 @@ DEFINE_LIBUNWIND_FUNCTION(__unw_getcontext)
   movq  $X86_64_GET_FSBASE,%rdi
   leaq  152(%rbx),%rsi
   syscall
+  # FreeBSD reports errors in CF with a positive errno in rax; Linux returns
+  # a negative errno in rax. Check the appropriate convention after each call.
 #if defined(__FreeBSD__)
   jc    9f
 #else
diff --git a/libunwind/src/libunwind.cpp b/libunwind/src/libunwind.cpp
index 430f7fa43cd692..29364a0fe2818f 100644
--- a/libunwind/src/libunwind.cpp
+++ b/libunwind/src/libunwind.cpp
@@ -18,6 +18,7 @@
 
 #if defined(__FreeBSD__) && defined(__x86_64__) &&                             \
     !defined(_LIBUNWIND_IS_BAREMETAL)
+#include <signal.h>
 #include <ucontext.h>
 #endif
 
@@ -41,6 +42,8 @@ using namespace libunwind;
     !defined(_LIBUNWIND_IS_BAREMETAL)
 // Changing FS/GS selectors can clear their bases, while sysarch base setters
 // replace the selectors. Let sigreturn restore the complete state atomically.
+// use sigreturn() over setcontext() as setcontext() intentionally leaves CS
+// unchanged.
 extern "C" _LIBUNWIND_HIDDEN void
 __libunwind_Registers_x86_64_jumpto(Registers_x86_64 *registers) {
   ucontext_t context;
@@ -76,8 +79,8 @@ __libunwind_Registers_x86_64_jumpto(Registers_x86_64 *registers) {
   RESTORE(GS_BASE, mc_gsbase);
 #undef RESTORE
   mc.mc_flags |= _MC_HASSEGS | _MC_HASBASES;
-  setcontext(&context);
-  _LIBUNWIND_ABORT("setcontext failed");
+  sigreturn(&context);
+  _LIBUNWIND_ABORT("sigreturn failed");
 }
 #endif
 
diff --git a/libunwind/test/x86_64_resume_extended_registers.pass.cpp b/libunwind/test/x86_64_resume_extended_registers.pass.cpp
index ea8419419cb535..c3c51eedf7d805 100644
--- a/libunwind/test/x86_64_resume_extended_registers.pass.cpp
+++ b/libunwind/test/x86_64_resume_extended_registers.pass.cpp
@@ -57,6 +57,8 @@ extern "C" void verify_resume() {
 // Record state before any flag-changing instruction or C call. Restore the
 // original TLS bases with raw syscalls before entering C again.
 extern "C" __attribute__((naked)) void resumed() {
+  // Keep one assembly instruction per line despite stringification macros.
+  // clang-format off
   __asm__(
       "pushfq\n"
       "popq %rax\n"
@@ -74,44 +76,34 @@ extern "C" __attribute__((naked)) void resumed() {
       "movq %gs:0, %rax\n"
       "movq %rax, 32(%r12)\n"
 #if defined(__FreeBSD__)
-      "movq $" STRINGIFY(
-          SYS_sysarch) ", %rax\n"
-                       "movq $" STRINGIFY(
-                           AMD64_SET_FSBASE) ", %rdi\n"
-                                             "leaq 40(%r12), %rsi\n"
-                                             "syscall\n"
-                                             "jc 1f\n"
-                                             "movq $" STRINGIFY(
-                                                 SYS_sysarch) ", %rax\n"
-                                                              "movq "
-                                                              "$" STRINGIFY(
-                                                                  AMD64_SET_GSBASE) ", %rdi\n"
-                                                                                    "leaq 48(%r12), %rsi\n"
-                                                                                    "syscall\n"
-                                                                                    "jc 1f\n"
+      "movq $" STRINGIFY(SYS_sysarch) ", %rax\n"
+      "movq $" STRINGIFY(AMD64_SET_FSBASE) ", %rdi\n"
+      "leaq 40(%r12), %rsi\n"
+      "syscall\n"
+      "jc 1f\n"
+      "movq $" STRINGIFY(SYS_sysarch) ", %rax\n"
+      "movq $" STRINGIFY(AMD64_SET_GSBASE) ", %rdi\n"
+      "leaq 48(%r12), %rsi\n"
+      "syscall\n"
+      "jc 1f\n"
 #else
-      "movq $" STRINGIFY(
-          SYS_arch_prctl) ", %rax\n"
-                          "movq $" STRINGIFY(
-                              ARCH_SET_FS) ", %rdi\n"
-                                           "movq 40(%r12), %rsi\n"
-                                           "syscall\n"
-                                           "testq %rax, %rax\n"
-                                           "js 1f\n"
-                                           "movq $" STRINGIFY(
-                                               SYS_arch_prctl) ", %rax\n"
-                                                               "movq "
-                                                               "$" STRINGIFY(
-                                                                   ARCH_SET_GS) ", %rdi\n"
-                                                                                "movq 48(%r12), %rsi\n"
-                                                                                "syscall\n"
-                                                                                "testq %rax, %rax\n"
-                                                                                "js 1f\n"
+      "movq $" STRINGIFY(SYS_arch_prctl) ", %rax\n"
+      "movq $" STRINGIFY(ARCH_SET_FS) ", %rdi\n"
+      "movq 40(%r12), %rsi\n"
+      "syscall\n"
+      "testq %rax, %rax\n"
+      "js 1f\n"
+      "movq $" STRINGIFY(SYS_arch_prctl) ", %rax\n"
+      "movq $" STRINGIFY(ARCH_SET_GS) ", %rdi\n"
+      "movq 48(%r12), %rsi\n"
+      "syscall\n"
+      "testq %rax, %rax\n"
+      "js 1f\n"
 #endif
 #endif
-                                                                                    "jmp " SYMBOL(
-                                                                                        verify_resume) "\n"
-                                                                                                       "1: ud2\n");
+      "jmp " SYMBOL(verify_resume) "\n"
+      "1: ud2\n");
+  // clang-format on
 }
 
 static unw_word_t get(unw_cursor_t &cursor, int reg) {



More information about the cfe-commits mailing list