You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于longjmp的手动栈切换栈式协程崩溃问题及longjmp对主栈的依赖探究

基于longjmp的手动栈切换栈式协程崩溃问题及longjmp对主栈的依赖探究

我太懂你现在的挫败感了——手动抠寄存器切换栈实现栈式协程,结果不管是协程内部跳转还是跳回主栈,直接崩在ntdll的chkstk()里,用的还是MinGW GCC 14.2.0 x86_64。咱们先把你的代码摆出来,再一步步拆解根因:

#include <iostream>
#include <functional>
#include <cstdint>
#include <csetjmp>
#include <memory>
#include <cstdint>

struct coroutine_context {
    std::jmp_buf exit;
    std::size_t stack_memory_size = 0;
    std::unique_ptr<std::byte[]> stack_memory;
    std::function<void(coroutine_context&)> coroutine_body;
    int value_thoroughfare = 0;
};

#define SET_RSP(new_rsp) \
asm volatile ( \
    "mov %0, %%rsp" \
    : \
    : "r" (new_rsp) \
    : "memory" \
)

std::uintptr_t get_stack_base(const coroutine_context& context) {
    const static bool towards_address_LSB = []() -> bool {
        volatile int a;
        volatile int b;
        return &a > &b;
    }();
    if(towards_address_LSB)
        return (reinterpret_cast<std::uintptr_t>(context.stack_memory.get() + context.stack_memory_size) & (~0xF));
    else
        return (reinterpret_cast<std::uintptr_t>(context.stack_memory.get()) & (~0xF)) + 0x10;
}

#if 0
std::jmp_buf coro_jmp_buf;
void coro_test(coroutine_context& context) {
    if(setjmp(coro_jmp_buf) == 0) // Note: Even if the position where setjmp is called is within a coroutine, longjmp will crash after being called.
        std::longjmp(coro_jmp_buf, 1);
    context.value_thoroughfare = 114514;
    std::longjmp(context.exit, 1);
}
#else
void coro_test(coroutine_context& context) {
    context.value_thoroughfare = 114514;
    std::longjmp(context.exit, 1); // The program crashes here.
}
#endif

void create_coroutine_stack_frame(coroutine_context& context) {
    if(setjmp(context.exit)) return;
    SET_RSP(get_stack_base(context));
    coro_test(context);
}

int main() {
    coroutine_context context;
    context.stack_memory_size = 1024 * 16;
    context.stack_memory = std::make_unique<std::byte[]>(1024 * 16);
    context.coroutine_body = coro_test;
    create_coroutine_stack_frame(context);
    std::cout << context.value_thoroughfare;
    return 0;
}

一、崩溃的核心:Windows x64栈检查机制与手动栈的冲突

你遇到的chkstk()崩溃,本质是Windows x64线程的栈范围校验机制在搞事情:

  1. TEB的栈范围锁定:Windows x64每个线程的TEB(线程环境块,存在FS:0地址)里,有StackBase和StackLimit两个字段,记录了当前线程默认栈的上下边界。
  2. chkstk的强制检查:GCC在MinGW环境下,会给需要栈空间的函数(比如longjmp、有大局部变量的函数)插入chkstk()调用,用来检查当前RSP是否在TEB记录的栈范围内。如果不在,直接触发段错误。
  3. 手动栈的“非法身份”:你在堆上分配的协程栈,完全没更新TEB里的栈范围。当切换到协程栈后,RSP落在了主栈的StackLimit之外(因为主栈从高地址往低地址增长,堆分配的栈地址可能比主栈下限还低),chkstk()一检查就判定栈越界,直接崩了。

二、setjmp/longjmp的隐藏依赖:不止是寄存器

你可能以为setjmp只保存寄存器状态,但在MinGW的实现里,它还间接依赖当前的栈上下文:

  • jmp_buf里虽然没直接存TEB信息,但longjmp执行时,会假设当前栈和setjmp保存时的栈是同一个连续栈。
  • 当你跨栈(主栈→协程栈)使用longjmp时,longjmp内部的栈操作(比如建立自己的栈帧)会触发chkstk()检查,而此时RSP不在TEB的合法范围内,直接触发崩溃。

三、两种场景的具体崩溃拆解

场景1:协程内部的setjmp+longjmp(#if 0分支)

当你在协程栈上调用setjmp(coro_jmp_buf),再立刻longjmp回去:

  • setjmp保存的jmp_buf里,包含了协程栈的RSP和返回地址(这个返回地址是主栈上create_coroutine_stack_frame里调用coro_test的位置)。
  • longjmp恢复后,coro_test继续执行后续代码,但此时栈帧的上下文已经混乱:后续要返回的地址在主栈,而当前RSP在协程栈,函数返回时的栈清理操作会触发chkstk()检查,直接崩掉。

场景2:协程跳回主栈的longjmp(#else分支)

当你在协程里调用longjmp(context.exit)(context.exit是主栈上setjmp保存的):

  • 调用longjmp本身就需要在当前协程栈上建立栈帧,这个操作会触发chkstk()检查栈空间是否足够。
  • 此时TEB里的StackLimit还是主栈的下限,协程栈的RSP已经低于这个值,chkstk()判定栈越界,直接触发SIGSEGV。

四、解决思路

要解决这个问题,你需要让手动栈被Windows的栈机制“认可”,或者绕过栈检查:

  1. 临时禁用栈检查:编译时加-fno-stack-check选项,让GCC不插入chkstk()调用,这是最快验证根因的方法。
  2. 更新TEB的栈范围:通过Windows API修改当前线程的TEB中StackBase和StackLimit,让系统把堆分配的栈当成合法栈。你可以用NtSetInformationThread或者直接修改FS:0指向的TEB结构(注意权限和结构偏移,x64下TEB的StackBase偏移是0x8,StackLimit是0x10)。
  3. 用系统API分配合法栈:不要用std::unique_ptr直接分配,改用VirtualAlloc分配带保护页的栈,这样栈扩展时会自动触发页分配,同时更新TEB的栈范围(或者手动更新)。

结论

手动实现栈式协程在Windows MinGW环境下,不能只单纯切换RSP——必须处理Windows线程的栈上下文(TEB),否则chkstk()这类系统级栈检查会直接判定非法。setjmp/longjmp的跨栈使用,本质上是因为它们依赖当前线程的默认栈上下文,脱离这个上下文就会触发崩溃。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.07 12:54:29