参考大牛博客:传送门
coctx_swap.S大概功能
分析文件:coctx_swap.S、coctx.h、coctx.cpp
协程的调度和线程是很类似的。也需要保存和恢复上下文,这就要牵扯到各种寄存器了,而牵扯到寄存器,就不得不使用汇编指令。coctx_swap.S文件内实现的功能是协程之间上下文保存和切换。
贴一遍代码:
.globl coctx_swap
#if !defined( __APPLE__ ) && !defined( __FreeBSD__ )
.type coctx_swap, @function
#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
简要看一下协程上下文(协程控制字)的定义
//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字节(下面要用到)
值得一提的是,regs的类型是void*类型,并不是说里面存储的是指针类型,而是为了开辟符合机器位长的空间(指针类型大小跟机器位长相同)
我们主要分析x86_64部分,对于x86_64来说,上下文有14个信息:
coctx.cpp | |
// 64 bit | |
| // | regs[0]: r15 | low |
// | 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 | //ret func addr 保存断点地址 | |
// | regs[10]: rdx | | |
// | regs[11]: rcx | | |
// | regs[12]: rbx | | |
// | regs[13]: rsp | hig |
.globl coctx_swap
#if !defined( __APPLE__ ) && !defined( __FreeBSD__ )
.type coctx_swap, @function
#endif
coctx_swap:
这部分是汇编函数声明,global的意思是使函数在其他文件可见。第三行是定义函数的格式。最后一行是函数入口标号
直接转到x86_64部分:
#elif defined(__x86_64__)
leaq 8(%rsp),%rax
leaq 112(%rdi),%rsp
注意,x86_64栈是 满递减结构。
lleaq 8(%rsp),%rax 翻译一下是: rax=rsp+8(取地址,不引用内存),sp是栈顶指针,函数调用进来之前已经保存了上一个函数的各种寄存器,至此原来的函数结束(栈帧结束),然后保存一个断点地址以便返回。于最后的断点地址。rsp当前指向断点地址,+8是栈后退一格就指向了原来的栈顶,所以rax保存了原来的栈顶。
说明一下rax默认是保存返回值,这里显然覆盖掉rax了,至于为什么可以不保存rax的现场?好吧,这个还没弄明白等以后补坑吧。
leaq 112(%rdi),%rsp翻译一下是rsp=rdi+112
x86_64中函数传参优先使用rdi,rsi,rdx,rcx,r8,r9,当这6个不够用的时候才会借用栈。所以rdi是第一个参数,是保存寄存器组内存的首地址。最后一个位置偏移了112=13*8个字节。这句话意思是让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
接下来是一系列入栈操作,注意刚刚rax被赋值为rsp+8,所以第一条语句的意思是保存上一个状态的栈顶指针。
pushq -8(%rax) //ret func addr这条语句:由于rax保存的rsp+8,函数调用后栈最后一个位置rsp是断点地址,所以rax-8=rsp指向的就是断点地址。
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
接下来截止到popq %rsp,就是恢复下一个协程的现场了。
第一句是让rsp指向下一个协程控制块保存寄存器组的内存地址。然后不断出栈恢复寄存器。rax保存的是断点地址。最后将rax断点地址压栈,为ret返回断点继续执行做准备。
至于倒数第二句 xorl %eax, %eax 意思是将eax第16位清零(b,w,l,q是操作属性限定符,分别表示1字节,2字节,4字节,8字节),本程序代码没有返回值,所以不是用作返回值。故推测这句话是为了安全考虑。
coctx.cpp大概功能
int coctx_init( coctx_t *ctx );函数负责清零,很简单就不说了。
int coctx_make( coctx_t *ctx,coctx_pfn_t pfn,const void *s,const void *s1 );
协程上下文初始化
分析文件:https://github.com/Tencent/libco/blob/master/coctx.cpp
贴一遍代码
#include "coctx.h"
#include <string.h>
#define ESP 0
#define EIP 1
#define EAX 2
#define ECX 3
// -----------
#define RSP 0
#define RIP 1
#define RBX 2
#define RDI 3
#define RSI 4
#define RBP 5
#define R12 6
#define R13 7
#define R14 8
#define R15 9
#define RDX 10
#define RCX 11
#define R8 12
#define R9 13
//----- --------
// 32 bit
// | regs[0]: ret |
// | regs[1]: ebx |
// | regs[2]: ecx |
// | regs[3]: edx |
// | regs[4]: edi |
// | regs[5]: esi |
// | regs[6]: ebp |
// | regs[7]: eax | = esp
enum
{
kEIP = 0,
kESP = 7,
};
//-------------
// 64 bit
//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 | //ret func addr
// | regs[10]: rdx |
// | regs[11]: rcx |
// | regs[12]: rbx |
//hig | regs[13]: rsp |
enum
{
kRDI = 7,
kRSI = 8,
kRETAddr = 9,
kRSP = 13,
};
//64 bit
extern "C"
{
extern void coctx_swap( coctx_t *,coctx_t* ) asm("coctx_swap");
};
#if defined(__i386__)
//略去若干代码
#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;
}
#endif
主要分析一下coctx_make:见注释
参数1是协程控制块地址。
参数2是,coctx_pfn_t定义如下:
typedef void* (*coctx_pfn_t)( void* s, void* s2 ); |
表示创建协程后要执行的第一个函数的函数指针,函数包含两个void*类型的参数,返回值为void* |
参数3和参数4是向上述pfn传递的两个参数。
int coctx_make( coctx_t *ctx,coctx_pfn_t pfn,const void *s,const void *s1 )
{
char *sp = ctx->ss_sp + ctx->ss_size; //计算栈顶地址,这里多加了一个寄存器单位也就是8字节
sp = (char*) ((unsigned long)sp & -16LL );//这里对第4位清零,使栈64位内存对齐。向下取整跟满递减搭配。
memset(ctx->regs, 0, sizeof(ctx->regs));
ctx->regs[ kRSP ] = sp - 8; //这里把多加的8扣回来,表示栈顶指针
ctx->regs[ kRETAddr] = (char*)pfn;//将返回地址初始化为用户创建协程时传入的开始函数地址,也就是从函数头开始执行。
ctx->regs[ kRDI ] = (char*)s; //函数原型设置为两个参数,s和s1即向函数传递的两个参数。
ctx->regs[ kRSI ] = (char*)s1;
return 0;
}