使用BackTrace查看调用堆栈

有时我们遇到问题去想查看call stack时,一般利用gdb工具,断点再bt即可查看,但是很多时候也许没有条件去使用gdb工具,这时就可以利用backtrace函数。下面就对该函数进行简单的原理分析和方法介绍

用法介绍

按照下图的code写即可显示出调用堆栈

不过需要注意的是,如果在gcc的时候 没有加上-rdynamic选项,那么在显示调用堆栈的时候,是没有显示那个函数调用的,像下图

如果加上了-rdynamic选项,就会显示出对应的函数了,我们project Makefile都有加这个选项

-rdynamic选项 主要是将所有的链接符号加到动态链接表(.dynsym)里,但不包含static修饰的函数

可利用readelf -s 命令查看dynsym表

扫描二维码关注公众号,回复: 3707552 查看本文章

对每个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>

跳转至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>

跳转至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一样的效果调试

猜你喜欢

转载自blog.csdn.net/qq_28351465/article/details/82999140