本文件进行协程切换,把协程运行时的上下文信息在寄存器和栈之间传输。栈空间是向下增长的。AT&T格式的汇编和intel格式的汇编不同,王爽书中写的是intel格式的汇编,gcc反汇编是AT&T格式的。在C语言和现代CPU的设计规范中,栈帧是一块由栈分配的内存块,在运行时,每当调用一次函数,都要存储其自动变量。因此对于同一函数的递归调用在每一次都会连续的获得自己独立的栈帧。函数的栈帧是指esp和ebp之间的一块地址。因此如果一个函数把更多的值压入堆栈,实际上是在扩展它本身的栈帧。
(1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
(2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
栈帧就是esp和ebp之间的一块内存,看一下一个栈帧的实际布局:
调用一个函数时,先将堆栈原先的基址(EBP)入栈,以保存之前任务的信息。然后将栈顶指针ESP的值赋给EBP,将之前的栈顶作为新的基址(栈底),然后再这个基址上开辟相应的空间用作被调用函数的堆栈。函数返回后,从EBP中可取出之前的ESP值,使栈顶恢复函数调用前的位置;再从恢复后的栈顶可弹出之前的EBP值,因为这个值在函数调用前一步被压入堆栈。这样,EBP和ESP就都恢复了调用前的位置,堆栈恢复函数调用前的状态。
coctx_swap.S
%rdi,%rsi分别是调用此汇编代码时的参数1和参数2,在coctx_swap(&(curr->ctx),&(pending_co->ctx) )执行时,参数curr->ctx地址入rdi,pending_co->ctx地址入rsi。rsp是栈顶寄存器。x86_64中函数传参优先使用rdi,rsi,rdx,rcx,r8,r9,当这6个不够用的时候才会借用栈。
.globl coctx_swap // 让符号对链接器可见
#if !defined( __APPLE__ ) && !defined( __FreeBSD__ )
.type coctx_swap, @function // 告诉GNU汇编器,coctx_swap是个函数
#endif
coctx_swap: // 函数入口
#if defined(__i386__)
//...............
#elif defined(__x86_64__)
leaq 8(%rsp),%rax
leaq 112(%rdi),%rsp
pushq %rax
pushq %rbx
pushq %rcx
pushq %rdx
pushq -8(%rax) //ret func addr
pushq %rsi
pushq %rdi
pushq %rbp
pushq %r8
pushq %r9
pushq %r12
pushq %r13
pushq %r14
pushq %r15
movq %rsi, %rsp
popq %r15
popq %r14
popq %r13
popq %r12
popq %r9
popq %r8
popq %rbp
popq %rdi
popq %rsi
popq %rax //ret func addr
popq %rdx
popq %rcx
popq %rbx
popq %rsp
pushq %rax
xorl %eax, %eax
ret
#endif
以上代码可分为两部分:切入(恢复上下文信息,即从栈pop到寄存器)切出(保存寄存器的上下文信息,push进栈)。
前半部分(切出过程):
leaq 8(%rsp),%rax //将rsp的上一个地址保存到%rax上,就是把当前的%rsp地址保存到%rax上,
//后面会移动%rsp把寄存器信息保存到regs数组中,%rsp是栈顶寄存器,通过移动它可以指定栈帧地址。
leaq 112(%rdi),%rsp //%rsp把curr->ctx加112个字节偏移量处的地址给rsp,
//112是sizeof(void*)(=8)*14,即regs[14]
pushq %rax //后面的pushq就是把寄存器中的内容保存到regs数组中
pushq %rbx
pushq %rcx
pushq %rdx
pushq -8(%rax) //ret func addr
pushq %rsi
pushq %rdi
pushq %rbp
pushq %r8
pushq %r9
pushq %r12
pushq %r13
pushq %r14
pushq %r15
后半部分(切入过程)
rsi为第二个参数,rsp指向新的栈空间即regs[0],以上实现把regs中的原有内容popq到各寄存器中。
movq %rsi, %rsp
popq %r15
popq %r14
popq %r13
popq %r12
popq %r9
popq %r8
popq %rbp
popq %rdi
popq %rsi
popq %rax //ret func addr
popq %rdx
popq %rcx
popq %rbx
popq %rsp
pushq %rax //将返回地址压入栈中
xorl %eax, %eax //清0rax寄存器的低32位
ret //将栈顶的返回地址弹出给rip寄存器
reg数组对应寄存器的值
enum
{
kRDI = 7,
kRSI = 8,
kRETAddr = 9,
kRSP = 13,
};
//low | regs[0]: r15 |
// | regs[1]: r14 |
// | regs[2]: r13 |
// | regs[3]: r12 |
// | regs[4]: r9 |
// | regs[5]: r8 |
// | regs[6]: rbp |
// | regs[7]: rdi |
// | regs[8]: rsi |
// | regs[9]: ret | //函数的返回地址
// | regs[10]: rdx |
// | regs[11]: rcx |
// | regs[12]: rbx |
//hig | regs[13]: rsp | //该值为上个栈帧在调用该函数前的值
协程上下文(协程控制字):
//coctx.h
struct coctx_t
{
#if defined(__i386__)
void *regs[ 8 ];
#else
void *regs[ 14 ];
#endif
size_t ss_size;//协程剩余大小。
char *ss_sp; //协程栈底,每个协程都有独立的栈空间 ,sp+size=栈顶指针
};
以上结构体(64位部分)主要包含了14个寄存器,每个寄存器是64位(8字节),所以寄存器组最后一个位置偏移112=13*8字节。
初始化函数:
#elif defined(__x86_64__)
int coctx_make( coctx_t *ctx,coctx_pfn_t pfn,const void *s,const void *s1 )
{
char *sp = ctx->ss_sp + ctx->ss_size;
sp = (char*) ((unsigned long)sp & -16LL );
memset(ctx->regs, 0, sizeof(ctx->regs));
ctx->regs[ kRSP ] = sp - 8;
ctx->regs[ kRETAddr] = (char*)pfn;
ctx->regs[ kRDI ] = (char*)s;
ctx->regs[ kRSI ] = (char*)s1;
return 0;
}
int coctx_init( coctx_t *ctx )
{
memset( ctx,0,sizeof(*ctx));
return 0;
}
coctx_init函数初始化ctx,全部赋值0。coctx_make初始化上下文,即相关寄存器。