View on GitHub

开源开发工具技术博客

编译器/工具链、模拟器、虚拟机、Linux内核、调试和性能分析工具

在这个部分,我们要介绍signal trampoline frame对调试过程的影响,以及调试器如何处理signal trampoline frame。

1. signal trampoline

当程序被信号中断,它的状态会被保存,当信号处理函数执行完毕以后,程序可以恢复到它之前被中断的点上继续执行。这就意味着从信号处理函数返回要比从一般的函数返回复杂。Linux内核安排信号处理函数在返回的时候,跳转到一小段代码,这段代码会最终执行sigreturn或者rt sigreturn, 来执行到程序之前被中断的点。这一小段代码就叫做signal trampoline。

这样说听着有些玄妙,我们看看内核是如何使用signal trampoline, 并保证信号处理函数返回到signal trampoline。

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
/*
* handle the actual delivery of a signal to userspace
*/
static int handle_signal(int sig, siginfo_t *info, struct k_sigaction *ka,
                         sigset_t *oldset, struct pt_regs *regs,
int syscall)
{
/* ... */
ret = setup_frame(sig, ka, oldset, regs);
}
 
static int setup_frame(int signr, struct k_sigaction *ka,
sigset_t *set, struct pt_regs *regs)
{
  /* ... */
  retcode = (unsigned long *) frame->;retcode;
  put_user(0x00003BAAUL, retcode++); /* MVK 139,B0 ; __NR_rt_sigreturn in B0 */
  put_user(0x10000000UL, retcode++); /* SWE */
  put_user(0x00006000UL, retcode++); /* NOP 4 */
  put_user(0x00006000UL, retcode++); /* NOP 4 */
  put_user(0x00006000UL, retcode++); /* NOP 4 */
  put_user(0x00006000UL, retcode++); /* NOP 4 */
  put_user(0x00006000UL, retcode++); /* NOP 4 */
 
  /* Change user context to branch to signal handler */
  regs->sp = (unsigned long) frame - 8;
  regs->b3 = (unsigned long) retcode;
  regs->pc = (unsigned long) ka->sa.sa_handler;
 
  /* Give the signal number to the handler */
  regs->a4 = signr;
  regs->b4 = (unsigned long) &frame->sc;
 
  /* ... */
}

我们看到函数handle signal调用了setup frame,其中setup frame设置了使得信号处理函数返回到signal trampoline (函数返回值的地址在寄存器B3)。这样当信号处理函数执行完毕,就会自动的转到signaltrampoline,最后调用sigreturn返回。这里可以看出,signal trampoline完全处于正常程序和信号处理函数之间,如果调试器不能正确识别signal trampoline,很多调式功能在信号处理函数上,就会有问题。

2. signal trampoline给调试带来的问题

在知道了signal trampoline是什么以后,我们来描述一下signal trampoline给调式带来的问题。例如我们现在有个正常函数func,在执行到一半的时候,受到信号, 进而执行信号处理函数handler, 假如我们在handler上设置了断点,有一些调试操作是不能正确执行的,

1. 查看函数调用堆栈。实际上,当前的函数调用栈包括:正常函数func,signaltrampoline的一些信息,信号处理函数handler。其中func和handler都是一般的函数 ,所以它们的frame都可以容易处理,但是signal trampoline是很特殊的一段代码,它的frame不能按照通常的frame来处理。

2. 命令next。命令next的意思是执行程序到下一行,如果程序当前已经在函数最后一行,那么这个命令应该使得程序执行一段停止在调用这个函数的下一句。这些是我们在调试器手册看到的解释,但是调试器内部怎么理解这样的条件呢?调试器会根据不同frame之间的关系(inner than 或者 outter than),来判断这些条件(调试器内部的frame管理,是一个很复杂的模块,对于不熟悉frame和unwinding的读者,可以暂时忽略frame和unwinding的内部原理)。如果调试器不能识别signal trampoline,这些命令在信号处理函数的尾部,就无法使用。

3 支持signal trampoline

在上边分析完signal trampoline给调试带来的问题后,我们把支持signal trampoline总结为两个部分

  1. 识别signal trampoline。调试器首先能够在代码里边识别signal trampoline。因为signal trampoline都是很特别而且很短的代码,所以调试器可以匹配代码指令,就能找到signal trampoline。
  2.  unwind signal trampoline frame。我们上边说过,signal trampoline的frame联系着正常函数和信号处理函数,如果调试器希望在堆栈上正确的找到他们各自的内容,就需要正确分析signal trampoline的frame(对于不清楚frame unwinding的读者,可以暂时认为frame unwinding就是递归地找到当前函数的caller的寄存器保存的位置和值)。所以,对于signal trampoline frame,我们也需要找到保存它的caller(由于从signal trampoline后就进入sigreturn,进而回到原来正常函数,我们可以认为signal trampoline的caller就是原来的正常函数)的寄存器的位置。

识别signal trampoline就是一个对指令进行模式匹配的过程,如果指令匹配上了一个模式,这个几条指令就是signal trampoline。如所示,我们可以把指令模式定义如下,

我这里不打算具体去讲这个结构的定义和含义(结构tramp_frame也是GDB内部的数据结构),只展示如何定义自己的指令模式,以识别signal trampoline。在定义完这个指令模式后,第一个问题就解决了,下来我们看第二个问题,如何找到caller的寄存器的位置和值
。当函数执行收到信号的时候,内核会把进程当前状态保存在堆栈上某个位置,进而在恢复的,从堆栈上得到。我们这里的工作就是,查看内核源代码,得到寄存器的保存位置,然后我们就可以得到这些寄存器的值。在这里,“获得寄存器的值”还是有些模糊的,在这个环境中,我们可以把获得寄存器分为两个部分:sp,fp和其他寄存器。在函数调用的过程中,寄存器都是保存在堆栈上,所以基本都是基于sp或者fp的寻址。

我们结合代码来看看如何得到signal trampoline frame上保存的寄存器。前提是我们已经知道sp,下来就是寻找保存寄存器的位置(在堆栈上)对sp的偏移。

1
2
3
4
5
6
7
asmlinkage int do_rt_sigreturn(struct pt_regs *regs)
{
  struct rt_sigframe *frame;</code>
 
  frame = (struct rt_sigframe *) ((unsigned long) regs->sp + 8);
/* ... */
}

从上边的代码中我们能看到,rt_sigframe的起始地址距离sp为8,我们接着看rt_sigframe的结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct rt_sigframe
{
  struct siginfo *pinfo;
  void *puc;
  struct siginfo info;
  struct ucontext uc;
  unsigned long retcode[RETCODE_SIZE >> 2];
};
 
struct ucontext {
  unsigned long uc_flags;
  struct ucontext *uc_link;
  stack_t uc_stack;
  struct sigcontext uc_mcontext;
  sigset_t uc_sigmask; /* mask last for extensibility */
};

最后,我们发现寄存器都保存在结构struct sigcontext中。这样我们就可以计算出保存每个寄存器距离sp的偏移,然后从这些地址读出寄存器的值,填写每个frame,

trad_frame_set_reg_addr (this_cache, reg_num, base + foo_register_sigcontext_offset (reg_num));

这样,一个完整的signal trampoline frame unwinding就做好了。