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