Libco协程库实现(二) - Go语言中文社区

Libco协程库实现(二)


这里补充下libco后续对于协程间切换的汇编新实现,原来的实现方法之前分析过Libco协程库实现,早期分析的时候有一个地方写错。没有写具体debug信息及过程,应私信的网友要求,这里详细分析下整个过程并配上相关的数据。

这里先贴上老的实现x86_64:

 56 #elif defined(__x86_64__)
 57     leaq 8(%rsp),%rax
 58     leaq 112(%rdi),%rsp
 59     pushq %rax
 60     pushq %rbx
 61     pushq %rcx
 62     pushq %rdx
 63 
 64     pushq -8(%rax) //ret func addr
 65 
 66     pushq %rsi
 67     pushq %rdi
 68     pushq %rbp
 69     pushq %r8
 70     pushq %r9
 71     pushq %r12
 72     pushq %r13
 73     pushq %r14
 74     pushq %r15

 76     movq %rsi, %rsp
 77     popq %r15
 78     popq %r14
 79     popq %r13
 80     popq %r12
 81     popq %r9
 82     popq %r8
 83     popq %rbp
 84     popq %rdi
 85     popq %rsi
 86     popq %rax //ret func addr
 87     popq %rdx
 88     popq %rcx
 89     popq %rbx
 90     popq %rsp
 91     pushq %rax
 92 
 93     xorl %eax, %eax
 94     ret
 95 #endif

以上是保存要切出协程的寄存器上下文,和要切入协程的寄存器上下文,具体不再分析。

新的实现如下:

 48     leaq (%rsp),%rax
 49     movq %rax, 104(%rdi)
 50     movq %rbx, 96(%rdi)
 51     movq %rcx, 88(%rdi)
 52     movq %rdx, 80(%rdi)
 53       movq 0(%rax), %rax
 54       movq %rax, 72(%rdi)
 55     movq %rsi, 64(%rdi)
 56       movq %rdi, 56(%rdi)
 57     movq %rbp, 48(%rdi)
 58     movq %r8, 40(%rdi)
 59     movq %r9, 32(%rdi)
 60     movq %r12, 24(%rdi)
 61     movq %r13, 16(%rdi)
 62     movq %r14, 8(%rdi)
 63     movq %r15, (%rdi)
 64       xorq %rax, %rax
 65 
 66     movq 48(%rsi), %rbp
 67     movq 104(%rsi), %rsp
 68     movq (%rsi), %r15
 69     movq 8(%rsi), %r14
 70     movq 16(%rsi), %r13
 71     movq 24(%rsi), %r12
 72     movq 32(%rsi), %r9
 73     movq 40(%rsi), %r8
 74     movq 56(%rsi), %rdi
 75     movq 80(%rsi), %rdx
 76     movq 88(%rsi), %rcx
 77     movq 96(%rsi), %rbx
 78         leaq 8(%rsp), %rsp
 79         pushq 72(%rsi)
 80 
 81     movq 64(%rsi), %rsi
 82     ret

由于两者的区别从实现上看一个是pushq指令,一个是movq指令,而前者的实现大概如下:
pushq时,先将栈顶指针减8,再将值写到新栈顶地址,如:
   pushq %rbp
等价于:
   subq $8,%rsp
   movq %rbp,(%rsp)
这里因为知道要保存和恢复的寄存器,取哪个直接加上相对偏移量,可能这里为了省去subq这个步骤?

由于切换的时候并不算是真正的函数调用,所以汇编代码处并没有被调函数开始处的pushq %rbp的指令,最后返回函数的popq %rbp,在asm中手动保存这两个,保存和恢复的反汇编:

