协程基础 自实现setjmp和longjmp 进行上下文切换

setjmp和longjmp可以实现跨函数跳转,由于自由度非常高,所以一直被很多人诟病,以至于很多公司的规范中明令禁止使用这个两个函数,甚至goto也在内。
这次来实现它们,是为了下一步研究协程做准备,我们来看下,怎么做子过程的上下文切换。

1. 上下文的组成

%rax 作为函数返回值使用。
%rsp 栈指针寄存器,指向栈顶
%rdi,%rsi,%rdx,%rcx
用作函数参数,依次对应第1参数,第2参数。。。
%rbx,%rbp 用作数据存储
%rip 下一条指令
一个函数在执行的时候,某一时刻的状态,就主要靠这些寄存器来描述。同理,如果想恢复一个函数此前的状态,恢复这些寄存器即可。
数据结构

struct stack_ctx {
    
    
    unsigned long rax;
    unsigned long rbx;
    unsigned long rcx;
    unsigned long rdx;
    unsigned long rdi;
    unsigned long rsi;
    unsigned long rsp;
    unsigned long rbp;
    unsigned long rip;
};

总结一下

寄存器 16位 32位 64位 说明
累加寄存器 ax eax rax
基址寄存器 bx ebx rbx
计数寄存器 cx ecx rcx
数据寄存器 dx edx rdx
目的变址寄存器 di edi rdi
堆栈基指针 bp ebp rbp 存放栈底指针
源变址寄存器 si esi rsi
堆栈顶指针 sp esp rsp 存放栈顶指针
指令 ip eip rip 存放下一条指令

而这些就是协程运行的上下文。由于这个例子是基于64位的,所以我们使用了r前缀的变量。关于为什么是r,文末[3]中进行了讨论,r的意思是register,与r1,r2,r3等含义相同。

2. setjmp的实现

先熟悉下mov指令
movb 操作8位寄存器
movw 操作16位寄存器
movl 操作32位寄存器
movq 操作64位寄存器,这里我们使用的是64位寄存器,q Quad word,4的意思
movq rax,rbx;将rax寄存器的数据传送到rbx寄存器
%rdi 用作函数第一个参数,即ctx
所以这个函数的作用就是,把寄存器组中的值保存到ctx指向的内存中

void set_jmp(struct stack_ctx* ctx) {
    
    
    asm volatile(
        "movq %%rax,0(%%rdi)\n\t"
        "movq %%rbx,8(%%rdi)\n\t"
        "movq %%rcx,16(%%rdi)\n\t"
        "movq %%rdx,24(%%rdi)\n\t"
        "movq %%rdi,32(%%rdi)\n\t"
        "movq %%rsi,40(%%rdi)\n\t"
        "movq %%rbp,%%rbx\n\t"
        "add $16,%%rbx\n\t"
        "movq %%rbx,48(%%rdi)\n\t"
        "movq 0(%%rbp),%%rbx\n\t"
        "movq %%rbx,56(%%rdi)\n\t"
        "movq 8(%%rbp),%%rbx\n\t"
        "movq %%rbx,64(%%rdi)\n\t"
        :
        :);
}

另外, 为什么要用两个%,不是一个%?
其实内联汇编分两种,一种使用是基本格式,另一种使用带有C/C++表达式格式,在其最后会有冒号(:)存在。我们在这里使用的是后者,参考[6]

3. longjmp的实现

理解setjmp,那么逆操作longjmp就不难理解了。
%rdi 用作函数第一个参数,即ctx
所以这个函数的作用就是私用ctx所指向的内存数据恢复寄存器组

void long_jmp(struct stack_ctx* ctx) {
    
    
    asm volatile(
        "movq 0(%%rdi), %%rax\n\t"
        "movq 16(%%rdi), %%rcx\n\t"
        "movq 24(%%rdi), %%rdx\n\t"
        "movq 48(%%rdi), %%rsp\n\t"
        "movq 56(%%rdi), %%rbp\n\t"
        "movq 64(%%rdi), %%rbx\n\t"
        "pushq %%rbx\n\t"
        "movq 8(%%rdi), %%rbx\n\t"
        "movq 32(%%rdi), %%rdi\n\t"
        "movq 40(%%rdi), %%rsi\n\t"
        "ret\n\t"
        :
        :);
}

4. 测试

struct stack_ctx ctx = {
    
    0};
jmp_buf env;

void loop() {
    
    
    printf("---%s---\n", __func__);
    long_jmp(&ctx);
}
int g_count = 0;
int main(int argc, char const* argv[]) {
    
    
    set_jmp(env);
    if (++g_count > 3) {
    
    
        return 0;
    }
    loop();
    return 0;
}

Outputs:

---loop---
---loop---
---loop---

源码: https://github.com/cpptips/coroutines/blob/main/my_jmp.c

参考文章:
[0] c/c++ setjmp、longjmp实现,实现一个简单的协程
[1] X86 64 Register and Instruction Quick Start
[2] https://www.amd.com/system/files/TechDocs/40332.pdf
[3] https://softwareengineering.stackexchange.com/questions/127668/what-does-the-r-in-x64-register-names-stand-for
[4] x86-64 函数调用过程中寄存器的使用
[5] https://blog.csdn.net/luoyhang003/article/details/46786591
[6] https://blog.csdn.net/weixin_36071439/article/details/111967993

猜你喜欢

转载自blog.csdn.net/niu91/article/details/112106896