core文件使用gdb来dump线程堆栈是很简单的事情,但是dump协程就有点困难了
因为gdb本身不知道协程的数据结构,本文记录了如何解决这个问题
协程的背景在我以前的博文在 C++ 中实现协程已经详细写过了,不再赘述
首先深入协程切换的原理
协程切换原理
协程实现原理
回顾一下协程的实现原理:
协程实例,主要功能是把寄存器的数据和当前栈整个记录在申请的内存中,随时可以将其恢复出来
记录和恢复两个功能,就完成不同的栈之间的切换,而如何切换是协程调度器的工作
这个切换有点像游戏中的“存档”功能, 就好比家长(协程调度器)安排几个孩子轮流玩同一台游戏机,每个孩子玩一会儿累了, 暂停游戏并“保存进度”(记录),让出游戏机, 家长再让下一个孩子“读取进度”(恢复)继续他自己之前玩到一半的游戏内容, 就这样依次排队轮流玩,每个孩子随时都能从之前暂停的位置继续玩,不用重新开始游戏。
fcontext_t实际上就是记录的协程数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| struct fc_stack_t { void* sp; std::size_t size; fc_stack_t() : sp(0) , size(0) {} };
struct fp_t { uint32_t fc_freg[2]; fp_t(): fc_freg() {} };
struct fcontext_t { uint64_t fc_greg[8]; fc_stack_t fc_stack; fp_t fc_fp; fcontext_t() : fc_greg(), fc_stack(), fc_fp(){} };
|
而操作fcontext_t的make_fcontext和jump_fcontext都是汇编实现
而他们的函数签名是这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
extern "C" fcontext_t * make_fcontext(void * sp, std::size_t size, void (* fn)(intptr_t));
extern "C" intptr_t jump_fcontext(fcontext_t * ofc, fcontext_t const* nfc, intptr_t vp, bool preserve_fpu = true);
|
使用make_fcontext可以新开辟一个协程栈空间,运行自己期望的函数,并返回一个fcontext_t指针
随后使用jump_fcontext就可以跳转到创建的协程栈空间去了
显然jump_fcontext就是对应了上文所述的恢复功能,而记录是通过组合了make_fcontext和jump_fcontext实现的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #include <cstdint> #include <cstdlib> #include <cstddef>
extern "C" fcontext_t * make_fcontext(void * sp, std::size_t size, void (* fn)(intptr_t)); extern "C" intptr_t jump_fcontext(fcontext_t * ofc, fcontext_t const* nfc, intptr_t vp, bool preserve_fpu);
struct record_bounce_t { fcontext_t self; fcontext_t* back; };
static void record_trampoline(intptr_t vp) { record_bounce_t* b = reinterpret_cast<record_bounce_t*>(vp); jump_fcontext(&b->self, b->back, 0, true); }
void record_context(fcontext_t* out) { constexpr std::size_t kStackSize = 64 * 1024; void* stack = std::malloc(kStackSize); void* sp = static_cast<char*>(stack) + kStackSize;
fcontext_t* tramp = make_fcontext(sp, kStackSize, record_trampoline);
record_bounce_t b{}; b.back = out;
jump_fcontext(out, tramp, reinterpret_cast<intptr_t>(&b), true);
std::free(stack); }
|
fcontext
在make_fcontext和jump_fcontext中都有一张字符串图,对应了fcontext的数据分布,0x54代表从0x54开始的4个字节,因此一共0x58字节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| /**************************************************************************************** * * * ---------------------------------------------------------------------------------- * * | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | * * ---------------------------------------------------------------------------------- * * | 0x0 | 0x4 | 0x8 | 0xc | 0x10 | 0x14 | 0x18 | 0x1c | * * ---------------------------------------------------------------------------------- * * | RBX | R12 | R13 | R14 | * * ---------------------------------------------------------------------------------- * * ---------------------------------------------------------------------------------- * * | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | * * ---------------------------------------------------------------------------------- * * | 0x20 | 0x24 | 0x28 | 0x2c | 0x30 | 0x34 | 0x38 | 0x3c | * * ---------------------------------------------------------------------------------- * * | R15 | RBP | RSP | RIP | * * ---------------------------------------------------------------------------------- * * ---------------------------------------------------------------------------------- * * | 16 | 17 | 18 | 19 | | * * ---------------------------------------------------------------------------------- * * | 0x40 | 0x44 | 0x48 | 0x4c | | * * ---------------------------------------------------------------------------------- * * | sp | size | | * * ---------------------------------------------------------------------------------- * * ---------------------------------------------------------------------------------- * * | 20 | 21 | | * * ---------------------------------------------------------------------------------- * * | 0x50 | 0x54 | | * * ---------------------------------------------------------------------------------- * * | fc_mxcsr|fc_x87_cw| | * * ---------------------------------------------------------------------------------- * * * * **************************************************************************************/
|
- 0x00 ~ 0x1C(每 8 字节一项)
- 0x00: RBX
- 0x08: R12
- 0x10: R13
- 0x18: R14
- 0x20 ~ 0x3C
- 0x20: R15
- 0x28: RBP
- 0x30: RSP(该上下文恢复时使用的栈指针)
- 0x38: RIP(该上下文恢复时跳转的指令地址;对于新上下文,这里保存的是入口函数地址)
- 0x40 ~ 0x4C
- 0x40: sp(调用 make_fcontext 传入的栈基指针,通常为栈内存的高地址)
- 0x48: size(栈大小)
- 0x50 ~ 0x54
- 0x50: fc_mxcsr(SSE/MMX 控制与状态字)
- 0x54: fc_x87_cw(x87 FPU 控制字)
注意:
- RSP 和 sp 的区别:
- RSP(偏移 0x30):该上下文“当前有效”的栈指针,在切入时会加载到 CPU 的 RSP。
- sp(偏移 0x40):原始的栈顶指针(高地址),便于调试或做边界检查,不直接作为运行时 RSP。
- 保存的是“非易失寄存器”(RBX、RBP、R12~R15),因为按 ABI 约定这些必须在函数调用间保持不变;而像 RAX、RCX、RDX 等易失寄存器不需要在上下文控制块中持久保存。
make_fcontext
源码在这里:https://github.com/tedcy/sheep_cpp/blob/master/src/coroutine/tc_make_x86_64_sysv_elf_gas.S
首先看下参数和返回值定义:
1
| extern "C" fcontext_t * make_fcontext(void * sp, std::size_t size, void (* fn)(intptr_t));
|
- 参数(SysV x86-64 调用约定):
- RDI:sp,指向栈内存的“顶端”(高地址)指针
- RSI:size,栈大小(字节数)
- RDX:context function 的入口地址(即将来首次切入该上下文时要执行的函数)
- 返回值:RAX 返回 fcontext_t 的指针(对齐到 16 字节)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| .text .globl make_fcontext .type make_fcontext,@function .align 16 make_fcontext: # 在栈顶(sp)下方预留 0x58 字节空间用于 fcontext_t 控制块(x86-64 栈向下增长) leaq -0x58(%rdi), %rax
# 将fcontext_t指针向下对齐到 16 字节边界(与 ~0xF 做 AND,遵守 SysV ABI 的 16 字节对齐要求) andq $-16, %rax
movq %rdi, 0x40(%rax) # 保存传入的栈顶指针 sp movq %rsi, 0x48(%rax) # 保存栈大小 size movq %rdx, 0x38(%rax) # 保存上下文入口函数地址到 RIP 字段
stmxcsr 0x50(%rax) # 保存当前的 MXCSR(SSE/MMX 控制状态字) fnstcw 0x54(%rax) # 保存 x87 FPU 控制字
leaq -0x8(%rax), %rdx # rax指向fcontext_t指针,把fcontext_t指针-0x8存到rdx寄存器 movq %rdx, 0x30(%rax) # 把rdx寄存器值写到RSP
leaq finish(%rip), %rcx # 计算finish标签的绝对地址,存到rcx寄存器 movq %rcx, (%rdx) # 把rcx寄存器值写到rdx寄存器值指向的地址(也就是RSP)
ret # 栈上弹出返回地址,跳转到返回地址
finish: call abort@PLT # context function如果ret了,就会返回到finish,从而abort hlt # 防止继续执行(理论上不会到这里) .size make_fcontext,.-make_fcontext
|
总的来说,这里可以分成两个部分
- 创建除了RSP以外的非易失寄存器值
- 创建RSP寄存器值
重点看下创建RSP寄存器值的逻辑
1 2 3 4 5
| leaq -0x8(%rax), %rdx # rax指向fcontext_t指针,把fcontext_t指针-0x8存到rdx寄存器 movq %rdx, 0x30(%rax) # 把rdx寄存器值写到RSP
leaq finish(%rip), %rcx # 计算finish标签的绝对地址,存到rcx寄存器 movq %rcx, (%rdx) # 把rcx寄存器值写到rdx寄存器值指向的地址(也就是RSP)
|
这里为什么不能直接把fcontext_t指针作为RSP,而是写入了一个finish的label作为RSP呢
之前博文栈回溯原理在总结hook的backtrace问题时,对函数调用的函数序言(prologue)和函数后记(epilogue)有过详述:
简单来说
调用一个foo函数时是把函数调用返回的那一行(.Lafter_foo)指令计数器 % rip 写入到寄存器 % rax 以后,push 到栈里面,然后跳转到 foo 函数
1 2 3 4 5
| #call foo leaq .Lafter_foo(%rip), %rax pushq %rax jmp foo .Lafter_foo:
|
那么返回的时候调用ret实际上是如下汇编
1 2 3
| #ret popq %rax # 弹出返回地址(也就是.Lafter_foo:) jmp *%rax # 跳转到返回地址继续执行
|
重新再看看make_context创建的内存栈:
1 2 3 4 5 6 7 8 9 10 11
| 高地址 ┌───────────────────────────────┐ │ sp (入参) │ ← 用户提供的栈顶(高地址) ├───────── 未用/对齐填充 ─────────┤ │ fcontext_t (0x58B) │ ← rax 指向这里(16B 对齐) └───────────────────────────────┘ rax │ fake return addr = &finish │ ← rdx = rax - 8 = 初始 RSP └───────────────────────────────┘ rsp(新上下文初始) │ │ ↓ 函数执行时进一步向下生长 │ ... │ 低地址
|
初始RSP指向finish以后,当make_fcontext传入的context func执行完毕以后
假如没有切换协程,而是正常返回了,那么当ret的时候
1 2 3
| #ret popq %rax # 弹出返回地址(也就是&finish:) jmp *%rax # 跳转到返回地址继续执行
|
从而abort掉(协程栈已经到顶了,没得ret了,协程实例应该把控制权还给协程调度器)
jump_fcontext
源码在这里:https://github.com/tedcy/sheep_cpp/blob/master/src/coroutine/tc_jump_x86_64_sysv_elf_gas.S
首先看下参数和返回值定义:
1
| extern "C" intptr_t jump_fcontext(fcontext_t * ofc, fcontext_t const* nfc, intptr_t vp, bool preserve_fpu = true);
|
- 参数(SysV x86-64 调用约定):
- RDI:ofc,当前上下文的 fcontext_t 存放位置(把当前状态保存到这里)
- RSI:nfc,目标上下文的 fcontext_t(从这里恢复状态)
- RDX:vp,传递给目标上下文入口函数的“用户值”,同时也将作为“对原调用者的返回值”(当将来切回时)
- RCX:preserve_fpu_flag,是否保存/恢复 FPU 控制字(mxcsr/x87_cw);非 0 则执行保存/恢复,0 则跳过
- 返回值:RAX 返回 从其他上下文再切换回此上下文时,返回传递回来的intptr_t值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| .text .globl jump_fcontext .type jump_fcontext,@function .align 16 jump_fcontext: movq %rbx, (%rdi) # 保存 RBX movq %r12, 0x8(%rdi) # 保存 R12 movq %r13, 0x10(%rdi) # 保存 R13 movq %r14, 0x18(%rdi) # 保存 R14 movq %r15, 0x20(%rdi) # 保存 R15 movq %rbp, 0x28(%rdi) # 保存 RBP
cmp $0, %rcx # rcx==0 则跳到1这个label,跳过FPU控制字处理 je 1f
stmxcsr 0x50(%rdi) # 保存当前FPU 控制字(mxcsr/x87_cw)到 ofc fnstcw 0x54(%rdi)
ldmxcsr 0x50(%rsi) # 从目标 nfc 恢复相应FPU 控制字(mxcsr/x87_cw) fldcw 0x54(%rsi) 1:
leaq 0x8(%rsp), %rax # 计算“去掉返回地址后的栈顶” movq %rax, 0x30(%rdi) # 保存为 ofc->RSP movq (%rsp), %rax # 取保存在栈上的返回地址(上一层栈调用jump_context的下一行地址) movq %rax, 0x38(%rdi) # 保存为 ofc->RIP
movq (%rsi), %rbx # 恢复 RBX movq 0x8(%rsi), %r12 # 恢复 R12 movq 0x10(%rsi), %r13 # 恢复 R13 movq 0x18(%rsi), %r14 # 恢复 R14 movq 0x20(%rsi), %r15 # 恢复 R15 movq 0x28(%rsi), %rbp # 恢复 RBP
movq 0x30(%rsi), %rsp # 恢复 RSP movq 0x38(%rsi), %rcx # 恢复 RIP
movq %rdx, %rax # vp作为返回值存到rax movq %rdx, %rdi # vp作为参数传递给即将进入的context func
jmp *%rcx # 跳转到context func .size jump_fcontext,.-jump_fcontext
|
这里有两个注意点: