有时我们遇到问题去想查看call stack时,一般利用gdb工具,断点再bt即可查看,但是很多时候也许没有条件去使用gdb工具,这时就可以利用backtrace函数。下面就对该函数进行简单的原理分析和方法介绍
用法介绍
按照下图的code写即可显示出调用堆栈
不过需要注意的是,如果在gcc的时候 没有加上-rdynamic选项,那么在显示调用堆栈的时候,是没有显示那个函数调用的,像下图
如果加上了-rdynamic选项,就会显示出对应的函数了,我们project Makefile都有加这个选项
-rdynamic选项 主要是将所有的链接符号加到动态链接表(.dynsym)里,但不包含static修饰的函数
可利用readelf -s 命令查看dynsym表
对每个API进行分析
int backtrace(void **buffer, int size);
返回调用堆栈
buffer :提供一个指针的数组
size :指定缓冲区的个数,即设置的调用深度
int : 返回实际返回的调用深度
每个地址指针由 函数名、地址偏移、返回地址组成
char **backtrace_symbols(void *const *buffer, int size);
字符串结果通过该API返回,会在该函数中malloc,由我们free
一般的程序员写到上面就结束了或者看到上面就结束了,不一般的还会继续下面的内容
原理分析
想要弄清backtrace函数是怎么实现,需要先弄清调用栈
因为不同的芯片架构,指令、寄存器表示均不同,这里用ARM架构去看效果
将之前测试code全部贴出来如下,以这个code为例子
将main反汇编
lr寄存器,Link register,记录之前的执行位置,在退出时,赋给pc寄存器,返回执行
pc寄存器,Program counter,当前程序执行位置,随程序执行变化
sp寄存器,Stack pointer,当前栈指针的位置,随栈变化,每次PUSH -4 ;POP +4
r11寄存器,用来记录栈帧底部的地址
分析上面的main汇编语句
<+0> push {r11, lr}
将r11 和 lr寄存器推入至栈中
就是将上一个程序的栈帧底部位置保存至栈里,退出程序的时候好恢复,上一个程序的栈位置
<+4> add r11, sp, #4
将 sp + 4 赋给 r11
将该程序的栈帧底部位置保存至r11寄存器,
<+8> bl 0x89c4
跳转至fun1函数
<+12> mov r3, #0
赋给r3寄存器 3 值
<+16> mov r0, r3
将r3寄存器赋给r0
<+20> pop {r11, pc}
从栈中恢复r11寄存器,并将之前保存的lr寄存器的值赋给pc,以便恢复到之前的函数运行的状态
有点糊涂,没关系,再看一下fun1函数的汇编语句
再分析下fun1的汇编语句
<+0> push {r11, lr}
同样地,将r11寄存器和lr寄存器压入栈,这里的r11寄存器的值,就是在main函数中的栈帧底部的位置,这里的lr寄存器的值,就是在main函数执行bl命令时,将pc的值赋给了lr寄存器
<+4> add r11, sp, #4
就是将 sp + 4 赋给r11,现在r11的值就为fun1函数栈帧底部地址
<+8> bl 0x89b4
跳转至fun2函数
<+12> pop {r11, pc}
从栈中取出 之前保存的main函数的r11 和 lr,赋值给r11 和 pc,这样就恢复了main函数的运行
晓得上述流程之后,就可以实现backtrace函数,
首先,拿到本函数的r11寄存器,所指示的栈地址,出栈,就能得到调用函数的lr寄存器的值,然后就能通过dynsym动态链接表,找到对应的函数名
再出栈,就能得到调用函数的r11寄存器的值,以此类推,最终得到整个调用栈
本来想把glibc库中的实现秀出来,不过这个库是有调到so文件实现
利用该API还可以迅速定位出段错误来,段错误时,会发送SIGSEGV信号,重载信号处理程序,将上面code加入进去,就能够得到调用堆栈
接着利用objdump 反编译出来objdump -d test > test.s,即可大致推测哪个语句导致的段错误
甚至,巧妙的利用send SIGTSTP信号和上述函数,就能制造出断点,和gdb一样的效果调试