协程基础 自实现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位说明
累加寄存器axeaxrax
基址寄存器bxebxrbx
计数寄存器cxecxrcx
数据寄存器dxedxrdx
目的变址寄存器diedirdi
堆栈基指针bpebprbp存放栈底指针
源变址寄存器siesirsi
堆栈顶指针spesprsp存放栈顶指针
指令ipeiprip存放下一条指令

而这些就是协程运行的上下文。由于这个例子是基于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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值