最近项目中遇到一个偶现的SIGSEGV段错误问题,使用普通的printf方法排查了一段时间依旧没有定位到问题点,于是尝试使用backtrace来跟进问题。本文主要记录一下我在arm平台上使用backtrace来回溯函数调用栈的步骤,作为对问题的整理和回顾。
嵌入式开发过程中,难免会遇到各种死机问题,常用以下方法来帮助定位:
1、添加打印/日志信息梳理业务逻辑,跟踪代码运行轨迹,找到死机位置;
2、根据设备死机时输出函数调用栈(backtrace),结合符号文件表定位问题;
3、保留死机时的内存镜像(coredump),利用gdb工具来还原“案发现场”;
以上三种定位手段中,第1种是最基本最常用的方法,但信息量较少,需要多次添加日志来跟踪定位; 第2种能够给出函数调用关系,但是一般无法给出各个参数的值;第3种不仅能够给出函数调用关系,还能查看各个参数的值。后两种方法对编译工具链、系统都有一定的要求。
不过大部分嵌入式操作系统因存储空间有限,程序出现异常时不会自动生成coredump,想要生成core文件需要在程序运行前手动进行以下配置(因设备端空间有限,此处以nfs挂载/mnt为例):
sysctl -w kernel.core_pattern=/mnt/core.%e.%p
chmod 666 /mnt/ -R
ulimit -c unlimited
执行以上操作后,程序出现异常崩溃时会自动在/mnt目录下生成core文件,再使用gdb进行调试即可。
下面主要介绍backtrace。
当程序出现异常时通常伴随着会收到一个由内核发过来的异常信号,如当对内存出现非法访问时将收到段错误信号SIGSEGV,然后才退出。利用这一点,当我们在收到异常信号后将程序的调用栈进行输出。
backtrace()系列函数有3个:backtrace,backtrace_symbols,backtrace_symbols_fd。主要用于应用程序反调试(self-debugging)。
参见man 3 BACKTRACE,3个函数原型:
#include <execinfo.h>
int backtrace(void **buffer, int size);
char **backtrace_symbols(void *const *buffer, int size);
void backtrace_symbols_fd(void *const *buffer, int size, int fd);
1、int backtrace(void **buffer, int size);
backtrace() 返回调用程序的回溯(跟踪)信息,存储在由buffer指向的数组中。对于特定程序,backtrace就是一系列当前激活的函数调用(active function call)。
参数:
buffer 由buffer指向的数组,每一项都是void*类型,存储的是相应(调用函数的)栈帧的返回地址。
size 指定存储在buffer中的地址最大数量。
返回值:
返回buffer中实际地址的数量,应当<=size。如果返回值 < size,那么完整的回溯信息被存储;如果返回值 = size,那么它可能被截断,最旧的栈帧可能没有返回。
2、char **backtrace_symbols(void *const *buffer, int size);
backtrace() 返回一组地址,backtrace_symbols()象征性地翻译这些地址为一个描述地址的字符串数组。
参数:
buffer一个字符串数组,由backtrace()返回的buffer,每项代表一个函数地址。backtrace_symbols()会用字符串描述每个函数地址,字符串包括:函数名称,一个16进制偏移(offset),实际的返回地址(16进制)。
size 表明buffer中的地址个数。
返回值:
成功时,返回一个指向由malloc(3)分配的array;失败时,返回NULl。
arrary是一个二维数组,该数组的每个元素 指向一个代表backtrace()返回的函数地址的符号信息的字符串,数组由函数内部调用malloc分配空间,必须由调用者free。
注意:指向字符串的指针的数组,不必释放,而且不应该释放。应该释放的是返回的二维数组指针。
3、void backtrace_symbols_fd(void *const *buffer, int size, int fd);
backtrace_symbols_fd()的参数buffer、size同backtrace_symbos(),不同之处在于,backtrace_symbols_fd()并不会返回一个字符串数组给调用者,而是将字符串写入fd对应文件。backtrace_symbols_fd()也不会调用malloc分配二维数组空间,因此可应用于malloc可能会失败的情形。
backtrace,backtrace_symbols,backtrace_symbols_fd在glibc 2.1以后就提供了。
3个函数是GNU 扩展(GNU extensions),因此只能用于GNU gcc/g++系列编译器。
使用它们时需要注意以下几点:
1、backtrace的实现依赖于栈指针(fp寄存器),在gcc编译过程中任何非零的优化等级(-On参数)或加入了栈指针优化参数-fomit-frame-pointer后多将不能正确得到程序栈信息;
2、backtrace_symbols的实现需要符号名称的支持,在gcc编译过程中需要加入-rdynamic参数;
3、内联函数没有栈帧,它在编译过程中被展开在调用的位置;
4、尾调用优化(Tail-call Optimization)将复用当前函数栈,而不再生成新的函数栈,这将导致栈信息不能正确被获取。
我的应用平台是armv7,编译选项中增加了以下几个参数:
-O0 -rdynamic -g -funwind-tables -ffunction-sections
刚开始只使用了-rdynamic -g参数,在我所使用的平台板子上并不能抓取程序运行时的栈信息,加了-funwind-tables -ffunction-sections之后才成功抓取到栈回溯信息。
下面是我在使用backtrace调试实际应用程序前,编写的一个简单的测试用例:
用户空间的程序无法直接访问物理地址,此处显示的地址是经过MMU(内存管理单元)映射过的。通过maps信息显示test程序运行时的栈起始地址为0x55f6e2212000,则程序运行过程出现异常时的偏移地址应该为:
0x55f6e2212cf8 - 0x55f6e2212000 = 0xcf8
找到了正确的地址,此时可以使用addr2line工具来找到程序中的具体位置: