SylixOS backtrace实现
backtrace
用于输出当前调用栈信息,根据这些信息可以知道程序运行流程,有助于分析bug
实现原理
下面分别以ARM
平台(ARMV7-A)和x86平台
(32位)为例,讲述backtrace
实现原理。
ARM平台实现
这里先简单介绍下ARM
平台下,c
语言函数的调用过程,应用程序代码如下:
#include <stdio.h>
void fun2 (int i)
{
printf("%d\n", i);
}
void fun1 (int i)
{
fun2(i);
}
int main (int argc, char **argv)
{
fun1(0x1);
return (0);
}
这里调用流程为 main -> fun1 -> fun2
,函数的返回过程是相反的,为了能够实现函数的返回,ARM处理器提供了LR
寄存器,在函数跳转前,会将返回地址保存在LR
寄存器,利用保存的值即可实现函数返回。同时ARM处理器会用R11
(FP)作为栈帧寄存器,栈帧你可以理解为当前函数栈空间的起始地址(ARMV7-A 一般采用满递减类型栈空间,所以此时的起始地址就栈底),调用过程中各个函数栈空间如下:
main<---------------+ fun1<--------------+
| | fun2
+------------+ | +------------+ | +------------+
| LR +<-------+ +-------+ LR +<-------+ +-------+ LR |
+------------+ | +------------+ | +------------+
| caller(FP)| +----------+ (main)FP | +---------+ (fun1)FP |
+------------+ +------------+ +------------+
| argc | | i | | i |
+------------+ +------------+ +------------+
| argv | | | | |
+------------+ +------------+ +------------+
| | | | | |
| | | | | |
| | | | | |
| | | | | |
| | | | | |
+------------+ +------------+ +------------+
首先main
函数会保存自己的LR(R14)
和FP(R11)
寄存器、函数参数压入堆栈,然后利用R0
寄存器作为参数,调用fun1
函数,函数fun1
也需将LR(R14)
和FP(R11)
寄存器保存在栈中,同时将R0
传入的参数i
保存在栈中,然后调用fun2
,fun2
入口操作与fun1
基本相同,只不过后续调用了printf
而已。这里需要注意的是当前函数栈帧中保存的LR
是当前函数执行完的返回地址,而FP
是当前函数调用者的栈帧,比如在函数fun1
栈中的LR
,是函数fun1
返回到main
函数的地址,而FP
指向main
函数的栈帧起始地址。之所以要保存LR
,是因为函数调用时,会更新LR
的值,保存FP
是因为要将FP
寄存器指向当前函数的栈帧起始地址来访问栈上一些变量。下面结合反汇编代码了解这个过程.
Disassembly of section .text:
000002d0 <fun2>:
2d0: e92d4800 push {fp, lr} ;保存LR及fun1函数的FP
2d4: e28db004 add fp, sp, #4 ;设置FP指向自身函数栈帧起始地址
2d8: e24dd008 sub sp, sp, #8 ;SP便宜保存局部变量,保证8字节对齐
2dc: e50b0008 str r0, [fp, #-8] ;将参数R0保存,即局部变量i
2e0: e59f3018 ldr r3, [pc, #24] ; 300 <fun2+0x30>
2e4: e08f3003 add r3, pc, r3
2e8: e1a00003 mov r0, r3
2ec: e51b1008 ldr r1, [fp, #-8]
2f0: ebffffed bl 2ac <fun2-0x24> ;调用printf
2f4: e24bd004 sub sp, fp, #4
2f8: e8bd4800 pop {fp, lr}
2fc: e12fff1e bx lr
300: 0000006c andeq r0, r0, ip, rrx
00000304 <fun1>:
304: e92d4800 push {fp, lr} ;保存LR及main函数的FP
308: e28db004 add fp, sp, #4 ;设置FP指向自身函数栈帧起始地址
30c: e24dd008 sub sp, sp, #8 ;SP便宜保存局部变量,保证8字节对齐
310: e50b0008 str r0, [fp, #-8] ;将参数R0保存,即局部变量i
314: e51b0008 ldr r0, [fp, #-8] ;取值,给R0,准备调用fun2,这里感觉有点多余
318: ebffffe6 bl 2b8 <fun2-0x18> ;跳转plt,再跳fun2
31c: e24bd004 sub sp, fp, #4
320: e8bd4800 pop {fp, lr}
324: e12fff1e bx lr
00000328 <main>:
328: e92d4800 push {fp, lr}
32c: e28db004 add fp, sp, #4
330: e24dd008 sub sp, sp, #8
334: e50b0008 str r0, [fp, #-8] ;保存argc
338: e50b100c str r1, [fp, #-12] ;保存argv
33c: e3a00001 mov r0, #1 ;R0 传参
340: ebffffdf bl 2c4 <fun2-0xc> ;由于编译时加入了PIC编译选项,会跳转到plt表中,再跳转到fun1函数
344: e3a03000 mov r3, #0
348: e1a00003 mov r0, r3 ;函数返回值
34c: e24bd004 sub sp, fp, #4
350: e8bd4800 pop {fp, lr}
354: e12fff1e bx lr
以上代码都具有详细注释,如有疑问可以在下方讨论。
通过调用流程图就可以看出,利用当前函数栈帧中的LR就能获取当前函数调用者,而通过栈帧中的FP
就能知道调用者的FP
,有了调用者的栈帧,就能知道更上一级的调用者。在实际使用时,通过GCC编译器内建函数__builtin_frame_address(0)
即可获取当前函数栈帧,此时栈帧指向返回地址LR
,对地址-4
操作即可获取当前函数调用者栈帧。
x86平台实现
待更新