libgo的协程是基于boost的fcontex接口。实现简单而且高效。理解汇编实现后,能够更好地理解协程以及协程间的切换。
1.汇编相关的基础知识(个人见解)
1)Directives
指示对编译器,链接器,调试器有用的结构信息
.file 源文件名
.data .file .string
.globl main :指明标签main是一个可以在其他模块中被访问的全局符号
2)标签
Tags: 可用于跳转 也可用作别名替换
以.开始的标签是编译器生成的临时局部标签
3)x86_64含有16个64为整数寄存器
%rsi,%rdi 用于字符串处理
%rsp,%rbp 栈相关,栈从高地址到低地址 %rsp ---> 栈顶,push和pop会改变 %rbp ---> 栈基址
%8~%15
4)操作的数据大小
B Byte
W Word 2Byte
L Long 4Byte
Q QuadWord 8Byte
5)函数的调用约定
a.整型参数依次存放在 %rdi,%rsi,%rdx,%rcx,%8,%9
b.浮点参数依次存放在%xmm0 - %xmm7中
c.寄存器不够用时,参数放到栈中
d.被调用的函数可以使用任何寄存器,但它必须保证%rbx,%rbp,%rsp,and %12-%15恢复到原来的值
e.返回值存放在%rax中
6)调用函数前
调用方要将参数放到寄存器中
然后把%10,%11的值保存到栈中
然后调用call 跳转到函数执行
返回后,恢复%10,%11
从%eax中取出返回值
2.jump_fcotext的实现
intptr_t jump_fcontext(fcontext_t * ofc,
fcontext_t nfc,
intptr_t vp,
bool preserve_fpu = false);
简单说明:调用时会将当前的上下文保存到ofc中.并切换到目标上下文nfc进行执行.
.text
.globl jump_fcontext //声明jump_fcontext为全局可见的符号
.type jump_fcontext,@function
.align 16
jump_fcontext:
//被调用的函数有责任保存这些寄存器
pushq %rbp /* save RBP */
pushq %rbx /* save RBX */
pushq %r15 /* save R15 */
pushq %r14 /* save R14 */
pushq %r13 /* save R13 */
pushq %r12 /* save R12 */
//rsp栈顶下移8字节 --->prepare stack for FPU 浮点运算寄存器
leaq -0x8(%rsp), %rsp
//整数参数依次存放在 %rdi,%rsi,%rdx,%rcx,%8,%9
cmp $0, %rcx //rcx:第四个参数 preserve_fpu
je 1f //先不考虑fpu相关
// 保存MXCSR内容 rsp 寄存器
stmxcsr (%rsp)
// 保存当前FPU状态字到 rsp+4 的位置
fnstcw 0x4(%rsp)
1:
movq %rsp, (%rdi) //rdi:fdcontext* ofc
//,当前的栈顶保存到ofc中 ----> 源上下文
movq %rsi, %rsp //rsi:fdcontext nfc ---> 目标上下文.
/* test for flag preserve_fpu */
cmp $0, %rcx
je 2f
/* restore MMX control- and status-word */
ldmxcsr (%rsp)
/* restore x87 control-word */
fldcw 0x4(%rsp)
2:
leaq 0x8(%rsp), %rsp //栈顶上移8字节 ----> prepare stack for FPU
//此时rsp保存着目标上下文
//恢复这些寄存器 ----> 恢复目标上下文的环境
popq %r12 /* restrore R12 */
popq %r13 /* restrore R13 */
popq %r14 /* restrore R14 */
popq %r15 /* restrore R15 */
popq %rbx /* restrore RBX */
popq %rbp /* restrore RBP */
popq %r8 //目标上下文切换之前的下一条指令地址,这个要结合make_fcontext
//用第三个参数作为返回值
movq %rdx,%rax //rax用作返回值
//将第三个参数作为目标上下文启动函数的第一参数 ---> 结合make_fcontext理解
movq %rdx, %rdi
jmp *%r8 //跳转到目标上下文的代码处执行
.size jump_fcontext,.-jump_fcontext
2.make_fcontext的实现
fcontext_t make_fcontext(void* stack,
size_t size,
fn_t fn);
简单说明:创建上下文:启动函数+执行栈
bl make_fcontext
.type make_fcontext,@function
.align 16
make_fcontext:
movq %rdi, %rax //rdi为第一参数: stack
andq $-16, %rax //将地址取为16的整数倍
//-16补码:0xfffffff0
//预留72字节的栈空间
/* reserve space for context-data on context-stack */
/* size for fc_mxcsr .. RIP + return-address for context-function */
/* on context-function entry: (RSP -0x8) % 16 == 0 */
leaq -0x48(%rax), %rax //以此时rax的地址为基点
//启动函数入口地址保存在0x38处
movq %rdx, 0x38(%rax) //rdx为第三参数: fn
//FPU相关
/* save MMX control- and status-word */
stmxcsr (%rax)
/* save x87 control-word */
fnstcw 0x4(%rax)
/* compute abs address of label finish */
leaq finish(%rip), %rcx
/* save address of finish as return-address for context-function */
/* will be entered after context-function returns */
movq %rcx, 0x40(%rax) //finish刚好位于启动函数上方 ---> 启动函数执行完以后就会执行finish处的代码
//而finish会调用_exit结束进程
ret //rax就是上述的基点.返回的类型为fcontext_t
finish:
/* exit code is zero */
xorq %rdi, %rdi
/* exit application */
call _exit@PLT
hlt
.size make_fcontext,.-make_fcontext
/* Mark that we don't need executable stack. */
.section .note.GNU-stack,"",%progbits
用一张图来帮助理解:

可见fcontxt_t类型值就是上下文的标识
为什么make_fcontext()需要预留72字节的栈空间呢?不预留行吗?
当然也行,但会增加jump_fcontext()的代码,即要判断目标上下文是刚初始化好(上下文环境为空)的呢,还是已经调用过jump_fcontext()(上下文环境不为空,含有rbp这些寄存器),根据判断结果来做出不同的行为
预留72字节的栈空间,营造假想的上下文环境,来统一jump_fcontext()的行为
注意点: finish会结束进程 ---> 若代码最终切换到make_fcontext()生成的上下文环境中,那么最后就会结束进程