Powerpc栈帧结构和错误回溯
PowerPC 栈帧结构和错误回溯
本文主要说明两个问题:
- PowerPC 的栈帧结构
- 如何利用栈帧获取系统崩溃时的函数调用链
栈帧结构
首先说明一下 PowerPC 各个寄存器的功能:
ppc 寄存器功能
PPC 只有堆栈寄存器sp(r1),并没有栈帧寄存器fp,整个栈帧的操作都要靠sp来完成,根据手册,ppc 的栈帧结构如下所示:
ppc 栈帧结构
PPC 是满减栈(栈向低地址生长),每个函数的栈帧开始保存了函数参数,最后保存了LR和back chain word(在ppc 中即为sp)。中间保存了通用寄存器、条件寄存器、局部变量和padding (PPC 的stack 要求 8-byte 对齐)等。
举个例子:
void xxx(int a,int b,int c,int d)
{
}
int main()
{
xxx(1,2,3,4);
return 0;
}
编译、反编译后得到汇编代码:
100004dc <xxx>:
100004dc: 94 21 ff d0 stwu r1,-48(r1)
100004e0: 93 e1 00 2c stw r31,44(r1)
100004e4: 7c 3f 0b 78 mr r31,r1
100004e8: 90 7f 00 1c stw r3,28(r31)
100004ec: 90 9f 00 18 stw r4,24(r31)
100004f0: 90 bf 00 14 stw r5,20(r31)
100004f4: 90 df 00 10 stw r6,16(r31)
100004f8: 39 7f 00 30 addi r11,r31,48
100004fc: 83 eb ff fc lwz r31,-4(r11)
10000500: 7d 61 5b 78 mr r1,r11
10000504: 4e 80 00 20 blr
10000508 <main>:
10000508: 94 21 ff e0 stwu r1,-32(r1)
1000050c: 7c 08 02 a6 mflr r0
10000510: 90 01 00 24 stw r0,36(r1)
10000514: 93 e1 00 1c stw r31,28(r1)
10000518: 7c 3f 0b 78 mr r31,r1
1000051c: 38 60 00 01 li r3,1
10000520: 38 80 00 02 li r4,2
10000524: 38 a0 00 03 li r5,3
10000528: 38 c0 00 04 li r6,4
1000052c: 4b ff ff b1 bl 100004dc <xxx>
10000530: 39 20 00 00 li r9,0
10000534: 7d 23 4b 78 mr r3,r9
10000538: 39 7f 00 20 addi r11,r31,32
1000053c: 80 0b 00 04 lwz r0,4(r11)
10000540: 7c 08 03 a6 mtlr r0
10000544: 83 eb ff fc lwz r31,-4(r11)
10000548: 7d 61 5b 78 mr r1,r11
1000054c: 4e 80 00 20 blr
先看 main
函数调用 xxx
前的准备: 保存当前的 sp(stwu r1,-32(r1)
),将lr 保存到 caller 的栈顶(stw r0,36(r1)
)(注:根据手册的说明,LR 应该保存在自己的栈帧,但是从汇编代码看,gcc 首先将 sp)保存到 r1-324byte 的位置,即 Back Chain Word,并更新 sp 的值为 r1-324byte,然后将 lr 保存到 r1+36*4byte 的位置,显然已经保存到了caller 函数的stack的结束位置。),接着保存 r31,将传给 xxx
的参数传给寄存器 r3~r6,最后进入函数 xxx
。
进入 xxx
后,类似 main
函数,也是保存 sp 和lr,位置也是当前栈帧的顶部和caller 的栈顶。
这样一来,函数之间的栈帧就通过 sp(back word chain)形成一个栈帧链表,每个函数栈帧的 back word chain 都保存了上一个函数栈帧的 back word chain 的地址,找到一个栈帧,就可以向上一路找遍整个函数调用链的栈帧,直到第一个 caller。
错误回溯
当系统出错或者进入异常时我们可以通过检查栈帧、出错时的指令地址,根据处理器的 ABI 规范向上追溯整个函数调用链。
首先,出错时我们能获取到 pc 寄存器的值(如果是进入了cpu 异常,pc 的值可以通过特殊寄存器 srr0 获取到),sp 寄存器。
其次,根据 ppc 的 ABI 规范我们已经知道了每个函数执行的一条汇编指令是 stwu r1,-xx(r1)
,而对应的二进制值就是 0x9421xxxx
,同时程序的二进制代码我们也是可以访问到的,函数的栈帧结构也是固定的。
接下来,进行错误回溯:
- 读取 sp 寄存器获取栈顶地址,sp 寄存器里的的地址根据上文所述,就是caller 的back word chain 的地址,所以,通过下面的伪代码可以找到 caller 的栈顶地址:
caller_sp = *callee_sp;
- 通过 sp 获取栈顶地址,然后
sp+4
定位到栈帧中保存 lr的地址,取出lr 寄存器。因为ppc 没有push/pop 指令,每次函数调用都会首先执行stwu r1,-xx(r1)
,而对应的二进制值如前文所述就是 0x9421xxxx,所以,从lr 对应的指令向上遍历,检查是否有那条指令对应的二进制值为 0x9421xxxx,就可以找到函数入口地址。 - 通过步骤1可以获取到callee 的栈顶地址,也就知道了caller 的栈顶地址; 然后通过步骤2得到lr 寄存器,也就能知道caller函数入口地址。重复步骤1、2就可以遍历整个函数调用链并且获取到每个函数的入口地址,以及sp地址。
在回溯函数调用链的过程中,每次获取了caller 的sp之后,还可以获取到callee 的函数参数:sp-4,sp-8,sp-12,sp-16 对应函数的四个参数。
这样下来,我们就获取到了函数调用链中的大部分信息,但是这并不能保证能获取到完整的函数执行过程,因为中间的某些函数调用、栈已经消失了。但还是能够为我们调试bug 提供很多有效信息:堆栈内容、函数参数、函数调用关系等。
PowerPC Linux 上的错误回溯
PPC Linux 的错误回溯代码位于 arch/powerpc/kernel/process.c
,函数名 show_stack
,代码如下所示。
void show_stack(struct task_struct *tsk, unsigned long *stack)
{
unsigned long sp, ip, lr, newsp;
int count = 0;
int firstframe = 1;
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
int curr_frame = current->curr_ret_stack;
extern void return_to_handler(void);
unsigned long rth = (unsigned long)return_to_handler;
#endif
sp = (unsigned long) stack;
if (tsk == NULL)
tsk = current;
if (sp == 0) {
if (tsk == current)
sp = current_stack_pointer();
else
sp = tsk->thread.ksp;
}
lr = 0;
printk("Call Trace:\n");
do {
if (!validate_sp(sp, tsk, STACK_FRAME_OVERHEAD))
return;
stack = (unsigned long *) sp;
newsp = stack[0];
ip = stack[STACK_FRAME_LR_SAVE];
if (!firstframe || ip != lr) {
printk("["REG"] ["REG"] %pS", sp, ip, (void *)ip);
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
if ((ip == rth) && curr_frame >= 0) {
pr_cont(" (%pS)",
(void *)current->ret_stack[curr_frame].ret);
curr_frame--;
}
#endif
if (firstframe)
pr_cont(" (unreliable)");
pr_cont("\n");
}
firstframe = 0;
/*
* See if this is an exception frame.
* We look for the "regshere" marker in the current frame.
*/
if (validate_sp(sp, tsk, STACK_INT_FRAME_SIZE)
&& stack[STACK_FRAME_MARKER] == STACK_FRAME_REGS_MARKER) {
struct pt_regs *regs = (struct pt_regs *)
(sp + STACK_FRAME_OVERHEAD);
lr = regs->link;
printk("--- interrupt: %lx at %pS\n LR = %pS\n",
regs->trap, (void *)regs->nip, (void *)lr);
firstframe = 1;
}
sp = newsp;
} while (count++ < kstack_depth_to_print);
}
其中的关键代码行数不多:
do {
stack = (unsigned long *) sp;
newsp = stack[0];
ip = stack[STACK_FRAME_LR_SAVE];
if (!firstframe || ip != lr) {
printk("["REG"] ["REG"] %pS", sp, ip, (void *)ip);
if (firstframe)
pr_cont(" (unreliable)");
pr_cont("\n");
}
firstframe = 0;
sp = newsp;
} while (count++ < kstack_depth_to_print);
思路和我们类似,也是通过当前的 sp 得到栈帧数据,取得 LR 和上一个函数的栈顶地址,这里 Linux 并没有直接去检索函数入口,而是直接调用 printk
打印出来(printk("%pS",(void *)ip)
),然后 printk 会自动找到函数入口地址和ip 在函数代码中的偏移。
在PPC 上调试时可能用到的特殊寄存器
当系统崩溃进入异常时(比如取指异常、数据访问异常、浮点异常等)
- srr0 保存了出错时的pc 寄存器
- srr1 保存了出错时的 msr 寄存器
- dar 出错时指令访问的内存地址
- dsisr 异常类型
更多的特殊寄存器可以参考ppc 的相关datasheet。