这里补充下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 , 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->ctx
,mov 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
整个切入到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
1517 100002c70: e8 1b fc ff ff callq -997 <__Z22co_get_curr_thread_envv>
后续有时间分析下lua中协程的实现。