17079 coctx_swap:
17080 1000096c0:  48 8d 04 24     leaq    (%rsp), %rax
17081 1000096c4:  48 89 47 68     movq    %rax, 104(%rdi)//last rsp
17085 1000096d4:  48 8b 00    movq    (%rax), %rax
17086 1000096d7:  48 89 47 48     movq    %rax, 72(%rdi)//last rip
17089 1000096e3:  48 89 6f 30     movq    %rbp, 48(%rdi)//last rbp
17097 100009701:  48 8b 6e 30     movq    48(%rsi), %rbp//restore rbp
17098 100009705:  48 8b 66 68     movq    104(%rsi), %rsp//restore rsp
17109 100009730:  48 8d 64 24 08  leaq    8(%rsp), %rsp//skip old rip
17110 100009735:  ff 76 48    pushq   72(%rsi)//last rip
17112 10000973c:  c3  retq //jump last rip

这里说明下call和ret指令的作用,为后续的debug时作说明:
cpu执行call跳转指令时,cpu做了如下操作:
   rsp = rsp-8
   rsp = rip
//即跳转之前会将下一条要执行语句指令地址压入栈顶,call等同于以下两条语句,但call本身就一条指令
   pushq %rip
   jmp 标号
类似ret指令会将栈顶的内容弹出到rip寄存器中,继续执行:
   rip = rsp
   rsp = rsp+8
//等同于
   pop %rip

这里以example_copystack.cpp为例说明整个切换过程,其中在coctx_make/co_swap/coctx_swap打断点,因为算上主协程,一共有三个协程。
0号协程执行完coctx_make后的内容快照为:

(gdb) p ctx
$1 = (coctx_t *) 0x100805018
(gdb) p *ctx
$2 = {regs = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x100805000, 0x0, 
    0x100001f70 <CoRoutineFunc(stCoRoutine_t*, void*)>, 0x0, 0x0, 0x0, 0x10031fff0}, 
  ss_size = 131072, ss_sp = 0x100300000 ""}
(gdb) p &ctx->regs
$3 = (void *(*)[14]) 0x100805018

当执行到co_swap时,因为就主协程和0号协程:

(gdb) n
co_swap (curr=0x7ffeefbff9a8, pending_co=0x100805000) at co_routine.cpp:637
637     stCoRoutineEnv_t* env = co_get_curr_thread_env();
(gdb) n
641     curr->stack_sp= &c;
(gdb) p &pending_co->ctx
$4 = (coctx_t *) 0x100805018

接着进入coctx_swap,这里跳过切出主协程的快照,只看切入0号协程的内容,执行:

   0x00000001000058bd <+62>:    xor    %rax,%rax
=> 0x00000001000058c0 <+65>:    mov    0x30(%rsi),%rbp
   0x00000001000058c4 <+69>:    mov    0x68(%rsi),%rsp
   0x00000001000058c8 <+73>:    mov    (%rsi),%r15
   0x00000001000058cb <+76>:    mov    0x8(%rsi),%r14
   0x00000001000058cf <+80>:    mov    0x10(%rsi),%r13
   0x00000001000058d3 <+84>:    mov    0x18(%rsi),%r12
   0x00000001000058d7 <+88>:    mov    0x20(%rsi),%r9
   0x00000001000058db <+92>:    mov    0x28(%rsi),%r8
   0x00000001000058df <+96>:    mov    0x38(%rsi),%rdi
   0x00000001000058e3 <+100>:   mov    0x50(%rsi),%rdx
   0x00000001000058e7 <+104>:   mov    0x58(%rsi),%rcx
   0x00000001000058eb <+108>:   mov    0x60(%rsi),%rbx
   0x00000001000058ef <+112>:   lea    0x8(%rsp),%rsp
   0x00000001000058f4 <+117>:   pushq  0x48(%rsi)
   0x00000001000058f7 <+120>:   mov    0x40(%rsi),%rsi
   0x00000001000058fb <+124>:   retq 

以下是分别执行每一条汇编时各寄存器的内容:

(gdb) i r rsi
rsi            0x100805018        

