在core文件dump协程堆栈

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() {}
};

// 协程上下文结构(fcontext_t)
struct fcontext_t {
uint64_t fc_greg[8]; // 通用寄存器(general registers)保存区:
// fc_greg[0]: RBX
// fc_greg[1]: R12
// fc_greg[2]: R13
// fc_greg[3]: R14
// fc_greg[4]: R15
// fc_greg[5]: RBP (基址指针, FramePointer)
// fc_greg[6]: RIP (指令地址寄存器, Instruction Pointer)
// fc_greg[7]: RSP (栈顶寄存器, Stack Pointer)
fc_stack_t fc_stack; // 分配给当前协程的执行栈空间
fp_t fc_fp; // 浮点寄存器状态保存区域
fcontext_t() // 构造函数初始化寄存器与栈空间
: fc_greg(), fc_stack(), fc_fp(){}
};

而操作fcontext_tmake_fcontextjump_fcontext都是汇编实现

而他们的函数签名是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建一个新的协程上下文函数(make function context)
// 参数说明:
// sp: 栈空间的地址(通常指向栈空间的顶端)
// size: 栈的大小,单位字节
// fn: 上下文启动后执行的协程函数指针,函数类型为 void fn(intptr_t)
// 返回值: 返回指向新创建的上下文结构的指针(fcontext_t指针)
extern "C" fcontext_t * make_fcontext(void * sp, std::size_t size, void (* fn)(intptr_t));

// 上下文跳转函数,从当前上下文切换至目标上下文。
// 参数说明:
// ofc: 用于保存当前上下文(old context)的指针
// nfc: 目标上下文(new context)的指针
// vp: 在切换到目标上下文时传递的整型参数
// preserve_fpu: 是否需要保存浮点寄存器 (默认true,防止浮点计算数据丢失)
// 返回值: 从其他上下文再切换回此上下文时,返回传递回来的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_fcontextjump_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);

// 用于 trampoline 的临时数据
struct record_bounce_t {
fcontext_t self; // 保存 trampoline 自身上下文(仅用于完成跳转保存,不再使用)
fcontext_t* back; // 要回跳的目标(即我们要“记录”的那个上下文 ofc)
};

// trampoline:什么也不做,立刻跳回,完成“记录”
static void record_trampoline(intptr_t vp) {
record_bounce_t* b = reinterpret_cast<record_bounce_t*>(vp);
// 从 trampoline 跳回去:这一步会把“当前(trampoline)上下文”保存到 b->self
// 并恢复到 b->back(也就是调用方的上下文),从而让调用方的 jump_fcontext 返回
jump_fcontext(&b->self, b->back, 0, true);
// 不会返回到这里
}

// 最简单的“记录当前上下文”函数:把调用点的 CPU 上下文保存到 *out 中
void record_context(fcontext_t* out) {
// 1) 准备一个小栈给 trampoline
constexpr std::size_t kStackSize = 64 * 1024; // 示例大小
void* stack = std::malloc(kStackSize);
void* sp = static_cast<char*>(stack) + kStackSize;

// 2) 生成 trampoline 上下文
fcontext_t* tramp = make_fcontext(sp, kStackSize, record_trampoline);

// 3) 组织传递数据
record_bounce_t b{};
b.back = out;

// 4) 跳到 trampoline:
// - 这一步会把“当前上下文”保存到 *out(完成记录)
// - 然后开始在 trampoline 上运行 record_trampoline
// - record_trampoline 立刻跳回,于是本调用返回
jump_fcontext(out, tramp, reinterpret_cast<intptr_t>(&b), true);

// 5) 释放 trampoline 的栈(一次性使用)
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

这里有两个注意点:

  • RSP和RIP的值

    回顾调用foo的例子:

    调用一个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:

    jmp foo以后的堆栈如下:

    1
    2
    3
    4
    5
    6
    7
    高地址
    ┌───────────────────────────────┐
    │ ...(更老的调用栈数据) │
    ├───────────────────────────────┤
    return addr → .Lafter_foo │ ← %rsp
    └───────────────────────────────┘
    低地址

    那么返回的时候调用ret指令实际上是如下汇编:

    1
    2
    3
    #ret
    popq %rax # 弹出返回地址(也就是.Lafter_foo:)
    jmp *%rax # 跳转到返回地址继续执行

    当返回调用ret指令以后,堆栈如下:

    1
    2
    3
    4
    5
    6
    7
    高地址
    ┌───────────────────────────────┐
    │ ...(更老的调用栈数据) │ ← %rsp
    ├───────────────────────────────┤
    return addr → .Lafter_foo │ ← %rsp - 8(已经被弹出)
    └───────────────────────────────┘
    低地址

    回到jump_context的汇编代码来

    1
    2
    3
    4
    leaq     0x8(%rsp),  %rax         # 计算“去掉返回地址后的栈顶”
    movq %rax, 0x30(%rdi) # 保存为 ofc->RSP
    movq (%rsp), %rax # 取保存在栈上的返回地址(上一层栈调用jump_context的下一行地址)
    movq %rax, 0x38(%rdi) # 保存为 ofc->RIP
    • ofc->RSP = %rsp_cur + 8

      那么RSP就会指向已经被弹出的正确位置

    • ofc->RIP = *(%rsp_cur)

      那么RIP就会指向栈中存储的返回地址例如.Lafter_foo

    实际上就模拟了ret返回的过程,让ofc指向的context回到了调用jump_context前的状态

  • 第三个参数的vp的两个作用

    • vp作为返回值存到rax

      1
      movq     %rdx,        %rax        # vp作为返回值存到rax

      也就是刚才说的,某个ofc被存下来以后,将来会重新jump_context作为nfc参数切换回来,nfc目标栈是这样的:

      1
      2
      3
      4
      5
      6
      7
      高地址
      ┌───────────────────────────────┐
      │ ...(更老的调用栈数据) │ ← %rsp
      ├───────────────────────────────┤
      │ return addr → .Lafter_foo │ ← %rsp - 8
      └───────────────────────────────┘
      低地址
    • vp作为参数传递给即将进入的context func

      1
      movq     %rdx,        %rdi        # vp作为参数传递给即将进入的context func

      这里是配合make_context刚创建的一个全新的栈,nfc目标栈是这样的:

      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(新上下文初始)
      │ │ ↓ 函数执行时进一步向下生长
      │ ... │
      低地址

      nfc目标栈对应的context_func是下面的函数签名

      1
      static void record_trampoline(intptr_t vp);

gdb手动切换协程

以一个实际运行中的例子,来看如何切换协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int foo() {
int c = 10;
c += rand();
FDLOG("test") << taf::CoroutineScheduler::currentScheduler() << endl;
taf::CoroutineScheduler::currentScheduler()->sleep(10000);
return c;
}

taf::Int32 StressImp::testStr(const std::string& in, std::string &out, taf::JceCurrentPtr current)
{
AwaitRun() {
foo();
};
sleep(10000000);
return 0;
}

这里打印了foo函数所属线程的线程局部变量协程调度器地址

gdb进去以后堆栈在sleep上

1
2
3
4
5
6
7
8
9
10
(gdb) bt
#1 sleep()
#2 0x00000000004306e3 in StressImp::testStr (this=<optimized out>, in=..., out=..., current=...) at StressImp.cpp:153
#3 0x000000000045bdc3 in Test::Stress::onDispatch (this=0x414ac00, _current=..., _sResponseBuffer=std::vector of length 0, capacity 0) at Stress.hpp:454
#4 0x000000000052bf47 in taf::Servant::dispatch (this=0x414ac00, current=..., buffer=std::vector of length 0, capacity 0) at /root/taf/src/libservant/Servant.cpp:104
#5 0x0000000000541458 in taf::ServantHandle::handleTafProtocol (this=<optimized out>, current=...) at /root/taf/src/libservant/ServantHandle.cpp:1447
#6 0x000000000053a2e6 in taf::ServantHandle::handleRecvData (this=0x3daf600, stRecvData=0x3630460) at /root/taf/src/libservant/ServantHandle.cpp:452
#7 0x00000000004e649d in std::function<void()>::operator() (this=0x501ce60) at /usr/include/c++/5/functional:2267
#8 taf::CoroutineInfo::corotineProc (args=args@entry=0x43ed760) at /root/taf/src/libservant/CoroutineScheduler.cpp:424
#9 0x00000000004e1462 in taf::CoroutineInfo::corotineEntry (q=<optimized out>) at /root/taf/src/libservant/CoroutineScheduler.cpp:402

如何切换到休眠在taf::CoroutineScheduler::currentScheduler()->sleep的协程呢?

我已经从日志中得到了线程局部变量协程调度器地址是0x150c6a00了

而全部taf::CoroutineScheduler::currentScheduler()->sleep休眠的协程,存储在

1
2
3
class CoroutineScheduler {
CoroutineInfo _timeout;
}

这里的CoroutineInfo作为协程实例,同时也是侵入式链表的节点,_timeout显然是链表头

1
2
3
4
5
class CoroutineInfo {
CoroutineInfo* _prev;
CoroutineInfo* _next;
fcontext_t* _ctx_to;
}

这里看到熟悉的fcontext_t了,那么_timeout的第一个实例的寄存器变量就可以很容易得到了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) p/x (*(*('taf::CoroutineScheduler'*)0x150c6a00)._timeout._next)._ctx_to.fc_greg[0]
$1 = 0x151562c0
(gdb) p/x (*(*('taf::CoroutineScheduler'*)0x150c6a00)._timeout._next)._ctx_to.fc_greg[1]
$2 = 0x15202ce0
(gdb) p/x (*(*('taf::CoroutineScheduler'*)0x150c6a00)._timeout._next)._ctx_to.fc_greg[2]
$3 = 0x150c6a30
(gdb) p/x (*(*('taf::CoroutineScheduler'*)0x150c6a00)._timeout._next)._ctx_to.fc_greg[3]
$4 = 0x1
(gdb) p/x (*(*('taf::CoroutineScheduler'*)0x150c6a00)._timeout._next)._ctx_to.fc_greg[4]
$5 = 0x15202c60
(gdb) p/x (*(*('taf::CoroutineScheduler'*)0x150c6a00)._timeout._next)._ctx_to.fc_greg[5]
$6 = 0x150c6a30
(gdb) p/x (*(*('taf::CoroutineScheduler'*)0x150c6a00)._timeout._next)._ctx_to.fc_greg[6]
$7 = 0x15202bf0
(gdb) p/x (*(*('taf::CoroutineScheduler'*)0x150c6a00)._timeout._next)._ctx_to.fc_greg[7]
$8 = 0x4df73d

