协程的汇编实现
1 寄存器
X86-64有16个64位寄存器,分别是:%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp, %r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15
。其中:
%rax
作为函数返回值使用%rsp %rbp
栈指针寄存器,指向当前函数栈的栈顶和栈底%rdi,%rsi,%rdx,%rcx,%r8,%r9
用作函数参数,依次对应第1-6参数%rbx,%rbp,%r12,%r13,%r14,%r15
用作数据存储,遵循被调用者使用规则,简单说就是随便
用,调用子函数之前要备份它,以防他被修改%r10,%r11
用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值%rip
:用于存放下一条指令地址,CPU会取此寄存器的地址去找到下一条指令并执行。- 关于寄存器可以参考csapp中的图片:
2 函数调用
函数调用是从一个函数栈跳转到相邻的另一个函数栈。由于调用返回后还需恢复原函数栈的状态,因此必须在调用时通过寄存器和栈空间的配合来存储一些数据,方便调用完成后恢复。具有调用关系的两个函数栈空间一定是相邻的,也就是说主调方的函数栈空间与被调方的函数栈空间一点是相邻的。
add.c
int add(int a, int b){
return a+b;
}
int main(){
int a = 1;
int b = 2;
int x = add(a,b);
return 0;
}
add.s
.file "add.c"
.text
.globl add
.type add, @function
add:
.LFB0:
.cfi_startproc
pushq %rbp ;将main函数帧的起始位置压栈,记在add函数帧里
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp ;将add函数帧的栈顶位置赋给%rbp,没有像main修改%rsp
.cfi_def_cfa_register 6
movl %edi, -4(%rbp) ;第一个参数放入add函数帧
movl %esi, -8(%rbp) ;第二个参数放入add函数帧
movl -4(%rbp), %edx
movl -8(%rbp), %eax
addl %edx, %eax ;结果交给%eax做为返回值
popq %rbp ;%rbp恢复为main函数帧的起始位置
.cfi_def_cfa 7, 8
ret ;把main函数的返回地址赋值给%rip,下一步CPU会调到该地址的指令执行
.cfi_endproc
.LFE0:
.size add, .-add
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp ;%rsp下移16,为main函数栈预留空间
movl $1, -12(%rbp)
movl $2, -8(%rbp)
movl -8(%rbp), %edx
movl -12(%rbp), %eax
movl %edx, %esi
movl %eax, %edi
call add ;调用add函数,此步骤会将main函数的返回地址压入栈,然后将add函数入口地址赋值给%rip,下一步CPU就会执行到add函数
movl %eax, -4(%rbp)
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
3 gcc内联汇编
func.c
#include <stdio.h>
int func();
// func 的定义以汇编语言书写
__asm__(
".globl func \n\t"
".type func, @function \n\t"
"func: \n\t"
" movl $7, %eax \n\t"
" ret \n\t"
);
int main(void)
{
int n = func();
printf("n = %d\n", n);
return 0;
}
func.s
.file "func.c"
.text
#APP
.globl func ;内联汇编
.type func, @function
func:
movl $7, %eax
ret
.section .rodata
.LC0:
.string "n = %d\n"
#NO_APP
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $0, %eax
call func@PLT
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
movl %eax, %esi
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
4 协程栈的测试
coctx_swap_test.cpp
#include <cstdio>
#include <cstring>
#include <cstdlib>
struct coctx_t
{
void* rax; // 0
void* rbx; // 8
void* rcx; // 16
void* rdx; // 24
void* rsi; // 32
void* rdi; // 40
void* rbp; // 48
void* rsp; // 56
void* r8; // 64
void* r9; // 72
void* r12; // 80
void* r13; // 88
void* r14; // 96
void* r15; // 104
void* rip; // 112
size_t ss_size; // 120 协程栈大小
char* ss_sp; // 128 协程栈指针
};
extern "C"
{
void coctx_swap(coctx_t* /*rdi*/, coctx_t* /*rsi*/) asm("coctx_swap");
}
// %rdi %rsi 第一个和第二个参数
__asm__(
".global coctx_swap \n"
".type coctx_swap, @function \n"
"coctx_swap: \n"
" movq %rax, 0(%rdi) \n"
" movq %rbx, 8(%rdi) \n"
" movq %rcx, 16(%rdi) \n"
" movq %rdx, 24(%rdi) \n"
" movq %rsi, 32(%rdi) \n"
" movq %rdi, 40(%rdi) \n"
" movq %rbp, 48(%rdi) \n"
" movq %rsp, 56(%rdi) \n"
" movq %r8, 64(%rdi) \n"
" movq %r9, 72(%rdi) \n"
" movq %r12, 80(%rdi) \n"
" movq %r14, 96(%rdi) \n"
" movq %r15, 104(%rdi) \n"
" movq (%rsp), %rax \n"
" movq %rax, 112(%rdi) \n"
" xorq %rax, %rax \n"
" movq 0(%rsi), %rax \n"
" movq 8(%rsi), %rbx \n"
" movq 16(%rsi), %rcx \n"
" movq 24(%rsi), %rdx \n"
" movq 40(%rsi), %rdi \n"
" movq 48(%rsi), %rbp \n"
" movq 56(%rsi), %rsp \n"
" movq 64(%rsi), %r8 \n"
" movq 72(%rsi), %r9 \n"
" movq 80(%rsi), %r12 \n"
" movq 88(%rsi), %r13 \n"
" movq 96(%rsi), %r14 \n"
" movq 104(%rsi), %r15 \n"
" leaq 8(%rsp), %rsp \n"
" push 112(%rsi) \n"
" movq 32(%rsi), %rsi \n"
" ret \n"
);
coctx_t ctx_main;
coctx_t ctx;
void func()
{
printf("call func\n");
coctx_swap(&ctx, &ctx_main);
}
int main(void)
{
ctx.ss_size = 1024;
ctx.ss_sp = (char*)malloc(ctx.ss_size);
memset(ctx.ss_sp, 0, ctx.ss_size);
memset(&ctx, 0, 120);
ctx.rsp = ctx.ss_sp + ctx.ss_size;
ctx.rbp = ctx.rsp;
ctx.rip = (void*)func;
coctx_swap(&ctx_main, &ctx);
free(ctx.ss_sp);
return 0;
}
$ g++ coctx_swap_test.cpp
$ ./a.out
call func
-
当函数a调用函数b时,即call,会先将返回地址压栈,即将call后面的一个指令的地址压栈(%rsp - 8),然后再执行函数b,即PC指向函数b的第一个指令。
-
当函数b返回到函数a时,即ret,会先将返回地址出栈,即将call后面的指令地址赋值给PC(%rsp + 8)。