此时rsi指向的是&pending_co->ctxmov 0x30(%rsi),%rbp是把pending_co->ctx->regs[6]中的内容mov到rbp,即rbp;

0x00000001000058c8 in coctx_swap ()
(gdb) i r rsp
rsp            0x10031fff0         0x10031fff0

把pending_co->ctx->regs[13]中的内容mov到rsp,即rsp:

111   char* sp = ctx->ss_sp + ctx->ss_size - sizeof(void*);
112   sp = (char*)((unsigned long)sp & -16LL);
113   
114   memset(ctx->regs, 0, sizeof(ctx->regs));
115   void** ret_addr = (void**)(sp);
116   *ret_addr = (void*)pfn;
117   
118   ctx->regs[13] = sp;

后面的汇编代码实现的效果是把pending_co->ctx->regs[0]恢复到r15,

0x00000001000058e3 in coctx_swap ()
(gdb) i r rdi
rdi            0x100805000         4303376384

0x00000001000058f4 in coctx_swap ()
(gdb) i r rsp
rsp            0x10031fff8         0x10031fff8

当执行到时这四行时:

   0x00000001000058ef <+112>:   lea    0x8(%rsp),%rsp
   0x00000001000058f4 <+117>:   pushq  0x48(%rsi)
=> 0x00000001000058f7 <+120>:   mov    0x40(%rsi),%rsi
   0x00000001000058fb <+124>:   retq

此时后续的各寄存器内容为:

(gdb) x/2ag 0x10031fff0
0x10031fff0:    0x100001f70 <_ZL13CoRoutineFuncP13stCoRoutine_tPv>  0x0

0x0000000100001f70 in CoRoutineFunc(stCoRoutine_t*, void*) () at co_routine.cpp:568
568     co_swap( lpCurrRoutine, co );
(gdb) i r rip
rip            0x100001f70         0x100001f70 <CoRoutineFunc(stCoRoutine_t*, void*)>

整个切入到pending_co协程已经完成。

切出是个相反的过程,这里不再debug具体过程,其中:

   0x00000001000096c0 <+0>: lea    (%rsp),%rax
   0x00000001000096c4 <+4>: mov    %rax,0x68(%rdi)
   0x00000001000096c8 <+8>: mov    %rbx,0x60(%rdi)
   0x00000001000096cc <+12>:    mov    %rcx,0x58(%rdi)
   0x00000001000096d0 <+16>:    mov    %rdx,0x50(%rdi)
=> 0x00000001000096d4 <+20>:    mov    (%rax),%rax
   0x00000001000096d7 <+23>:    mov    %rax,0x48(%rdi)
   0x00000001000096db <+27>:    mov    %rsi,0x40(%rdi)
   0x00000001000096df <+31>:    mov    %rdi,0x38(%rdi)
   0x00000001000096e3 <+35>:    mov    %rbp,0x30(%rdi)
   0x00000001000096e7 <+39>:    mov    %r8,0x28(%rdi)
   0x00000001000096eb <+43>:    mov    %r9,0x20(%rdi)
   0x00000001000096ef <+47>:    mov    %r12,0x18(%rdi)
   0x00000001000096f3 <+51>:    mov    %r13,0x10(%rdi)
   0x00000001000096f7 <+55>:    mov    %r14,0x8(%rdi)
   0x00000001000096fb <+59>:    mov    %r15,(%rdi)

把返回地址保存到ctx->regs[9]:

0x00000001000096d7 in coctx_swap ()
(gdb) i r rax
rax            0x100002c70         4294978672
 1516 100002c6b:  e8 50 6a 00 00  callq   27216 <coctx_swap>
 1517 100002c70:  e8 1b fc ff ff  callq   -997 <__Z22co_get_curr_thread_envv>

后续有时间分析下lua中协程的实现。

版权声明:本文来源简书,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://www.jianshu.com/p/4a58de3461ab
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-01-12 13:03:22
  • 阅读 ( 1615 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

推荐文章

猜你喜欢