其中5,6,7分别代表了RBP,RSP,RIP,因此可以切换寄存器了

1
2
3
(gdb) set $rbp = 0x150c6a30
(gdb) set $rsp = 0x15202bf0
(gdb) set $rip = 0x4df73d

展示手动切换的结果,第3个栈帧正是CoroutineScheduler::sleep,而0栈帧和1栈帧实际上是在jump_context到协程调度器主协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(gdb) bt
#0 taf::CoroutineInfo::switchTo (this=0x4332210, to=0x150c6a30, to@entry=0x501c1a0) at /root/taf/src/libservant/CoroutineScheduler.cpp:353
#1 0x00000000004df805 in taf::CoroutineScheduler::switchCoro (this=this@entry=0x150c6a00, from=<optimized out>, to=to@entry=0x501c1a0)
at /root/taf/src/libservant/CoroutineScheduler.cpp:1539
#2 0x00000000004e5e44 in taf::CoroutineScheduler::sleep (this=0x150c6a00, iSleepTime=iSleepTime@entry=10000, cb=...)
at /root/taf/src/libservant/CoroutineScheduler.cpp:1333
#3 0x0000000000430412 in foo () at StressImp.cpp:143
#4 0x0000000000438463 in std::function<void()>::operator() (this=0x1b875e30) at /usr/include/c++/5/functional:2267
#5 taf::CoroutineAwaitRunner::invoke<void>(std::function<void ()>, taf::TC_AutoPtr<taf::CoroutineResource<void> > const&)::{lambda()#1}::operator()() const (
__closure=0x1b875e30) at /usr/local/taf-version/taf-3.4.6.3-6b6b59/include/servant/AwaitRun.h:432
#6 std::_Function_handler<void (), taf::CoroutineAwaitRunner::invoke<void>(std::function<void ()>, taf::TC_AutoPtr<taf::CoroutineResource<void> > const&)::{lambda()#1}>::_M_invoke(std::_Any_data const&) (__functor=...) at /usr/include/c++/5/functional:1871
#7 0x0000000000432955 in std::function<void()>::operator() (this=0x1c69a180) at /usr/include/c++/5/functional:2267
#8 taf::CoroutineAwaitRunner::doCall<void>(std::function<void ()>, taf::CoroutineResource<void>*)::{lambda()#1}::operator()() (__closure=0x1c69a180)
at /usr/local/taf-version/taf-3.4.6.3-6b6b59/include/servant/AwaitRun.h:462
#9 std::_Function_handler<void (), taf::CoroutineAwaitRunner::doCall<void>(std::function<void ()>, taf::CoroutineResource<void>*)::{lambda()#1}>::_M_invoke(std::_Any_data const&) (__functor=...) at /usr/include/c++/5/functional:1871
#10 0x00000000004e649d in std::function<void()>::operator() (this=0x15202e60) at /usr/include/c++/5/functional:2267
#11 taf::CoroutineInfo::corotineProc (args=args@entry=0x151562c0) at /root/taf/src/libservant/CoroutineScheduler.cpp:424
#12 0x00000000004e1462 in taf::CoroutineInfo::corotineEntry (q=<optimized out>) at /root/taf/src/libservant/CoroutineScheduler.cpp:402

自动化解析的问题

gdb加载core操作寄存器问题

刚才手动切换协程,是基于正在运行的进程执行的,如果对core文件gdb以后进行切换,则会报错

1
2
(gdb) set $rip=0
You can't do that without a process to debug.

提示只能对正在执行的进程执行这个操作,这也很好理解,进程都没了,去操作谁的寄存器?

因此,这里需要对gdb做一定的魔改,主要是参考这个issue的代码:

support gdb_bthread_stack.py in coredump

gdb修改思路: core target,增加m_registers寄存器缓存变量,用来接收set $rsp等的临时寄存器值; 同时实现虚函数prepare_to_store和store_registers的override。当store_registers时,m_registers保存当前set的寄存器值。 同时修改fetch_registers,如果m_registers有效,就从直接从m_registers中返回给上层调用者。

基于这个修改,我fork了gdb-static的项目https://github.com/tedcy/gdb-static/tree/v16.3-static-enable-set-register-in-core,并修改依赖的gdb源码(在gdb-static的submodule binutils-gdb中):

https://github.com/tedcy/binutils-gdb/commit/f8f54982ef4999e614cf141ba659d2b915984797

在理解核心代码前,需要理解几个概念:

  • regcache(struct regcache*)
    • 寄存器缓存对象,按“线程”维度存在。一个 regcache 对应当前线程的寄存器视图。
    • regcache->ptid() 表示这个缓存对应的线程 ID(ptid)
    • regcache 内部有一块连续的“原始寄存器”缓冲区,以及每个寄存器对应的偏移量和大小(register_offset[]、sizeof_register[]),还有寄存器状态数组(有效/未知/不可用等)
      • sizeof_raw_registers:整块原始寄存器缓冲区的总大小。
      • num_regs:原始寄存器个数(数组长度)。
      • register_offset[i]:第 i 个原始寄存器在缓冲区内的起始字节偏移。
      • sizeof_register[i]:第 i 个原始寄存器的字节大小。
  • core_target
    • 通常“每个已加载的 core 文件”会有一个 core_target 实例。
    • 在大部分场景下,只有一个core文件,因此这个实例是全局唯一的

主要是设置了fetch_registers和store_registers回调,对应的核心代码就是

