what

在计算机中,stack backtrace(又称为stack trace、stack traceback)是程序执行中某一时间点的stack frames的报告,而每个stack frame对应一个还没有返回的函数调用,里面包含函数的局部变量、参数、使用的寄存器等。举个例子,比如funcA调用了funcB,而funcB还没返回,那么此时stack中就有两个stack frame,一个是funcA的,一个是funcB的,当然两个都是在一个stack中。那么stack backtrace就会包含这两个stack frame。

why

当程序出错的时候,可以通过stack backtrace获取有效的信息。

how

主要有三种方式,参考breakpad中的代码,以stackwalker_arm中的GetCallerFrame为例,主要包含三种方式,按顺序分别是根据CFI、根据fp、扫描stack,下面主要介绍根据fp register来backtrace:

fp register

首先先了解一下stack frame,方式是直接看汇编代码,由于gcc版本会有多个,在本地测试不方便,因此,直接在arm线上编译器编写。代码如下:

1
2
3
int funcA(){
printf("this is funcA");
}

编译时加入-mapcs-frame选项,则对应的汇编如下:

1
2
3
4
5
6
7
8
9
10
mov     ip, sp   //将sp的值存入ip
push {fp, ip, lr, pc} //将pc、lr、ip、fp的值压栈,且每次sp的值减小4,且是先减小4,然后在压栈,即pc的值存储在sp-4,lr的值存储在sp-8,ip的值存储在sp-12,fp的值存储在sp-16中
sub fp, ip, #4 //将ip的值减去4存入fp
ldr r0, .L2
bl printf
nop
mov r0, r3 //将r3的值存入r0
sub sp, fp, #12 //将fp中的值减去12存入sp
ldm sp, {fp, sp, lr} //从sp中取值3个值分别存入fp、sp、lr,对应的是sp->fp,sp+4->sp,sp+8->lr
bx lr

我们主要关注fp、ip、lr、pc这几个寄存器,从上面的注释可以看到,该stack frame中首先将sp的值存入ip中,然后压入了4个寄存器的值,即此时,而后将ip的值减去4存入fp中,这样操作之后,该stack frame中就有4个寄存器的值。如下表格:

ip register previous function area
pc
lr
ip
sp register fp

之所以顺序是如此,因为,根据arm的官方文档描述:The registers are stored in sequence, the lowest-numbered register to the lowest memory address (start_address), through to the highest-numbered register to the highest memory address (end_address).而根据寄存器的编号大小是可以知道是这个顺序。

经过sub fp, ip, #4之后的stack frame如下

previous function area
fp register pc
lr
ip
sp register fp

而经过sub sp, fp, #12之后的stack frame如下

previous function area
fp register pc
lr
ip
sp register fp

经过ldm sp, {fp, sp, lr}之后的stack frame如下

ip register previous function area
fp register pc
lr register lr
sp register ip
fp register fp

注意registers中的值都是地址,所以,当我们对寄存器的值取值之后才是寄存器值对应那块地址中存储的值,所以,下面会出现寄存器的值和取值两个用词,注意区分,从上面就可以看出,我们进行stack backtrace其实只需要知道fp register的值即可,假设我们知道fp register的值,将其-4之后取值就会得到该frame的lr 那块区域的值,即该函数的调用者,同时,将该值减12取值就会得到调用者的fp的值,于是就可以继续这个递归。获取方法的整条链路。

从上面可以看出,pc、ip寄存器是不需要的,所以,可以进行优化,即将-mapcs-frame选项删除,gcc则会使用默认的mno-apcs-frame选项,注意在gcc5.0之后,-mapcs-frame选项已被废弃,产生的汇编如下:

1
2
3
4
5
6
7
8
9
push    {fp, lr}  //将lr、fp寄存器的值压栈,且每次sp的值减小4,且是先减小4,然后在压栈,即lr的值存储在sp-4,fp的值存储在sp-8
add fp, sp, #4 //sp寄存器的值加4存入fp中
ldr r0, .L2
bl printf
nop
mov r0, r3 //将r3的值存入r0
sub sp, fp, #4 //将fp的值减去4存入sp
pop {fp, lr} //从栈中弹值存入fp和lr,且每次sp增大4,且是先弹出再存值,因此sp的值存入fp,sp+4的值存入lr。sp同样增大了8
bx lr

经过push {fp, lr}之后的栈如下:

previous function area
lr
sp register fp

经过add fp, sp, #4

previous function area
fp register lr
sp register fp

经过sub sp, fp, #4

previous function area
fp register lr
sp register fp

pop {fp, lr}

sp register previous function area
lp register lr
fp register fp

此时,fp的值存入了fp register,lr的值存入了lr register,且stack frame收缩了。即sp register此时指向了之前的stack frame。跟之前一样,可以通过fp跟踪整个调用链,就不展开了。

上面的例子都是arm编译器编译的,arm64编译的会不一样,比如stack frame的fp寄存器的值,建议读者自己跑一遍。

题外话

这里说句题外话,从上面的这些例子可以看出,函数调用返回后,只是stack frame收缩或没有收缩了,但是不管收缩还是没有收缩,数据都是未被清理的,这也就会导致如果两个stack frame完全一样,那么就会导致后一个stack frame的局部变量如果没有赋初值,就会复用上一个stack frame的局部变量的值,举个例子。

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
#include <stdio.h>

void func1(void)
{
int a = 23;
int b = 24;
int c;
c = a + b;
printf("a = %d, b = %d, c = %d\n", a, b, c);
}

void func2(void)
{
int a;
int b;
int c;

printf("a = %d, b = %d, c = %d\n", a, b, c);
}

int main(void)
{
func1();
func2();
return (0);
}