简单介绍下传统栈回溯原理,方便理解。
栈回溯原理
如上图所示,是一个传统的arm架构下函数栈数据分布,需要编译选项-fno-omit-fram-pointer -mapcs -mno-sched-prolog
函数进入时,首先会
mov ip sp
push {fp, ip, lr pc}
即将fp、sp、lr、pc依次入栈
示例函数调用关系:A->B->C
| pc | ---->b_fp //stack high addr
| lr |
| sp |
| a_fp |
...{func stack}
| pc | ---->c_fp
| lr |
| sp |
| b_fp |
...{func stack}
| pc | ---->reg_fp
| lr |
| sp |
| c_fp |
...{func stack} //stack low addr
函数进入时,当前fp寄存器指向上层函数栈顶。所以,栈回溯时,获取当前fp寄存器,就可以把last func的fp\sp\lr\pc
都获取到。
再根据获取到的上一个函数的fp指针。循环嵌套即可获取所有调用关系。
当fp指针指向deadbeef
,认为回溯结束。
stauct stackframe {
unsigned long fp;
unsigned long sp;
unsigned long lr;
unsigned long pc;
};
static void __dump_backtrace(struct stackframe *stack)
{
struct stackframe *prev;
printk("Function enter at [<%08x>] from [<%08x>]\n", stack->pc, stack->lr);
prev = (struct stackframe *)(stack->fp - 12);
prev->pc = stack->lr;
if (prev->fp = 0xdeadbeef)
return;
__dump_backtrace(prev);
}
void dump_stack(void)
{
struct task_struct *tsk = current;
struct stackframe stack;
asm volatile("mov %0, pc" : "=r"(stack.pc));
asm volatile("mov %0, fp" : "=r"(stack.fp));
asm volatile("mov %0, sp" : "=r"(stack.sp));
stack.lr = *((unsigned long *)(stack.fp - 4));
__dump_backtrace(&stack);
}
dump_stack
接口用于函数中直接调用,所以需要用汇编指令把当前的pc、fp、sp
取出来。以及通过fp
获取调用lr(即调用dump_stack
函数的返回地址)。
随后进入__dump_backtrace
函数,打印pc、lr
地址。然后通过fp指针获取上一函数的pc存放位置。减去12字节地址后,就是struct stackframe
数据结构地址。
随后嵌套直至回溯结束。
ps: prev->pc = stack->lr;
这行代码可能比较疑惑,因为pc和lr地址不一致,为了防止打印地址不一致,除了第一次进入pc,后续嵌套统一使用lr回溯
在daba_aboat
等场景中,struct stackframe
内容由异常处理时保存的异常时寄存器列表中获取。
...
struct stackframe stack;
stack.fp = regs->fp;
stack.fp = regs->fp;
stack.fp = regs->fp;
stack.fp = regs->fp;
__dump_backtrace(&stack)
...
缺点
注意:此模式每个函数进入时都会保存上述四个寄存器,所以缺点是:
- 执行效率较低,每次进入都有保存寄存器动作
- 因为指令多,所以编译生成的代码尺寸也会较大。
所以通常情况下Linux采用unwind方式栈回溯。