https://github.com/tedcy/binutils-gdb/blob/f8f54982ef4999e614cf141ba659d2b915984797/gdb/corelow.c#L1404-L1450

  • fetch_registers 会在以下场景被调用:
    • 打开 core 文件后,首次需要访问寄存器时(比如 info registers、p $pc、反汇编需要 PC、回溯需要寄存器做展开等)
    • 切换到不同线程(ptid 变化),需要加载该线程的寄存器
  • store_registers 会在以下场景被调用:
    • 用户执行 set $reg = value(例如 set $pc = 0x...、set $rax = 1
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
47
48
49
50
51
52
53
54
55
56
void
core_target::fetch_registers (struct regcache *regcache, int regno)
{
if (!(m_core_gdbarch != nullptr
&& gdbarch_iterate_over_regset_sections_p (m_core_gdbarch)))
{
gdb_printf (gdb_stderr,
"Can't fetch registers from this type of core file\n");
return;
}
//修改1:
//如果存在已经设置的m_registers缓存,且当前线程id和传入的需要访问的线程id相同,那么直接拷贝返回
if (m_registers && current_ptid == regcache->ptid ())
{
memcpy(regcache->m_registers.get(), m_registers.get(), regcache->sizeof_raw_registers);
memcpy(regcache->m_register_status.get(), m_register_status.get(), regcache->num_regs);
return;
}

struct gdbarch *gdbarch = regcache->arch ();
get_core_registers_cb_data data = { this, regcache };
gdbarch_iterate_over_regset_sections (gdbarch,
get_core_registers_cb,
(void *) &data, NULL);

/* Mark all registers not found in the core as unavailable. */
for (int i = 0; i < gdbarch_num_regs (regcache->arch ()); i++)
if (regcache->get_register_status (i) == REG_UNKNOWN)
regcache->raw_supply (i, NULL);

//修改2:
//如果m_registers缓存未设置,或者缓存的线程id和需要访问的线程id不同
//那么把获取的数据写入到m_registers缓存,并更新current_ptid
if (m_registers == nullptr || current_ptid != regcache->ptid ())
{
m_registers.reset (new gdb_byte[regcache->sizeof_raw_registers]);
m_register_status.reset (new register_status[regcache->num_regs]);
current_ptid = regcache->ptid ();

memcpy(m_registers.get(), regcache->m_registers.get(), regcache->sizeof_raw_registers);
memcpy(m_register_status.get(), regcache->m_register_status.get(), regcache->num_regs);
}
}

void
core_target::store_registers (struct regcache *regcache, int regnum)
{
//修改3:
//当主动写入的时候,存储到m_registers缓存
//regnum 表示“用户刚修改的那个寄存器编号”
//只拷贝该寄存器对应的那一段数据到缓存,并把状态置为 REG_VALID
memcpy(m_registers.get() + regcache->register_offset[regnum],
regcache->m_registers.get() + regcache->register_offset[regnum],
regcache->sizeof_register[regnum]);
m_register_status[regnum] = REG_VALID;
}

这里修改2的整块初始化实际上接管了整个gdb的寄存器读写逻辑,因为store_registers只写入了一个寄存器

  • 如果不做修改2的整块初始化,用户第一次 set $pc 触发 store_registers 时,m_registers 可能还是空/未初始化。你只会把 pc 那段写进去,其他寄存器在 m_registers 里是垃圾或全零。
  • 下一次 fetch 命中修改1的早返回,整块把 m_registers 拷贝到 regcache,就把“除了 pc 以外的所有寄存器”用错误数据覆盖了。

线程局部变量的协程调度器地址问题

为了方便编译使用了gdb-static,但是遇到一个很尴尬的问题:无法解析线程私有变量的地址

这是因为静态编译的gdb-static无法加载glibc的线程调试动态库libthread_db.so

如下命令,gdb设置debug模式,并且允许加载任何动态库,可以看到Cannot find thread-local variables on this target的报错:

1
2
3
4
5
6
7
8
9
~ ./gdb -iex 'set debug libthread-db 1' -iex 'set auto-load safe-path /' /data/app/taf/tafnode/data/HUYASZ.TafStressServer/bin/TafStressServer ~/core-handle-HUYASZ.T-249762-1764510106
Trying host libthread_db library: /lib/x86_64-linux-gnu/libthread_db.so.1.
dlopen failed: Dynamic loading not supported.
thread_db_load_search returning 0

warning: Unable to find libthread_db matching inferior's thread library, thread debugging will not be available.
(gdb) p/x 'taf::CoroutineScheduler::currentScheduler()::tlsScheduler'
Cannot find thread-local storage for LWP 249788, executable file /data/app/taf/tafnode/data/HUYASZ.TafStressServer/bin/TafStressServer:
Cannot find thread-local variables on this target

为什么静态编译的gdb不能加载,gdb-static的issues有记载:GDB warns that thread debugging is disabled but it's actually fine?

When handling a dynamically linked multithreaded application, gdb tries to dynamically load the libthread_db file associated with it. This is impossible to do when gdb itself is statically linked.

处理动态链接的多线程应用程序时,gdb 会尝试动态加载与其关联的 libthread_db 文件。但如果 gdb 本身是静态链接的,则无法做到这一点。

However, gdb contains a fallback in case it doesn't find the library or in case the dynamic load doesn't work. In that case, gdb maintains most of its functionally regarding threads (but some is still lost IIRC).

但是,gdb 包含一个备用方案,以防找不到库或动态加载失败。在这种情况下,gdb 仍然保留了大部分与线程相关的功能(但我记得有些功能仍然会丢失)。

不幸的是,线程私有变量正是那个备用方案无法解决的功能

因为libthread_db.so的用途,可以见https://docs.undo.io/LibThreadDB.html

libthread_db.so is a library used to extract thread information from programs by parsing the memory of libpthread.so which is linked into multithreaded programs. This is needed when reading from thread-local storage, such as the errno variable.

libthread_db.so是一个用于从程序中提取线程信息的库,它通过解析链接到多线程程序中的内存libpthread.so来实现。这在读取线程局部存储(例如errno变量)时是必需的。

方案一:自己解析(不太通用,可以协助理解原理)

这种情况下,我尝试自己去解析线程私有变量的位置:

gdb是可以看到线程局部变量是如何获取的

1
2
3
4
5
6
7
8
9
(gdb) disassemble 'taf::CoroutineScheduler::currentScheduler'
Dump of assembler code for function _ZN3taf18CoroutineScheduler16currentSchedulerEv:
0x0000000000433801 <+0>: push %rbp
0x0000000000433802 <+1>: mov %rsp,%rbp
0x0000000000433805 <+4>: mov %fs:0x0,%rax
0x000000000043380e <+13>: lea -0x570(%rax),%rax
0x0000000000433815 <+20>: pop %rbp
0x0000000000433816 <+21>: ret
End of assembler dump.

这里可以看到,获取了fs的地址,存到了rax寄存器,然后对rax寄存器指向的地址取值,就是这个线程私有变量的地址

fs是什么?

假设有一个程序加载到内存中。其中包含 .text 段(存放代码)和 .data 段(存放(可变)全局变量),两者之间保持固定的偏移量。

由于组合可以加载到内存中的任何基地址,因此 .text 段使用 %rip 寄存器来引用同一对象中的全局变量。

对于其他对象中的变量,它使用 GOT(全局偏移表);对于其他对象中的函数,它使用 PLT(过程链接表)。

但是对于线程局部数据……我们需要另一个部分:

.tdata 部分的问题在于每个线程必须拥有一个副本。线程共享 .text 部分、 .data 部分,甚至 .bss 部分——而且这些部分对于每个线程来说位置相同——但 .tdata 部分对于每个线程来说位置不同——相对于 .code 的偏移量也不同:

所以我们不能使用相对寻址!必须有一个地方,在某个地方告诉线程“这是 .tdata 部分的开始”。

我们不能使用像 %rax%rdi 这样的通用寄存器,因为它们已被 ABI 占用——用于返回值或传递参数。编译器也会占用它们——当编译器不调用函数时,它可以自由地使用 %rax%r15 来存储临时值。

那么该怎么办呢?利用那些额外的段寄存器!它们目前没有任何用途——因此, %gs 被用来指示 Linux x86 上线程局部存储区的地址,而 %fs 则被用来指示 Linux x86-64 上线程局部存储区的地址。

回到方案一

gdb的fs寄存器是使用fs_base来展示的,因此可以很轻松的获取到线程私有变量的值

像刚才一样手动切换协程验证一下是否可以dump出sleep着的某个协程

通过日志我知道sleep着的那个协程,在线程45

1
2
3
4
(gdb) thread 45
[Switching to thread 45 (LWP 51127)]
#0 pthread_cond_timedwait@@GLIBC_2.3.2 () at ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_timedwait.S:225
warning: 225 ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_timedwait.S: No such file or directory

打印线程局部变量地址:

1
2
(gdb) p/x *($fs_base-0x570)                                                            
$51 = 0x14aea000

像刚才一样设置rbp,rsp,rip

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
(gdb) p/x (*(*('taf::CoroutineScheduler'*)0x14aea000)._timeout._next)._ctx_to.fc_greg[5]
$52 = 0x14aea030
(gdb) p/x (*(*('taf::CoroutineScheduler'*)0x14aea000)._timeout._next)._ctx_to.fc_greg[6]
$53 = 0x14ba2b30
(gdb) p/x (*(*('taf::CoroutineScheduler'*)0x14aea000)._timeout._next)._ctx_to.fc_greg[7]
$54 = 0x510f7d
(gdb) set $rbp = 0x14aea030
(gdb) set $rsp = 0x14ba2b30
(gdb) set $rip = 0x510f7d
(gdb) bt
#0 taf::CoroutineInfo::switchTo (this=0x14aea000, to=0x14aea198, to@entry=0x7fc71c4ea7c0) at /root/taf/src/libservant/CoroutineScheduler.cpp:353
#1 0x0000000000511045 in taf::CoroutineScheduler::switchCoro (this=this@entry=0x14aea000, from=<optimized out>, to=to@entry=0x7fc71c4ea7c0)
at /root/taf/src/libservant/CoroutineScheduler.cpp:1539
#2 0x0000000000517684 in taf::CoroutineScheduler::sleep (this=0x14aea000, iSleepTime=<optimized out>, cb=...)
at /root/taf/src/libservant/CoroutineScheduler.cpp:1333
#3 0x000000000042fc7b in foo () at StressImp.cpp:143
#4 0x000000000042fd0f in <lambda()>::operator()(void) const (__closure=0x1b7a7c80) at StressImp.cpp:150
#5 0x0000000000430120 in std::_Function_handler<void(), StressImp::testStr(const string&, std::__cxx11::string&, taf::JceCurrentPtr)::<lambda()> >::_M_invoke(const std::_Any_data &) (__functor=...) at /usr/include/c++/5/functional:1871
#6 0x000000000044b230 in std::function<void()>::operator() (this=0x1b7a7c80) at /usr/include/c++/5/functional:2267
#7 0x0000000000470e6c in taf::CoroutineAwaitRunner::invoke<void>(std::function<void ()>, taf::TC_AutoPtr<taf::CoroutineResource<void> > const&)::{lambda()#1}::operator()() const (__closure=0x1b7a7c80) at /usr/local/taf-version/taf-3.4.6.3-6b6b59/include/servant/AwaitRun.h:432
#8 0x00000000004823f3 in std::_Function_handler<void (), taf::CoroutineAwaitRunner::invoke<void>(std::function<void ()>, taf::TC_AutoPtr<taf::CoroutineResource<void> > const&)::{lambda()#1}>::_M_invoke(std::_Any_data const&) (__functor=...) at /usr/include/c++/5/functional:1871
#9 0x000000000044b230 in std::function<void()>::operator() (this=0x14b028c0) at /usr/include/c++/5/functional:2267
#10 0x000000000047909d in taf::CoroutineAwaitRunner::doCall<void>(std::function<void ()>, taf::CoroutineResource<void>*)::{lambda()#1}::operator()() (
__closure=0x14b028c0) at /usr/local/taf-version/taf-3.4.6.3-6b6b59/include/servant/AwaitRun.h:462
#11 0x000000000048b8eb in std::_Function_handler<void (), taf::CoroutineAwaitRunner::doCall<void>(std::function<void ()>, taf::CoroutineResource<void>*)::{lambda()#1}>::_M_invoke(std::_Any_data const&) (__functor=...) at /usr/include/c++/5/functional:1871
#12 0x0000000000517cdd in std::function<void()>::operator() (this=0x14ba2e60) at /usr/include/c++/5/functional:2267
#13 taf::CoroutineInfo::corotineProc (args=args@entry=0x14a49760) at /root/taf/src/libservant/CoroutineScheduler.cpp:424
#14 0x0000000000512ca2 in taf::CoroutineInfo::corotineEntry (q=<optimized out>) at /root/taf/src/libservant/CoroutineScheduler.cpp:402

确实可以打印,那么这个方案的缺点是什么呢?

缺点(难以通用)

我刚才没说的是,刚才那个case是O0下编译的,重写用O2编译以后

1
2
(gdb) disassemble 'taf::CoroutineScheduler::currentScheduler'
No symbol "taf::CoroutineScheduler::currentScheduler" in current context.

currentScheduler这个仅返回一行的函数被编译器被内联优化了!那么解读汇编就只能从调用currentScheduler的函数入手了

例如

1
2
3
4
5
void CoroutineScheduler::sleep(int iSleepTime, std::function<void(taf::SuspendEntry, taf::CoroutineScheduler*)> cb) {
...省略
auto cs = currentScheduler(); //1326行
...省略
}

gdb查看是奇怪的乱码(可能是gdb的bug吧)

1
2
3
4
5
6
7
8
9
10
11
12
(gdb) disassemble /m 'taf::CoroutineScheduler::sleep'
...省略
1326 in /root/taf/src/libservant/CoroutineScheduler.cpp
0x00000000004e5dbc <+108>: add (%rax),%al
0x00000000004e5dbe <+110>: add %cl,-0x73(%rax)
0x00000000004e5dc1 <+113>: adc $0x3bb12a,%eax
0x00000000004e5dc6 <+118>: lea 0x3bb11d(%rip),%rsi # 0x8a0eea <touni+9418>
0x00000000004e5dd9 <+137>: add (%rax),%al
0x00000000004e5ddb <+139>: add %ch,%al
0x00000000004e5ddd <+141>: pop %rdi
0x00000000004e5dde <+142>: xchg %edi,%esi
0x00000000004e5ded <+157>: movabs 0x4810438d48000002,%al

这就更复杂了,需要手动解析出taf::CoroutineScheduler::sleep的1326行汇编代码

手动看下二进制的符号地址

1
2
3
~ readelf -Ws ~/TafStressServer | grep CoroutineScheduler|grep sleep
534: 0000000000875720 107 OBJECT LOCAL DEFAULT 18 _ZZN3taf18CoroutineScheduler5sleepEiSt8functionIFvNS_12SuspendEntryEPS0_EEE19__PRETTY_FUNCTION__
11264: 00000000004e5d50 342 FUNC GLOBAL DEFAULT 14 _ZN3taf18CoroutineScheduler5sleepEiSt8functionIFvNS_12SuspendEntryEPS0_EE

有两个符号,应该是下面的那个,验证下

1
2
~ c++filt _ZN3taf18CoroutineScheduler5sleepEiSt8functionIFvNS_12SuspendEntryEPS0_EE
taf::CoroutineScheduler::sleep(int, std::function<void (taf::SuspendEntry, taf::CoroutineScheduler*)>)

因此符号地址从00000000004e5d50开始,用objdump解析这个地址前100行汇编里面,符合1326行的代码:

  • -d:展示汇编
  • -l:展示行号
  • -C:打印出C++的可读符号
1
2
3
4
5
6
7
8
9
~ objdump -dl -C --start-address=0x4e5d50 ~/TafStressServer|head -n 100|grep -A 1 cpp:1326
/root/taf/src/libservant/CoroutineScheduler.cpp:1326
4e5dbc: 66 66 66 64 48 8b 04 data16 data16 data16 mov %fs:0x0,%rax
--
/root/taf/src/libservant/CoroutineScheduler.cpp:1326
4e5dd9: 48 8b 80 90 fa ff ff mov -0x570(%rax),%rax
--
/root/taf/src/libservant/CoroutineScheduler.cpp:1326
4e5ded: 48 89 44 24 08 mov %rax,0x8(%rsp)

这个0x570正是O0下的输出,可以发现,O0下比O2要拿到fs寄存器的偏移量复杂太多了

因此自己解析的缺点很明显了,不太通用,O0和O2是两套解析机制,而且依赖人工找目标线程私有变量函数的父函数

万一哪一天父函数不调用了,那解析就出错了

方案二:辅助函数(只对运行中进程有效)

gdb optimized out values when using __thread

简单说就是搞个全局函数用来打印数据,然后gdb运行中的时候调用这个函数

1
2
3
4
5
6
__thread long thread_cand;

long what_is_thread_cand()
{
return thread_cand;
}

然后gdb运行中时:

1
2
3
4
5
(gdb) p thread_cand
Cannot find thread-local storage for process 6472, executable file /home/ubuntu/tls:
Cannot find thread-local variables on this target
(gdb) p what_is_thread_cand()
$1 = 432

这个方案对于coredump场景无效

方案三:动态链接的gdb

这个方案最无脑,直接用系统自带的gdb,直接打印

1
2
3
4
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
(gdb) p/x 'taf::CoroutineScheduler::currentScheduler()::tlsScheduler'
$1 = 0x3a69e00

落地自动化解析脚本

初版解析脚本包含了3个子脚本

  • gdb_co_stack.py

    这是静态gdb使用的python脚本,用来执行切换协程等命令

  • gdb_collect_tls.py

    这是用来启动系统的gdb获取每个线程的调度器线程私有变量地址的

  • gdb_co_stack_helper.py

    这是入口脚本,用来粘合gdb_co_stack.py和gdb_collect_tls.py

    导出gdb_collect_tls.py获取的线程私有变量地址

    导入到gdb_co_stack.py供其使用

gdb_co_stack.py

这是最核心的python脚本

简单介绍下python的gdb的api,剩下来的逻辑就很容易理解了:

  • gdb.Command:用于定义并注册一个新的 gdb 命令(例如 co_load / co_list
  • gdb.Command.__init__(name, command_class):构造并注册命令
    • name 是命令名(gdb 里直接输入这个名字调用)
    • command_class 用的是 gdb.COMMAND_USER,表示“用户自定义命令”分类
  • gdb.Command.invoke(self, arg, from_tty):命令真正执行的入口
    • arg 是命令后面跟着的原始字符串(需要自己解析成参数)
    • from_tty 表示是否从交互终端触发(这段脚本里基本没用到)
  • gdb.GdbError:用于向 gdb 抛出“用户可读”的错误(比直接抛 Python 异常更适合命令用法/缺少前置条件等场景)
    • 例如:没选线程、没加载 map、参数缺失等都会 raise gdb.GdbError(...)
  • gdb.parse_and_eval(expr):执行一段“gdb 表达式字符串”,返回 gdb.Value
    • 用它读取寄存器:"$rip" "$rsp" "$rbp"
    • 也用它写寄存器:"$rip = <值>" 这种赋值表达式
    • 失败会抛异常(比如表达式不合法、当前架构没有该寄存器名等)
  • gdb.selected_thread():获取当前在 gdb 中选中的线程对象(未选中则可能为 None
    • 使用 thr.num:这是 gdb 的线程编号(对应 thread N 里的 N),用来在 _tls_map 里查找该线程的 tlsScheduler 地址
  • 下面的_eval_ptr函数为了把一个“裸地址”变成 taf::CoroutineScheduler*用了好几个api:
    • gdb.lookup_type(type_name):按名字查找调试信息里的类型,返回 gdb.Type
      • 用它找 "taf::CoroutineScheduler""unsigned long"
      • 找不到类型会抛异常(常见于符号没加载、类型名不对、被裁剪等)
    • gdb.Type.pointer():从一个类型构造其“指针类型”
    • gdb.Value(x):把一个 Python 值(通常是 int)包装成 gdb.Value
      • 用它把“裸地址”先变成 gdb.Value(addr),再 cast 成目标指针类型
    • gdb.Value.cast(gdb_type):强制类型转换,返回一个新的 gdb.Value
      • 用它把裸地址转换成 taf::CoroutineScheduler*
      • 也用它把各种值转成 unsigned long,再转成 Python int(用于统一拿地址/寄存器数值)
  • gdb.Value.dereference():解引用指针(T* -> T
    • 用它把 scheduler 指针变成 scheduler 对象、把链表节点指针变成节点对象、把 _ctx_to 指针变成 fcontext_t 对象等
  • gdb.Value.address:取某个对象的地址(&obj),返回指针类型的 gdb.Value
    • 用它获取 head 哨兵节点地址、ctx 的地址、CoroutineInfo 节点地址等用于比较/打印
  • gdb.Value.__getitem__(也就是 v["field"] / v[i] 这种语法):
    • v["field_name"]:访问结构体/类字段(等价于 C/C++ 里按字段名取成员;具体是 . 还是 ->v 的类型决定)
      • 用它访问:sched["_timeout"]head["_next"]coinfo["_ctx_to"]ctx["fc_greg"]
    • v[i]:访问数组/指针下标
      • 用它访问 greg[FC_GREG_RBP] / [FC_GREG_RSP] / [FC_GREG_RIP]
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# -*- coding: utf-8 -*-
# gdb_co_stack.py
#
# 在静态版 gdb 内的使用方法:
# (gdb) source gdb_co_stack.py # 加载本脚本,注册自定义命令
# (gdb) co_load /tmp/co_tls_map.json # 载入“线程 -> tlsScheduler地址”的映射表
# (gdb) co_threads # 查看每个 gdb 线程对应的 tlsScheduler 地址
# (gdb) thread <N> # 先切到某个 OS 线程(gdb thread)
# (gdb) co_list # 列出该线程调度器 _timeout 链表中的“休眠协程”
# (gdb) co_frame <idx> # 将寄存器切换到指定协程的上下文
# (gdb) bt # 现在 bt/up/down 看到的是协程栈
# (gdb) co_restore # 恢复切换前保存的寄存器
#
# Python 版本要求:3.5+

from __future__ import print_function
import gdb
import json

SCHED_TYPE = "taf::CoroutineScheduler"

# fcontext_t::fc_greg 的寄存器下标映射
FC_GREG_RBP = 5
FC_GREG_RSP = 6
FC_GREG_RIP = 7

# 遍历链表的最大步数保护:防止链表损坏导致死循环
MAX_LIST_WALK = 20000


# ---- Global state ----
_tls_map = {} # key: gdb 线程号(int) -> 调度器地址(int)
_status = False
_saved_regs = {}
_cached_coros = [] # list of dict: { "coinfo": gdb.Value, "coinfo_addr": int, "ctx": gdb.Value, "rbp/rsp/rip": int }

def _to_int(val):
# gdb.Value -> int(用于指针/整数类型)
return int(val.cast(gdb.lookup_type("unsigned long")))

# ---- Debug wrapper for parse_and_eval ----
_PARSE_EVAL_TRACE = True

def _pe(expr, *, want_int=False):
"""
Wrapper of gdb.parse_and_eval:
- print expr and result (or error)
- optionally return int value
"""
if _PARSE_EVAL_TRACE:
print("[parse_and_eval] expr:", expr)
try:
v = gdb.parse_and_eval(expr)
if _PARSE_EVAL_TRACE:
# gdb.Value 打印时通常会显示类型和值
print("[parse_and_eval] ret :", v)
if want_int:
return _to_int(v)
return v
except Exception as e:
if _PARSE_EVAL_TRACE:
print("[parse_and_eval] error:", repr(e))
raise

def _get_thread_num():
try:
thr = gdb.selected_thread()
if thr is None:
return None
return thr.num
except Exception:
return None

def _save_regs():
global _saved_regs
_saved_regs = {
"rip": _pe("$rip", want_int=True),
"rsp": _pe("$rsp", want_int=True),
"rbp": _pe("$rbp", want_int=True),
}

def _restore_regs():
if not _saved_regs:
return
_pe("$rip = {}".format(_saved_regs["rip"]))
_pe("$rsp = {}".format(_saved_regs["rsp"]))
_pe("$rbp = {}".format(_saved_regs["rbp"]))

def _get_scheduler_addr_for_selected_thread():
tnum = _get_thread_num()
if tnum is None:
raise gdb.GdbError("No selected thread")
if tnum not in _tls_map:
raise gdb.GdbError("No tlsScheduler for thread {}. Run helper or co_load map.".format(tnum))
return _tls_map[tnum]

def _eval_ptr(type_name, addr):
t = gdb.lookup_type(type_name).pointer()
return gdb.Value(addr).cast(t)

def _walk_timeout_list(sched_addr):
"""Return list of CoroutineInfo nodes in scheduler->_timeout intrusive list."""
sched = _eval_ptr(SCHED_TYPE, sched_addr).dereference()
head = sched["_timeout"]
# head is a CoroutineInfo object (not pointer)
head_addr = _to_int(head.address)

cur_ptr = head["_next"] # CoroutineInfo*
res = []
steps = 0
while True:
steps += 1
if steps > MAX_LIST_WALK:
raise gdb.GdbError("List walk exceeded MAX_LIST_WALK={}, maybe corrupted".format(MAX_LIST_WALK))

cur_addr = _to_int(cur_ptr)
if cur_addr == 0:
break
if cur_addr == head_addr:
break

cur = cur_ptr.dereference() # CoroutineInfo
res.append(cur)
cur_ptr = cur["_next"]
return res

def _get_ctx_regs(coinfo):
# coinfo._ctx_to is fcontext_t*
ctx_ptr = coinfo["_ctx_to"]
if int(ctx_ptr) == 0:
return None
ctx = ctx_ptr.dereference()
greg = ctx["fc_greg"]
rbp = _to_int(greg[FC_GREG_RBP])
rsp = _to_int(greg[FC_GREG_RSP])
rip = _to_int(greg[FC_GREG_RIP])
return (ctx, rbp, rsp, rip)

def _rebuild_cached_coros():
global _cached_coros
_cached_coros = []

sched_addr = _get_scheduler_addr_for_selected_thread()
coros = _walk_timeout_list(sched_addr)
for c in coros:
regs = _get_ctx_regs(c)
if regs is None:
continue
ctx, rbp, rsp, rip = regs
_cached_coros.append({
"coinfo": c,
"coinfo_addr": _to_int(c.address),
"ctx": ctx,
"rbp": rbp,
"rsp": rsp,
"rip": rip,
})

class CoLoadCmd(gdb.Command):
"""co_load <json_path>: load tlsScheduler map generated by helper"""
def __init__(self):
super(CoLoadCmd, self).__init__("co_load", gdb.COMMAND_USER)

def invoke(self, arg, from_tty):
global _tls_map
path = arg.strip()
if not path:
raise gdb.GdbError("Usage: co_load <json_path>")
with open(path, "r") as f:
data = json.load(f)

# data: {"threads": [{"gdb_thread_num":1,"lwp":123,"tlsScheduler":"0x..."}], ...}
m = {}
for item in data.get("threads", []):
tnum = int(item["gdb_thread_num"])
addr = int(item["tlsScheduler"], 16) if isinstance(item["tlsScheduler"], str) else int(item["tlsScheduler"])
if addr != 0:
m[tnum] = addr
_tls_map = m
print("Loaded tlsScheduler map: {} threads".format(len(_tls_map)))

class CoThreadsCmd(gdb.Command):
"""co_threads: show tlsScheduler address for each gdb thread"""
def __init__(self):
super(CoThreadsCmd, self).__init__("co_threads", gdb.COMMAND_USER)

def invoke(self, arg, from_tty):
if not _tls_map:
print("No tlsScheduler map. Run helper or `co_load` first.")
return
# Print sorted by thread num
for tnum in sorted(_tls_map.keys()):
print("thread {:<4} tlsScheduler=0x{:x}".format(tnum, _tls_map[tnum]))

class CoListCmd(gdb.Command):
"""co_list: list coroutines in current thread scheduler->_timeout list"""
def __init__(self):
super(CoListCmd, self).__init__("co_list", gdb.COMMAND_USER)

def invoke(self, arg, from_tty):
global _status
if not _status:
_save_regs()
_status = True

_rebuild_cached_coros()

print("{:<4} {:<18} {:<18} {:<18} {:<18} {:<18} (total={})".format(
"idx", "coinfo", "ctx", "rip", "rsp", "rbp", len(_cached_coros)
))

for i, it in enumerate(_cached_coros):
print("{:<4} 0x{:016x} 0x{:016x} 0x{:016x} 0x{:016x} 0x{:016x}".format(
i,
it["coinfo_addr"],
_to_int(it["ctx"].address),
it["rip"], it["rsp"], it["rbp"]
))

class CoFrameCmd(gdb.Command):
"""co_frame <idx>: switch gdb registers to selected coroutine (for bt/up/down)"""
def __init__(self):
super(CoFrameCmd, self).__init__("co_frame", gdb.COMMAND_USER)

def invoke(self, arg, from_tty):
global _status
if not _status:
_save_regs()
_status = True

if not arg.strip():
raise gdb.GdbError("Usage: co_frame <idx> (see co_list)")
idx = int(arg.strip())
if idx < 0 or idx >= len(_cached_coros):
raise gdb.GdbError("idx out of range, run co_list first")

it = _cached_coros[idx]
_pe("$rbp = {}".format(it["rbp"]))
_pe("$rsp = {}".format(it["rsp"]))
_pe("$rip = {}".format(it["rip"]))
print("Switched to coroutine idx={}, coinfo=0x{:x}".format(idx, it["coinfo_addr"]))

class CoRestoreCmd(gdb.Command):
"""co_restore: restore registers saved at first co_list/co_frame call"""
def __init__(self):
super(CoRestoreCmd, self).__init__("co_restore", gdb.COMMAND_USER)

def invoke(self, arg, from_tty):
global _status
if not _status:
print("Not in coroutine debug mode")
return
_restore_regs()
_status = False
print("Registers restored")

CoLoadCmd()
CoThreadsCmd()
CoListCmd()
CoFrameCmd()
CoRestoreCmd()

gdb_collect_tls.py

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
47
48
49
50
51
52
53
# -*- coding: utf-8 -*-
from __future__ import print_function
import gdb
import json

TLS_EXPR = "'taf::CoroutineScheduler::currentScheduler()::tlsScheduler'"

class CollectTlsCmd(gdb.Command):
"""collect_tls <out_json_path>"""

def __init__(self):
super(CollectTlsCmd, self).__init__("collect_tls", gdb.COMMAND_USER)

def invoke(self, arg, from_tty):
argv = gdb.string_to_argv(arg)
if len(argv) != 1:
raise gdb.GdbError("Usage: collect_tls <out_json_path>")

out_path = argv[0]
res = {"threads": []}
inferior = gdb.selected_inferior()
ulong_t = gdb.lookup_type("unsigned long")

for t in inferior.threads():
entry = {}
try:
t.switch()
entry["gdb_thread_num"] = int(t.num)
try:
entry["lwp"] = int(t.ptid[1]) if t.ptid and len(t.ptid) > 1 else 0
except Exception:
entry["lwp"] = 0

v = gdb.parse_and_eval(TLS_EXPR)
entry["tlsScheduler"] = hex(int(v.cast(ulong_t)))
except Exception as e:
if "gdb_thread_num" not in entry:
try: entry["gdb_thread_num"] = int(t.num)
except Exception: entry["gdb_thread_num"] = -1
if "lwp" not in entry:
try: entry["lwp"] = int(t.ptid[1]) if t.ptid and len(t.ptid) > 1 else 0
except Exception: entry["lwp"] = 0
entry["tlsScheduler"] = "0x0"
entry["error"] = str(e)

res["threads"].append(entry)

with open(out_path, "w") as f:
json.dump(res, f, indent=2, sort_keys=True)

gdb.write("Wrote TLS map to {}\n".format(out_path))

CollectTlsCmd()

每一个entry都代表了一个线程,gdb_thread_num和lwp直接使用gdb的api即可

而tlsScheduler则是通过parse_and_eval打印'taf::CoroutineScheduler::currentScheduler()::tlsScheduler'的值来输出

这个输出的文件内容是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cat /tmp/co_tls_map_mz_hk4mi.json
{
"threads": [
{
"gdb_thread_num": 48,
"lwp": 249785,
"tlsScheduler": "0x3a68a00"
},
{
"gdb_thread_num": 47,
"lwp": 249775,
"tlsScheduler": "0x0"
},
...
{
"gdb_thread_num": 1,
"lwp": 249788,
"tlsScheduler": "0x3a69e00"
}
]
}

gdb_co_stack_helper.py

这个脚本定下来了如何启动动态编译的gdb,以及静态编译的gdb

并且为两个版本gdb分别指定使用gdb_collect_tls.py和gdb_co_stack.py

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# -*- coding: utf-8 -*-
# gdb_co_stack_helper.py
# Python 3.5+

from __future__ import print_function
import argparse
import os
import subprocess
import tempfile
import sys

def run_system_gdb_collect(bin_path, core_path, gdb_collect_tls_py, out_json):
gdb_cmd = [
"gdb",
"-q",
bin_path,
core_path,
"-ex", "set pagination off",
"-ex", "set print pretty on",
"-ex", "set auto-load safe-path /",
"-ex", "source {}".format(gdb_collect_tls_py),
"-ex", "collect_tls {}".format(out_json),
"-ex", "quit",
]
return subprocess.call(gdb_cmd)

def run_static_gdb(static_gdb, bin_path, core_path, co_py, json_path):
gdb_cmd = [
static_gdb,
"-q",
"-iex", "set pagination off",
"-iex", "set auto-load safe-path /",
"-iex", "source {}".format(co_py),
"-iex", "co_load {}".format(json_path),
bin_path,
core_path,
]
os.execv(static_gdb, gdb_cmd)

def main():
ap = argparse.ArgumentParser()
ap.add_argument("--static-gdb", required=True, help="path to your static gdb binary (patched for core set $reg)")
ap.add_argument("--bin", required=True, help="path to executable")
ap.add_argument("--core", required=True, help="path to core file")
ap.add_argument("--co-py", required=True, help="path to gdb_co_stack.py (loaded by static gdb)")
ap.add_argument("--collect-py", required=True, help="path to gdb_collect_tls.py (loaded by system gdb)")
ap.add_argument("--keep-json", action="store_true", help="do not delete tmp json (default: keep anyway)")
args = ap.parse_args()

for p, name in [(args.static_gdb, "static-gdb"),
(args.bin, "bin"),
(args.core, "core"),
(args.co_py, "co-py"),
(args.collect_py, "collect-py")]:
if not os.path.exists(p):
print("{} not found: {}".format(name, p), file=sys.stderr)
return 2

fd, tmp_json = tempfile.mkstemp(prefix="co_tls_map_", suffix=".json")
os.close(fd)

rc = run_system_gdb_collect(args.bin, args.core, args.collect_py, tmp_json)
if rc != 0:
print("system gdb collect failed, rc={}".format(rc), file=sys.stderr)
if not args.keep_json:
try:
os.unlink(tmp_json)
except Exception:
pass
return rc

print("TLS map written:", tmp_json)
print("Launching static gdb ...")
run_static_gdb(args.static_gdb, args.bin, args.core, args.co_py, tmp_json)
return 0

if __name__ == "__main__":
sys.exit(main())

用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
~ sudo python3 gdb_co_stack_helper.py --static-gdb ~/gdb --bin ~/TafStressServer --core ~/core-handle-HUYASZ.T-249762-1764510106 --co-py gdb_co_stack.py --collect-py gdb_collect_tls.py
Reading symbols from /root/TafStressServer...done.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `/data/app/taf/tafnode/data/HUYASZ.TafStressServer/bin/TafStressServer --config='.
Program terminated with signal SIGABRT, Aborted.
#0 0x00007f6f4d17d428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
54 ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
[Current thread is 1 (Thread 0x7f6f3f9eb700 (LWP 249788))]
Wrote TLS map to /tmp/co_tls_map_mz_hk4mi.json
TLS map written: /tmp/co_tls_map_mz_hk4mi.json

Launching static gdb ...
Loaded tlsScheduler map: 24 threads
Reading symbols from /root/TafStressServer...
warning: Unable to find libthread_db matching inferior's thread library, thread debugging will not be available.
Program terminated with signal SIGABRT, Aborted.
#0 0x00007f6f4d17d428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
[Current thread is 1 (LWP 249788)]

来看下有哪些线程存在协程

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
(gdb) co_threads
thread 1 tlsScheduler=0x3a69e00
thread 15 tlsScheduler=0x39e3e00
thread 16 tlsScheduler=0x39e2a00
thread 18 tlsScheduler=0x3a6b200
thread 19 tlsScheduler=0x39e3400
thread 20 tlsScheduler=0x3a68000
thread 21 tlsScheduler=0x3a69400
thread 22 tlsScheduler=0x3a6a800
thread 23 tlsScheduler=0x39e2000
thread 25 tlsScheduler=0x14e1d400
thread 26 tlsScheduler=0x14e1ca00
thread 28 tlsScheduler=0x3a71e00
thread 30 tlsScheduler=0x14e1de00
thread 31 tlsScheduler=0x14e1f200
thread 32 tlsScheduler=0x3a71400
thread 35 tlsScheduler=0x39e4800
thread 36 tlsScheduler=0x14e2e000
thread 38 tlsScheduler=0x3a70000
thread 40 tlsScheduler=0x14e1c000
thread 41 tlsScheduler=0x14e1e800
thread 43 tlsScheduler=0x14e2ea00
thread 45 tlsScheduler=0x3a70a00
thread 46 tlsScheduler=0x39e5200
thread 48 tlsScheduler=0x3a68a00

选择线程40来切换协程,并co_list查看有哪些协程

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) thread 40
[Switching to thread 40 (LWP 605631)]
#0 pthread_cond_timedwait@@GLIBC_2.3.2 () at ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_timedwait.S:225
warning: 225 ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_timedwait.S: No such file or directory
(gdb) co_list
[parse_and_eval] expr: $rip
[parse_and_eval] ret : 0x7f6f4ddc0709 <pthread_cond_timedwait@@GLIBC_2.3.2+297>
[parse_and_eval] expr: $rsp
[parse_and_eval] ret : 0x7f6f399de790
[parse_and_eval] expr: $rbp
[parse_and_eval] ret : 0x14e1c198
idx coinfo ctx rip rsp rbp (total=1)
0 0x0000000014e68000 0x0000000014ed8fa0 0x00000000004df73d 0x0000000014ed8bf0 0x0000000014e1c030

接下来切换到协程id为0的协程

1
2
3
4
5
6
7
8
(gdb) co_frame 0
[parse_and_eval] expr: $rbp = 350339120
[parse_and_eval] ret : 0x14e1c030
[parse_and_eval] expr: $rsp = 351112176
[parse_and_eval] ret : 0x14ed8bf0
[parse_and_eval] expr: $rip = 5109565
[parse_and_eval] ret : 0x4df73d <taf::CoroutineInfo::switchTo(taf::CoroutineInfo*)+109>
Switched to coroutine idx=0, coinfo=0x14e68000

切换成功了,查看堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(gdb) bt
#0 taf::CoroutineInfo::switchTo (this=0x14e1c000, to=0x14e1c030, to@entry=0x7f6f399de840) at /root/taf/src/libservant/CoroutineScheduler.cpp:353
#1 0x00000000004df805 in taf::CoroutineScheduler::switchCoro (this=this@entry=0x14e1c000, from=<optimized out>, to=to@entry=0x7f6f399de840)
at /root/taf/src/libservant/CoroutineScheduler.cpp:1539
#2 0x00000000004e5e44 in taf::CoroutineScheduler::sleep (this=0x14e1c000, iSleepTime=iSleepTime@entry=10000, cb=...)
at /root/taf/src/libservant/CoroutineScheduler.cpp:1333
#3 0x0000000000430412 in foo () at StressImp.cpp:143
#4 0x0000000000438463 in std::function<void()>::operator() (this=0x1d4424b0) at /usr/include/c++/5/functional:2267
#5 taf::CoroutineAwaitRunner::invoke<void>(std::function<void ()>, taf::TC_AutoPtr<taf::CoroutineResource<void> > const&)::{lambda()#1}::operator()() const
(__closure=0x1d4424b0) at /usr/local/taf-version/taf-3.4.6.3-6b6b59/include/servant/AwaitRun.h:432
#6 std::_Function_handler<void (), taf::CoroutineAwaitRunner::invoke<void>(std::function<void ()>, taf::TC_AutoPtr<taf::CoroutineResource<void> > const&)::{lambda()#1}>::_M_invoke(std::_Any_data const&) (__functor=...) at /usr/include/c++/5/functional:1871
#7 0x0000000000432955 in std::function<void()>::operator() (this=0x14d18b00) at /usr/include/c++/5/functional:2267
#8 taf::CoroutineAwaitRunner::doCall<void>(std::function<void ()>, taf::CoroutineResource<void>*)::{lambda()#1}::operator()() (__closure=0x14d18b00)
at /usr/local/taf-version/taf-3.4.6.3-6b6b59/include/servant/AwaitRun.h:462
#9 std::_Function_handler<void (), taf::CoroutineAwaitRunner::doCall<void>(std::function<void ()>, taf::CoroutineResource<void>*)::{lambda()#1}>::_M_invoke(std::_Any_data const&) (__functor=...) at /usr/include/c++/5/functional:1871
#10 0x00000000004e649d in std::function<void()>::operator() (this=0x14ed8e60) at /usr/include/c++/5/functional:2267
#11 taf::CoroutineInfo::corotineProc (args=args@entry=0x14e68000) at /root/taf/src/libservant/CoroutineScheduler.cpp:424
#12 0x00000000004e1462 in taf::CoroutineInfo::corotineEntry (q=<optimized out>) at /root/taf/src/libservant/CoroutineScheduler.cpp:402

和手动切换的效果一致~

然后切换回线程的堆栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(gdb) co_restore 
[parse_and_eval] expr: $rip = 140116024362761
[parse_and_eval] ret : 0x7f6f4ddc0709 <pthread_cond_timedwait@@GLIBC_2.3.2+297>
[parse_and_eval] expr: $rsp = 140115684747152
[parse_and_eval] ret : 0x7f6f399de790
[parse_and_eval] expr: $rbp = 350339480
[parse_and_eval] ret : 0x14e1c198
Registers restored

(gdb) bt
#0 pthread_cond_timedwait@@GLIBC_2.3.2 () at ../sysdeps/unix/sysv/linux/x86_64/pthread_cond_timedwait.S:225
#1 0x00000000004e4e62 in taf::TC_ThreadCond::timedWait<taf::TC_ThreadMutex> (millsecond=<optimized out>, mutex=..., this=0x322ba10)
at /root/taf/include/util/tc_thread_cond.h:97
#2 taf::TC_Monitor<taf::TC_ThreadMutex, taf::TC_ThreadCond>::timedWait (millsecond=<optimized out>, this=0x322ba00)
at /root/taf/include/util/tc_monitor.h:115
#3 taf::CoroutineScheduler::run (this=0x14e1c000, isLoop=isLoop@entry=true) at /root/taf/src/libservant/CoroutineScheduler.cpp:1171
#4 0x00000000004347e6 in taf::CoroutineAwaitRunner::CoroutineAwaitRunner(int, int, int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)::{lambda()#2}::operator()() const (__closure=<optimized out>) at /usr/local/taf-version/taf-3.4.6.3-6b6b59/include/servant/AwaitRun.h:366
#5 std::_Bind_simple<taf::CoroutineAwaitRunner::CoroutineAwaitRunner(int, int, int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)::{lambda()#2} ()>::_M_invoke<>(std::_Index_tuple<>) (this=<optimized out>) at /usr/include/c++/5/functional:1531
#6 std::_Bind_simple<taf::CoroutineAwaitRunner::CoroutineAwaitRunner(int, int, int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)::{lambda()#2} ()>::operator()() (this=<optimized out>) at /usr/include/c++/5/functional:1520
#7 std::thread::_Impl<std::_Bind_simple<taf::CoroutineAwaitRunner::CoroutineAwaitRunner(int, int, int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)::{lambda()#2} ()> >::_M_run() (this=0x3224320) at /usr/include/c++/5/thread:115
#8 0x00007f6f4dae9c80 in ?? () from /usr/lib/x86_64-linux-gnu/libstdc++.so.6
#9 0x00007f6f4ddba6ba in start_thread (arg=0x7f6f399df700) at pthread_create.c:333
#10 0x00007f6f4d24f41d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:109

进阶功能

主要是对gdb_co_stack.py这个用户主要使用的脚本,存在可以进一步改进的地方,出于篇幅关系,就不在这里叙述了

  • 博客简化了协程调度器:

    不止存在一个超时队列,实际上是存在四个队列

    1
    2
    3
    4
    5
    6
    class CoroutineScheduler {    
    CoroutineInfo _active; //从inactive被唤醒的,即将被调度协程
    CoroutineInfo _avail; //从inactive自唤醒的,即将被调度协程
    CoroutineInfo _inactive; //等待唤醒的,当前不需要调度协程
    CoroutineInfo _timeout; //等待超时事件的,当前不需要调度协程
    }

    co_list应当展示四个队列的全部协程,合用同一个顺序ID,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    (gdb) co_list
    active: 从inactive被唤醒的,即将被调度协程
    avail: 从inactive自唤醒的,即将被调度协程(框架协程,不用看)
    inactive: 等待唤醒的,当前不需要调度协程
    timeout: 等待超时事件的,当前不需要调度协程
    idx type coinfo ctx rip rsp rbp (total=2)
    0 active 0x0000000003bc4160 0x0000000003e54fa0 0x00000000004e145d 0x0000000003e54ec0 0x0000000003bc4160
    1 avail 0x0000000003c51600 0x0000000003daafa0 0x00000000004df73d 0x0000000003daac10 0x0000000003a69e30
  • co_thread功能比较薄弱,大部分线程是不存在活跃协程的

    应该重点打印业务需要关心的线程,以及每个线程有多少可切换的协程

    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
    (gdb) co_threads
    active: 从inactive被唤醒的,即将被调度协程
    avail: 从inactive自唤醒的,即将被调度协程(框架协程,不用看)
    inactive: 等待唤醒的,当前不需要调度协程
    timeout: 等待超时事件的,当前不需要调度协程
    thread_id tlsScheduler total active avail inactive timeout
    以下线程存在业务协程,可关注:
    1 0x0000000003a69e00 2 0 1 1 0
    40 0x0000000014e1c000 1 0 0 0 1
    以下线程仅存在avail框架协程,可忽略:
    15 0x00000000039e3e00 1 0 1 0 0
    16 0x00000000039e2a00 1 0 1 0 0
    18 0x0000000003a6b200 1 0 1 0 0
    19 0x00000000039e3400 1 0 1 0 0
    20 0x0000000003a68000 1 0 1 0 0
    21 0x0000000003a69400 1 0 1 0 0
    22 0x0000000003a6a800 1 0 1 0 0
    23 0x00000000039e2000 1 0 1 0 0
    28 0x0000000003a71e00 1 0 1 0 0
    32 0x0000000003a71400 1 0 1 0 0
    35 0x00000000039e4800 1 0 1 0 0
    38 0x0000000003a70000 1 0 1 0 0
    45 0x0000000003a70a00 1 0 1 0 0
    46 0x00000000039e5200 1 0 1 0 0
    48 0x0000000003a68a00 1 0 1 0 0
    以下线程不存在任何协程,可忽略:
    25 0x0000000014e1d400 0 0 0 0 0
    26 0x0000000014e1ca00 0 0 0 0 0
    30 0x0000000014e1de00 0 0 0 0 0
    31 0x0000000014e1f200 0 0 0 0 0
    36 0x0000000014e2e000 0 0 0 0 0
    41 0x0000000014e1e800 0 0 0 0 0
    43 0x0000000014e2ea00 0 0 0 0 0

参考资料