在大型项目中,如果程序突然奔溃会是一件很头疼的事,程序员很难去寻找导致奔溃的代码,只能通过不断的设置断点或者打印消息来慢慢的找到出错位置,这会消耗程序员很大的精力和时间。所以很多公司都会有一套程序奔溃定位机制来找到奔溃函数和行数。目前常用的方法是生成core文件,然后再通过gdb调试得到奔溃定位和函数的调用历史,可以看文章《 Linux 利用gdb进行程序奔溃定位 》。但如果环境中没有gdb,那就抓瞎了。所以我们需要自己写代码来获得和bt类似的功能。
Linux中有几个接口类似于gdb中的bt命令,可以获取内核堆栈消息:
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)
具体可见:《Linux下利用backtrace追踪函数调用堆栈以及定位段错误》
但通过backtrace捕获的堆栈消息没有具体的行号,其书写形式也晦涩难懂,需要我们自己去解析。
源代码见《 Linux 利用gdb进行程序奔溃定位 》,通过backtrace获得的原始数据为:
./kasen(_Z12HPR_ErrTracei+0x5a)[0x402213]
/lib64/libc.so.6(+0x35650)[0x7fec9e8e0650]
./libhello.so(_ZN6CHello4funcEv+0x0)[0x7fec9f48b830]
./kasen(main+0x21)[0x401f1c]
/lib64/libc.so.6(__libc_start_main+0xf5)[0x7fec9e8ccaf5]
./kasen[0x401df9]
信息有:动态库、执行文件、函数、地址
通过上述信息我们就可得到具体的奔溃的行号。
函数的调用关系其实就是一个压栈和出栈的顺序,先压进的后出,所以可以看到前面两行和最后两行是对称的,其实这四行是比较固定的,我们可以忽略。我们主要解析中间的几行:
./libhello.so(_ZN6CHello4funcEv+0x0)[0x7fec9f48b830]
./kasen(main+0x21)[0x401f1c]
上面第一行的信息从左到右为:
动态库名–函数名–地址,我们可以用这些信息来获取这个地址所在的行号,我们在此可以用addr2line来获取:
addr2line -e 可执行文件名(或者动态库) 偏移地址
以上的命令对于执行文件和动态库的情况是不一样的,如果是可执行文件,其偏移地址就是上述[0x7fec9f48b830 ]中的地址,如果是动态库的话,偏移地址还需要减去动态库加载时的基地址。基地址的求解代码为:
char* GetBaseAddress(const char* pSoName)
{
static char BaseAddr[20];
int pid = getpid();
char szFileName[50];
sprintf(szFileName, "/proc/%d/maps", pid);
char szCmd[100];
sprintf(szCmd, "grep %s %s | head -1 | awk -F- \'{print $1}\'", pSoName, szFileName);
FILE* pFile = popen(szCmd, "r");
int n = fread(BaseAddr, 1, 20,pFile);
BaseAddr[n-1] = '\0';
return BaseAddr;
}
将上述字符串型的地址转化为int型:
//求动态库某行的地址:
int nOffsetAddress = strtol(szStack, NULL, 16) - strtol(pBaseAddress, NULL, 16);
//求执行文件或者静态库的某行的地址:
int nOffsetAddress = strtol(szStack, NULL, 16);
求出这些后就可以用addr2line来获取地址所对应的行号和具体文件路径:
char szCmd[128];
sprintf(szCmd, "addr2line -e %s 0x%x", pSoName, nOffsetAddress);
执行后,得到:
/mnt/shared/TMP/main/hello/hello.cpp:13
所以到现在我们就已经知道了地址——动态库(或者可执行文件)——文件名——行号,最后我们还需要知道函数名,其实函数名在上述原始数据中已经给我们了:
./libhello.so(_ZN6CHello4funcEv+0x0)[0x7fec9f48b830]
./kasen(main+0x21)[0x401f1c]
括号中的即为函数名,只是这个函数名是C++风格的经过名称修饰后的,我们可以根据这种风格进行解析得到函数名,如_ZN6CHello4funcEv解析后得到CHello::func(void);其他的一些例子如下:
int func(int) ---------> _Z4funci
float func(float) ---------> _Z4funcf
int C::func(int) ---------> _ZN1C4funcEi
int C::C2::func(int) ---------> _ZN1C2C24funcEi
int N::func(int) ---------> _ZN1N4funcEi