C++17 Windows x64实现栈式协程时抛异常致程序终止问题
Windows x64 C++17 栈式协程异常抛出崩溃问题
问题现象
在Windows x64平台基于C++17实现栈式协程时,协程内部抛出异常就会导致程序立刻以错误退出码终止,即使异常在抛出点所在函数内被立即捕获、完全不会逃逸出协程执行范围,也会触发该崩溃。
现有实现逻辑
协程栈分配
首先通过如下代码为新协程分配独立栈空间:
void* Allocate() { static constexpr std::size_t kStackSize{524'288}; auto new_stack{::operator new(kStackSize)}; return static_cast<std::byte *>(new_stack) + kStackSize; }
跳板函数(Trampoline)初始化
使用MSVC编译器,通过MASM编写跳板初始化逻辑:
SetTrampoline PROC mov rax, rsp ; 保存当前栈指针 mov rsp, [rcx] ; 切换到新栈指针 sub rsp, 20h ; 预留shadow stack空间 push rdx ; 保存传入的目标函数指针 ; 预留非易失寄存器保存空间 sub rsp, 0e0h mov [rcx], rsp ; 保存调整后的新栈指针 mov rsp, rax ; 切回原栈指针 ret SetTrampoline ENDP
上下文切换逻辑
参考MSVC x64调用约定实现上下文切换,完成协程之间的执行流跳转:
SwitchContext PROC ; 将调用方的非易失寄存器保存到栈上 push rbx push rbp push rdi push rsi push r12 push r13 push r14 push r15 sub rsp, 10h movdqu [rsp], xmm6 ; 此处省略xmm7-xmm14的入栈保存逻辑 sub rsp, 10h movdqu [rsp], xmm15 mov [rdx], rsp ; 保存调用方栈指针 SwitchContextFinally PROC mov rsp, [rcx] ; 切换到被调用方的栈指针 ; 恢复被调用方的寄存器值 movdqu xmm15, [rsp] add rsp, 10h ; 此处省略xmm7-xmm14的出栈恢复逻辑 movdqu xmm6, [rsp] add rsp, 10h pop r15 pop r14 pop r13 pop r12 pop rsi pop rdi pop rbp pop rbx ret SwitchContextFinally ENDP SwitchContext ENDP
问题根因
崩溃核心原因有3个,和异常是否逃逸出协程范围无关:
- SEH栈校验失败:MSVC的C++异常完全基于Windows原生结构化异常处理(SEH)实现,异常抛出时系统会首先校验当前RSP是否落在当前线程TEB(线程环境块)中记录的合法栈区间(StackBase/StackLimit)内,同时会遍历栈上的SEH注册节点完成栈展开。你直接通过
operator new分配的内存是普通堆内存,既没有被记录到TEB的栈区间信息中,也没有初始化对应的SEH节点,异常抛出的瞬间系统判定栈非法,直接终止进程,根本不会执行后续的catch逻辑。 - 栈对齐不符合调用约定:MSVC x64要求通过
call指令调用目标函数前,RSP必须保持16字节对齐。你当前的栈偏移计算逻辑没有满足该要求,哪怕异常问题修复,调用包含SIMD指令的函数时也会出现随机崩溃。 - shadow space预留位置错误:你确实需要为被调用函数预留32字节的shadow space(对应调用约定中RCX/RDX/R8/R9四个寄存器参数的保存空间),但当前预留位置和后续栈偏移计算没有配合对齐要求,属于无效预留。
修复方案
- 不要直接用堆内存裸分配协程栈,优先使用系统提供的
CreateFiberAPI完成协程栈的初始化。Fiber是Windows原生支持的用户态协程机制,会自动完成栈区间注册、SEH链初始化的全部工作,你只需要在Fiber的基础上封装自己的上下文切换逻辑即可,不需要手动处理TEB、SEH相关的底层细节,能避免90%以上的平台相关坑。 - 重新计算栈偏移量,保证所有C++函数调用点的RSP满足16字节对齐要求。
- 补全SwitchContext中省略的xmm7-xmm14寄存器保存/恢复逻辑,避免上下文切换时非易失寄存器值错乱。
内容的提问来源于stack exchange,提问作者Denis319199
相关产品推荐
相关产品推荐

