本文简要介绍在Linux上输出和解析C++的call stack的方法。
开发环境:
* 编译器: gcc 4.8.2
* 操作系统: Ubuntu 14.04 x86_64
输出调用堆栈
glibc中提供了backtrace()
和backtrace_symbols()
两个函数来输出和解析程序的call stack,详情见man backtrace
。
下面的代码修改自backtrace手册里的例子,当程序收到SIGSEGV信号(内存访问越界)时,输出程序的调用堆栈,以方便定位崩溃点。
#include <execinfo.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
void print_stack_frames(int signum) {
int j, nptrs;
#define SIZE 100
void *buffer[100];
char **strings;
nptrs = backtrace(buffer, SIZE);
strings = backtrace_symbols(buffer, nptrs);
if (strings == NULL) {
perror("backtrace_symbols");
exit(EXIT_FAILURE);
}
for (j = 0; j < nptrs; j++)
printf("%s\n", strings[j]);
free(strings);
exit(signum);
}
void myfunc2(int a) {
int *b;
*b = a;
}
void myfunc() {
int a = 1;
myfunc2(a);
}
int main(int argc, char *argv[]) {
signal(SIGSEGV, print_stack_frames);
myfunc();
exit(EXIT_SUCCESS);
}
这段代码在第29行必然会引发内存访问越界。
使用如下的命令编译代码,这是release版本
g++ -rdynamic -o test.r test_backtrace.cpp
为了解析文件的行号等信息,我们还需要一个debug版本
g++ -rdynamic -g -o test.d test_backtrace.cpp
上面的编译命令中,加入-rdynamic
可以将所有非static全局变量和非static函数的符号输出到符号表中。如果你不想在release版本中使用-rdynamic
选项,编译debug版本的时候也不能用,否则符号表会不一致,从而影响后面的解析过程。
解析bactrace输出
运行上面的程序,输出如下
./test.r(_Z18print_stack_framesi+0x25) [0x400a02]
/lib/x86_64-linux-gnu/libc.so.6(+0x36ff0) [0x7f7ddcfb6ff0]
./test.r(_Z7myfunc2i+0xe) [0x400ab4]
./test.r(_Z6myfuncv+0x19) [0x400ad1]
./test.r(main+0x23) [0x400af6]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f7ddcfa1ec5]
./test.r() [0x400919]
每一行的中括号里的十六进制数是返回地址,小括号里加号之前的内容是mangle后的函数名称,加号之后的十六进制数是返回地址相对函数地址的偏移量。
以第一行为例,首先解析函数名,mangle后的函数名称是_Z18print_stack_framesi
,使用c++filt
命令可以将其demangle
c++filt _Z18print_stack_framesi
输出print_stack_frames(int)
。
然后使用addr2line获取返回地址所在的行号
addr2line -e test.d 0x400a02
输出/home/wilbur/test_backtrace.cpp:13
需要注意的是,addr2line必须读取额外的调试信息来解析返回地址所在的行号,这也是我们之前编译debug版本的test.d
的原因。
python解析脚本
下面是一个专门解析backtrace输出的python脚本。
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# @author: mawenbao@hotmail.com
# @date: 2014.04.25
# @desc: parse backtrace output
import re
import os
import sys
import subprocess
_bt_line_regex = re.compile(r'(?P<path>.+)\((?P<func>.*?)(\+0x[0-9a-f]+)?\) \[(?P<addr>0x[0-9a-f]+)\]')
def _run_cmd(cmd):
"""run cmd(string) and return it's stdout"""
cmdProc = subprocess.Popen(
cmd,
shell=True,
stdout=subprocess.PIPE
)
return cmdProc.communicate()[0]
def main(object_file):
for line in sys.stdin:
# extract function name and return address
# from backtrace's output
match = _bt_line_regex.match(line)
if not match:
continue
groups = match.groupdict()
path = groups['path']
func = groups['func']
addr = groups['addr']
# demangle function name
func = _run_cmd('c++filt {}'.format(func)).strip()
# translate return address to file path and line number
addr = _run_cmd('addr2line -e {} {}'.format(object_file, addr)).strip()
print('[{}] {} {}'.format(path, addr, func))
return 0
def usage():
return 'usage: python {} <object_file>'.format(sys.argv[0])
if __name__ == '__main__':
if len(sys.argv) != 2:
print(usage())
sys.exit(2)
objFile = sys.argv[1]
if not os.path.exists(objFile):
print('object file does not exists: {}'.format(objFile))
sys.exit(2)
sys.exit(main(objFile))
输入如下命令
./test.r | python backtrace_parser.py ./test.d
输出如下
[./test.r] /home/wilbur/test_backtrace.cpp:13 print_stack_frames(int)
[./test.r] /home/wilbur/test_backtrace.cpp:29 myfunc2(int)
[./test.r] /home/wilbur/test_backtrace.cpp:35 myfunc()
backtrace和core dump
Linux的core dump机制可以让你的程序在崩溃的时候在磁盘上保留一份gdb可调试的内存镜像。相比core dump,backtrace虽然只能提供函数的名称和行号等信息,但是胜在简便灵活。另外在生产环境中,许多时候可能不方便使用gcc的-g
选项编译程序,因此就算有core dump也很难用gdb进行调试。