我们先搞清楚怎么切换协程。程序可以在某个地方暂停,跳转到另一个进程执行,在暂停的地方继续运行。如何实现?我们先来看一个例子。下面有两个函数。如果单线程输出结果是funcA1funcB1funcA2funcB2...,你会怎么做?voidfuncA(){inti=0;while(true){//做某事printf("funcA%d",i);我++;}}voidfuncB(){inti=0;while(true){//做某事printf("funcB%d",i);我++;}}从c代码来看,单线程如果跑到func1的while循环,怎么调用func2的while循环呢?必须使用跳跃。首先想到的是goto。goto可以实现跳转,goto不能实现函数间的跳转。无法满足此请求。即使可以在功能之间跳转,是否可行?那么就不得不说说C函数的调用过程了。详见这篇文章https://blog.csdn.net/jelly_9/article/details/53239718子程序或函数在所有语言中都是分层调用的,比如A调用B,B在执行过程中调用C,C执行完返回,B执行完返回,最后A完成执行。因此,子程序调用是通过栈实现的,子程序调用始终是一个入口和一个返回,调用顺序清晰。程序运行分为指令和数据两部分。栈中存储的数据由指令通过寄存器(rip)控制。两个函数内部的跳转必须保证栈是正确的,所以跳转前需要保存当前栈信息,然后再跳转。另外,我们还可以得到另外一个信息,在一个栈上实现多个进程的直接跳转是不可能的。所以需要多个栈来维护。那我们看看jump_fcontext是如何实现跳转的C语言函数声明intjump_fcontext(fcontext_t*ofc,fcontext_tnfc,void*vp,boolpreserve_fpu);汇编代码如下。text.globljump_fcontext.typejump_fcontext,@function.align16jump_fcontext:pushq%rbp/*保存RBP*/pushq%rbx/*保存RBX*/pushq%r15/*保存R15*/pushq%r14/*保存R14*/pushq%r13/*保存R13*/pushq%r12/*保存R12*//*为FPU准备堆栈*/leaq-0x8(%rsp),%rsp/*测试标志preserve_fpu*/cmp$0,%rcxje1f/*保存MMX控制字和状态字*/stmxcsr(%rsp)/*保存x87控制字*/fnstcw0x4(%rsp)1:/*在RDI中存储RSP(指向上下文数据)*/movq%rsp,(%rdi)/*从RSI恢复RSP(指向上下文数据)*/movq%rsi,%rsp/*测试标志preserve_fpu*/cmp$0,%rcxje2f/*恢复MMX控制-和状态字*/ldmxcsr(%rsp)/*恢复x87控制字*/fldcw0x4(%rsp)2:/*为FPU准备堆栈*/leaq0x8(%rsp),%rsppopq%r12/*恢复R12*/popq%r13/*恢复R13*/popq%r14/*恢复R14*/popq%r15/*恢复R15*/popq%rbx/*恢复RBX*/popq%rbp/*恢复RBP*//*恢复返回地址*/popq%r8/*使用第三个参数作为跳转后的返回值*/movq%rdx,%rax/*在上下文函数中使用第三个参数作为第一个参数*/movq%rdx,%rdi/*间接跳转到上下文*/jmp*%r8.sizejump_fcontext,.-jump_fcontext/*标记我们不需要可执行堆栈。*/.section.note.GNU-stack,"",%progbits寄存器的用途可以先了解一下https://www.jianshu.com/p/571...1.保存寄存器pushq%rbp/*保存RBP*/pushq%rbx/*保存RBX*/pushq%r15/*保存R15*/pushq%r14/*保存R14*/pushq%r13/*保存R13*/pushq%r12/*保存R12*/"calledfunction必须保证rbprbxr12~r15这几个寄存器的值在进入和退出函数前后一致。”rbx是基地址寄存器的起始地址,存放存储区。rbp(basepointer)基地址指针寄存器由调用者保存,用于提供一个单元在栈中的偏移地址,与rss段寄存器配合使用,可以访问堆对于栈中的任意一个存储单元,被调用者保存2并预留8个字节的fpu空间/*preparestackforFPU*/leaq-0x8(%rsp),%rsp表示%rsp中的内容减8,由于栈从高到低,表示预留8个字节的栈空间。FPU:(FloatPointUnit,浮点单元)3.判断是否保存fpucmp$0,%rcxje1frcx为第四个参数,判断是否等于0,如果为0则跳转到1标记的位置。即preserve_fpu。当preserve_fpu=true时,需要执行两条指令将浮点运算的两个32位寄存器数据保存到步骤2中预留的8字节空间。/*saveMMXcontrol-andstatus-word*/stmxcsr(%rsp)/*savex87control-word*/fnstcw0x4(%rsp)4.修改rsp此时已经改成其他栈,将rsp保存到第一个参数(第一个参数存放在内存中由rdi指向)。fcontext_t*ofcrsp的指针存放在第一个参数ofc指向的内存中。第二条指令实现复制第二个参数到rsp.1:/*storeRSP(pointtocontext-data)inRDI*/movq%rsp,(%rdi)/*restoreRSP(pointtocontext-data)inRDI*/movq%rsp,(%rdi)/*从RSI*/movq%rsi,%rsp5恢复RSP(指向上下文数据)数据。判断fpu是否保存,如果保存,则将保存在nfx栈中的fpu相关数据恢复到对应的寄存器中。/*测试标志preserve_fpu*/cmp$0,%rcxje2f/*恢复MMX控制字和状态字*/ldmxcsr(%rsp)/*恢复x87控制字*/fldcw0x4(%rsp)6,设置rsp存储地址+8(8字节fpu),将栈中的数据按顺序恢复到寄存器中。2:/*为FPU准备堆栈*/leaq0x8(%rsp),%rsppopq%r12/*恢复R12*/popq%r13/*恢复R13*/popq%r14/*恢复R14*/popq%r15/*恢复R15*/popq%rbx/*恢复RBX*/popq%rbp/*恢复RBP*/7.设置返回值实现指令跳转。接下来继续pop数据,栈里面存的是什么,c函数调用一文可以知道,调用的时候会把rip(指令寄存器)存入栈。所以此时POP的数据就是rip,也就是下一条指令。这是nfx栈保存的下一条指令,所以这是另一个协程的下一条指令。保存到r8。最后,跳转到下一条指令并恢复到另一个协程运行jmp*%r8。movq%rdx,%rax是将上一个协程A的jump_fcontext的第三个参数作为当前协程B的jump_fcontext的返回值,可以实现两个协程之间的直接数据传递。movq%rdx,%rdi如果跳转到一个新的协程,在协程B启动的入口处使用第三个参数作为voidfunc(intparam)的第一个参数。/*恢复返回地址*/popq%r8/*使用第三个参数作为跳转后的返回值*/movq%rdx,%rax/*使用第三个参数作为上下文函数中的第一个参数*/movq%rdx,%rdi/*间接跳转到上下文*/jmp*%r8了解了程序是如何跳转的,我们来看看如何创建协程栈。make_fcontextc语言语言数显fcontext_tmake_fcontext(voidsp,size_tsize,void(fn)(int));.text.globlmake_fcontext.typemake_fcontext,@function.align16make_fcontext:/*firstargofmake_fcontext()==topofcontext-stack*/movq%rdi,%rax/*将RAX中的地址移位到低16字节边界*/andq$-16,%rax/*为上下文堆栈上的上下文数据保留空间*//*fc_mxcsr的大小。.RIP+上下文函数的返回地址*//*在上下文函数入口:(RSP-0x8)%16==0*/leaq-0x48(%rax),%rax/*make_fcontext()的第三个参数==上下文函数的地址*/movq%rdx,0x38(%rax)/*保存MMX控制字和状态字*/stmxcsr(%rax)/*保存x87控制字*/fnstcw0x4(%rax)/*计算标签finish的abs地址*/leaqfinish(%rip),%rcx/*将finish的地址保存为上下文函数的返回地址*//*将在上下文函数返回后输入*/movq%rcx,0x40(%rax)ret/*返回指向上下文数据的指针*/finish:/*退出代码为零*/xorq%rdi,%rdi/*退出应用程序*/call_exit@PLThlt.sizemake_fcontext,.-make_fcontext/*标记我们不需要可执行堆栈。*/.section.note.GNU-stack,"",%progbits1,第一个参数为程序申请的内存地址的高位(栈从高到低),第一个参数放在rax,地址取16的整数倍和q$-16,%rax表示低4位取0。-16的补码表示为0xfffffffff0./*make_fcontext()的第一个arg==topofcontext-stack*/movq%rdi,%rax/*shiftaddressinRAXtolower16byteboundary*/andq$-16,%rax2,预留72字节栈空间,保存第三个参数(void(*fn)(int)函数指针)在当前偏移量0x38(大小为8个字节)处。/*为上下文堆栈上的上下文数据保留空间*//*fc_mxcsr的大小..RIP+上下文函数的返回地址*//*在上下文函数入口上:(RSP-0x8)%16==0*/leaq-0x48(%rax),%rax/*thirdargofmake_fcontext()==addressofcontext-function*/movq%rdx,0x38(%rax)3.保存fpu和jump_fcontext与总大小相似8个字节。/*saveMMXcontrol-andstatus-word*/stmxcsr(%rax)/*savex87control-word*/fnstcw0x4(%rax)4.计算finish的绝对地址,保存到栈的0x40位置.leaqfinish(%rip),%rcx表示finish是一个相对位置+rip是finish函数的地址。/*计算标签finish的abs地址*/leaqfinish(%rip),%rcx/*将finish的地址保存为上下文函数的返回地址*//*将在上下文函数返回后输入*/movq%rcx,0x40(%rax)5.返回,rax作为返回值,当前指针可以作为新栈的栈顶,相当于rspret/*returnpointertocontext-data*/看看返回并查看为什么72个字符是保留节大小。首先知道jump_fcontext在新栈中需要一个pop大小,fpu(8字节)+rbprbxr12~r15(8*6=48字节)=56字节。会继续POPrip8bytes,所以可以看到第二步的movq%rdx,0x38(%rax)就是把rip保存到这个位置。目前是64字节。堆栈中还存储了什么?协程(fn函数)运行后,会退出并调用ret。其实就是POPtorip。所以保存函数指针的大小是8个字节。总共72个字节。make_fcontext创建协同程序的堆栈。jump_fcontext实现跳转。李乐的网校内训视频https://biglive.xueersi.com/L...
