Linux C++ 跟踪程序奔溃及函数调用关系

在大型项目中,如果程序突然奔溃会是一件很头疼的事,程序员很难去寻找导致奔溃的代码,只能通过不断的设置断点或者打印消息来慢慢的找到出错位置,这会消耗程序员很大的精力和时间。所以很多公司都会有一套程序奔溃定位机制来找到奔溃函数和行数。目前常用的方法是生成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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值