linux移植cm_backtrace

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应用程序,需要以下几个步骤

  1. 实现接口获取线程栈的基地址和栈大小
    • 使用接口”pthread_attr_getstack“可以获取到当前线程的栈
  2. 实现接口获取应用程序的可执行代码空间地址范围
    • 通过”/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]
  1. 确认cortex-a7的BL/BLX的指令格式

    • 这点需要进一步确认,可能会存在误判断指令类型的风险
/* 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],反汇编如下
tapd_59727026_base64_1706686630_520.png

可以看到,能够通过反汇编追踪到0x10a52的上一句BL指令为glibc_strlen_null接口,即造成崩溃死机的函数

遗留问题以及展望

  • 使用移植后的ca_backtrace可以很大程度的解决libc backtrace的缺陷

  • BLX/BL的指令格式可能与其他指令发生重复(这点需要进一步确认),所以有可能跟踪到一些其他代码地址,但是所有的函数调用地址都不会错过

  • 最好ca_backtrace与backtrace同时使用,两者可以互相修正,协同分析

  • 19
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值