linux移植cm_backtrace
问题背景
- appserver使用了backtrace接口来跟踪程序死机崩溃的调用栈,得到死机的现场信息
- 当程序挂死在strlen中(strlen(NULL)),无法定位到是谁调用的strlen
问题分析
- 已经定位到是因为backtrace需要程序支持FP指针(栈帧),而strlen在C库中并没有使用FP指针,导致backtrace无法追踪strlen的上一层调用
- glibc在正常方案中不会使用FP指针(需要重新编译C库,并且影响运行效率),所以backtrace使用FP指针追踪调用栈不是一个好方法
- 在小核中,也有同样的获取调用栈的工具:cm_backtrace,该工具并没有依赖于FP指针,也可以追踪调用栈
- 小核(cortex-m3)和大核(cortex-a7)均为ARMv7指令集,可以尝试移植cm_backtrace到大核中,看能否成功获取调用栈
实现过程
实验1
cm_backtrace原理分析
CmBacktrace (Cortex Microcontroller Backtrace)是一款针对 ARM Cortex-M 系列 MCU 的错误代码自动追踪、定位,错误原因自动分析的开源库。
- 根据错误现场状态,输出对应的 线程栈 或 C 主栈;
- 适配 Cortex-M0/M3/M4/M7 MCU;
- 支持 IAR、KEIL、GCC 编译器;
cm_backtrace可以支持多种编译器,并不像c库里的backtrace函数依赖于FP指针的编译参数,对于所有的编译器和优化选项都可以通用
分析cm_backtrace源码,其实现原理十分简单:
- 从栈顶(低地址)开始,按出栈的方向(cortex-m的压栈顺序是从高地址往低地址压),遍历栈内的内容,按4字节读取
- 判定压入栈的内容是否为代码地址空间,若是则读取该代码空间地址的指令
- 判断该代码地址的代码指令是否为跳转指令(BL或BLX),即发生了函数调用,记录下该代码地址空间
- 由此遍历完整个栈空间,直到栈底,即可推导出整个栈内函数调用关系
两个关键函数:
/* check the disassembly instruction is 'BL' or 'BLX' */
static bool disassembly_ins_is_bl_blx(uint32_t addr);
/**
* backtrace function call stack
*
* @param buffer call stack buffer
* @param size buffer size
* @param sp stack pointer
*
* @return depth
*/
size_t cm_backtrace_call_stack(uint32_t *buffer, size_t size, uint32_t sp);
cortex-a7的压栈顺序与cortex-m一直,区别在于BL/BLX指令的格式可能略有不同,所以cm_backtrace同理应该也可以适用于cortex-a7平台
实验2
移植cm_backtrace
移植cm_backtrace到linux应用程序,需要以下几个步骤
- 实现接口获取线程栈的基地址和栈大小
- 使用接口”pthread_attr_getstack“可以获取到当前线程的栈
- 实现接口获取应用程序的可执行代码空间地址范围
- 通过”/proc/self/maps“文件,可以获取到当前进程的可执行代码空间
# cat /proc/self/maps
00010000-00017000 r-xp 00000000 00:01 2076 /usr/bin/cat
00026000-00027000 rw-p 00006000 00:01 2076 /usr/bin/cat
b6dd7000-b6df9000 rw-p 00000000 00:00 0
b6df9000-b6eda000 r-xp 00000000 00:01 2398 /usr/lib/libc-2.23.so
b6eda000-b6ee9000 ---p 000e1000 00:01 2398 /usr/lib/libc-2.23.so
b6ee9000-b6eeb000 r--p 000e0000 00:01 2398 /usr/lib/libc-2.23.so
b6eeb000-b6eec000 rw-p 000e2000 00:01 2398 /usr/lib/libc-2.23.so
b6eec000-b6eef000 rw-p 00000000 00:00 0
b6eef000-b6f07000 r-xp 00000000 00:01 2389 /usr/lib/ld-2.23.so
b6f14000-b6f16000 rw-p 00000000 00:00 0
b6f16000-b6f17000 r--p 00017000 00:01 2389 /usr/lib/ld-2.23.so
b6f17000-b6f18000 rw-p 00018000 00:01 2389 /usr/lib/ld-2.23.so
beb19000-beb3a000 rw-p 00000000 00:00 0 [stack]
bee74000-bee75000 r-xp 00000000 00:00 0 [sigpage]
bee75000-bee76000 r--p 00000000 00:00 0 [vvar]
bee76000-bee77000 r-xp 00000000 00:00 0 [vdso]
ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
/* check the disassembly instruction is 'BL' or 'BLX' */
static int disassembly_ins_is_bl_blx(uint32_t addr) {
uint16_t ins1 = *((uint16_t *)addr);
uint16_t ins2 = *((uint16_t *)(addr + 2));
if ((ins1 & 0xF000) == 0xF000)
return 1;
else if ((ins1 & 0x0B00) == 0x0B00)
return 1;
else if ((ins2 & 0x4780) == 0x4780)
return 1;
else
return 0;
}
实验3
代码移植完成,使用backtrace同样的测试程序,glibc使用原生的Linaro的C库(不支持FP指针),再次追踪strlen(NULL)崩溃
[backtrace:] ca_backtrace start
./demo[0x11406]
/lib/ld-linux-armhf.so.3(+0xb620)[0xb6f7f620]
/lib/libc.so.6(+0x25140)[0xb6e48140]
./demo(glibc_strlen_null+0x1b)[0x11740]
/lib/ld-linux-armhf.so.3(+0x6f96)[0xb6f7af96]
/lib/ld-linux-armhf.so.3(+0x7420)[0xb6f7b420]
/lib/ld-linux-armhf.so.3(+0xaeda)[0xb6f7eeda]
./demo(main+0xbf)[0x119dc]
/lib/libc.so.6(__libc_start_main+0x9d)[0xb6e398ae]
/lib/libc.so.6(+0xb7566)[0xb6eda566]
/lib/ld-linux-armhf.so.3(+0xaeda)[0xb6f7eeda]
./demo[0x11008]
/lib/ld-linux-armhf.so.3(+0xb9bc)[0xb6f7f9bc]
[backtrace:] ca_backtrace end
[backtrace:] libc backtrace start
[backtrace:] backtrace() returned 2 addresses
./demo[0x11458]
/lib/libc.so.6(+0x25140)[0xb6e48140]
[backtrace:] libc backtrace end
可以看到,使用标准C库里的backtrace,仅仅只能定位到/lib/libc.so.6(+0x25140)[0xb6e48140]
而使用ca_backtrace,可以跟踪定位此前更多的调用栈,并且知道了导致崩溃的位置为./demo(glibc_strlen_null+0x1b)[0x11740]
实验4
应用程序开启优化选项,看ca_backtrace能否追踪到崩溃调用栈
$ arm-linux-gnueabihf-gcc -O3 -g -lpthread demo.c -o demo
使用-O3编译,此时编译出来的程序,不会使用FP指针,故libc的backtrace无法起作用
再次运行demo程序,得到以下打印
[backtrace:] ca_backtrace start
/lib/libc.so.6(+0x25140)[0xb6e40140]
./demo[0x111b8]
/lib/libc.so.6(+0x25140)[0xb6e40140]
/lib/ld-linux-armhf.so.3(+0x7420)[0xb6f34420]
/lib/ld-linux-armhf.so.3(+0xaeda)[0xb6f37eda]
./demo[0x10a52]
/lib/libc.so.6(__libc_start_main+0x9d)[0xb6e318ae]
./demo[0x10960]
/lib/libc.so.6(+0xb7566)[0xb6ed2566]
/lib/ld-linux-armhf.so.3(+0xaeda)[0xb6f37eda]
./demo[0x10b08]
/lib/ld-linux-armhf.so.3(+0xb9bc)[0xb6f389bc]
[backtrace:] ca_backtrace end
[backtrace:] libc backtrace start
[backtrace:] backtrace() returned 0 addresses
[backtrace:] libc backtrace end
测试结果与理论相符,ca_backtrace仍然可以追踪到崩溃时的调用栈,而libc的backtrace无法追踪
由于-O3的优化力度太高,通过addrline2进行追踪已经不符合实际的代码逻辑,需要使用objdump将程序反汇编,跟踪汇编代码
崩溃地址为./demo[0x10a52],反汇编如下
可以看到,能够通过反汇编追踪到0x10a52的上一句BL指令为glibc_strlen_null接口,即造成崩溃死机的函数
遗留问题以及展望
-
使用移植后的ca_backtrace可以很大程度的解决libc backtrace的缺陷
-
BLX/BL的指令格式可能与其他指令发生重复(这点需要进一步确认),所以有可能跟踪到一些其他代码地址,但是所有的函数调用地址都不会错过
-
最好ca_backtrace与backtrace同时使用,两者可以互相修正,协同分析