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(¤t_fiber_ctx, &main_ctx); exit(0); }
切换流程的完整逻辑
当你调用fiber_switch(¤t_ctx, &target_ctx)时:
- 把当前纤程的所有非易失性寄存器、
rsp、返回地址(rip)保存到current_ctx里 - 从
target_ctx里加载所有寄存器和rsp - 通过
ret指令跳转到target_ctx里保存的rip,让目标纤程从上次暂停的地方继续执行
这样就能实现完整且可靠的纤程切换了,完全不需要依赖getcontext或者setjmp那套标准库函数。
内容的提问来源于stack exchange,提问作者Jesse Lactin
相关产品推荐
相关产品推荐

