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

x86-64架构下快速栈切换:最简实现代码咨询

嘿,这个问题问到点子上了——我之前折腾纤程实现的时候也踩过类似的坑,正好给你理清楚这里面的门道。

先给你拍板:仅交换rsp和rbp绝对不行

x86-64架构不管是Linux(System V调用约定)还是Windows(x64调用约定),都有非易失性寄存器的规则:这些寄存器的值在函数调用前后必须保持不变。比如Linux里的rbx、rbp、r12-r15,Windows还要多加上rsi和rdi。

如果你只切换栈指针rsp和基指针rbp,这些非易失性寄存器里的状态就丢了——比如当前纤程在某个函数里用rbx存了个关键指针,切换到另一个纤程后,这个指针被覆盖了,等切回来的时候程序直接就崩了。而且你还没保存rip(指令指针),切换回去根本不知道该从哪条指令继续执行。

x86-64下切换纤程的最简核心代码

要实现可靠的栈切换,核心是保存/恢复完整的上下文。下面分Linux和Windows给你最精简的实现:

第一步:定义纤程上下文结构体

先把需要保存的寄存器都装进去:

// Linux(System V调用约定)版本
typedef struct fiber_context {
    // 非易失性寄存器
    void *rbx;
    void *rbp;
    void *r12;
    void *r13;
    void *r14;
    void *r15;
    // 栈指针
    void *rsp;
    // 下次恢复时的指令地址
    void *rip;
} fiber_context_t;

// Windows(x64调用约定)版本,多了rsi/rdi
typedef struct fiber_context {
    void *rbx;
    void *rbp;
    void *rsi;
    void *rdi;
    void *r12;
    void *r13;
    void *r14;
    void *r15;
    void *rsp;
    void *rip;
} fiber_context_t;

第二步:实现栈切换的汇编函数

这是核心逻辑,用汇编直接操作寄存器和栈:

Linux版本(System V,参数用rdi/rsi传递)
.global fiber_switch
fiber_switch:
    // 1. 保存当前上下文到第一个参数(rdi指向当前纤程的context)
    mov [rdi + 0],  rbx
    mov [rdi + 8],  rbp
    mov [rdi + 16], r12
    mov [rdi + 24], r13
    mov [rdi + 32], r14
    mov [rdi + 40], r15
    mov [rdi + 48], rsp
    // 保存当前的返回地址(call指令把rip压栈了,栈顶就是返回地址)
    mov rax, [rsp]
    mov [rdi + 56], rax

    // 2. 加载目标纤程的上下文(第二个参数rsi指向目标context)
    mov rbx, [rsi + 0]
    mov rbp, [rsi + 8]
    mov r12, [rsi + 16]
    mov r13, [rsi + 24]
    mov r14, [rsi + 32]
    mov r15, [rsi + 40]
    mov rsp, [rsi + 48]
    mov rax, [rsi + 56]

    // 3. 跳转到目标纤程的上次暂停位置
    push rax
    ret
Windows版本(x64,参数用rcx/rdx传递)
.global fiber_switch
fiber_switch:
    // 保存当前上下文到rcx指向的context
    mov [rcx + 0],  rbx
    mov [rcx + 8],  rbp
    mov [rcx + 16], rsi
    mov [rcx + 24], rdi
    mov [rcx + 32], r12
    mov [rcx + 40], r13
    mov [rcx + 48], r14
    mov [rcx + 56], r15
    mov [rcx + 64], rsp
    mov rax, [rsp]
    mov [rcx + 72], rax

    // 加载目标上下文到rdx指向的context
    mov rbx, [rdx + 0]
    mov rbp, [rdx + 8]
    mov rsi, [rdx + 16]
    mov rdi, [rdx + 24]
    mov r12, [rdx + 32]
    mov r13, [rdx + 40]
    mov r14, [rdx + 48]
    mov r15, [rdx + 56]
    mov rsp, [rdx + 64]
    mov rax, [rdx + 72]

    // 跳转恢复执行
    push rax
    ret

第三步:初始化新纤程

创建新纤程时,得给它的栈做好初始化:

// 初始化纤程上下文,stack是栈的起始地址,stack_size是栈大小,entry是纤程入口函数
void fiber_init(fiber_context_t *ctx, void *stack, size_t stack_size, void (*entry)(void)) {
    // x86-64栈是向下生长的,所以栈顶是stack + stack_size
    void **sp = (void**)((char*)stack + stack_size);

    // 先压入一个假的返回地址(比如fiber_exit,防止纤程执行完后乱跳)
    *--sp = fiber_exit;
    // 压入纤程入口函数地址,第一次切换时会跳到这里
    *--sp = entry;

    // 初始化寄存器(第一次执行时还没用到的寄存器可以设为NULL)
    ctx->rbx = NULL;
    ctx->rbp = NULL;
#ifdef _WIN32
    ctx->rsi = NULL;
    ctx->rdi = NULL;
#endif
    ctx->r12 = NULL;
    ctx->r13 = NULL;
    ctx->r14 = NULL;
    ctx->r15 = NULL;
    ctx->rsp = sp;
    ctx->rip = entry;
}

// 纤程退出时的处理函数,这里可以切换回主纤程或者直接退出
void fiber_exit() {
    // 示例:切换回主纤程(假设main_ctx是全局的主纤程上下文)
    // fiber_switch(&current_fiber_ctx, &main_ctx);
    exit(0);
}

切换流程的完整逻辑

当你调用fiber_switch(&current_ctx, &target_ctx)时:

  1. 把当前纤程的所有非易失性寄存器、rsp、返回地址(rip)保存到current_ctx里
  2. 从target_ctx里加载所有寄存器和rsp
  3. 通过ret指令跳转到target_ctx里保存的rip,让目标纤程从上次暂停的地方继续执行

这样就能实现完整且可靠的纤程切换了,完全不需要依赖getcontext或者setjmp那套标准库函数。

内容的提问来源于stack exchange,提问作者Jesse Lactin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 06:49:46