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 | int funcA(){ |
编译时加入-mapcs-frame选项,则对应的汇编如下:
1 | mov ip, sp //将sp的值存入ip |
我们主要关注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 | push {fp, lr} //将lr、fp寄存器的值压栈,且每次sp的值减小4,且是先减小4,然后在压栈,即lr的值存储在sp-4,fp的值存储在sp-8 |
经过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 |
|