Linux程序异常退出用backtrace定位分析

前言

在调试代码时,难免会遇到段错误或各种异常导致进程退出,尤其处理大型工程代码时如果没有一个好的调试工具,定位问题很麻烦和费时间。下面简单介绍如何用backtrace定位程序退出位置。

一:获取程序堆栈API介绍

Linux c/c++开发环境下,可以使用以下函数API来获取进程调用的堆栈信息

#include 

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);

int backtrace(void **buffer, int size);
该函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针列表。参数 size 用来指定buffer中可以保存多少个void* 元素。函数返回值是实际获取的指针个数,最大不超过size大小,为了取得全部的函数调用列表,应保证buffer和size足够大。

  • 不同编译器(eg:gcc/arm-xxx-gcc)的优化选项对获取调用堆栈信息完整性有干扰,backtrace的实现依赖于栈指针,例如在gcc编译过程中加入任何非零的优化等级(-On参数)或加入了栈指针优化参数-fomit-frame-pointer后都将不能正确得到程序栈信息;
  • 内联函数没有堆栈框架,因为它在编译过程中是直接被展开在调用的位置;

char **backtrace_symbols(void *const *buffer, int size);
backtrace_symbols函数将从backtrace函数获取的信息转化为一个字符串数组.。参数buffer是从backtrace函数获取的指针数组,size是该数组中的元素个数(backtrace的返回值),函数返回值是一个指向字符串数组的指针,它的大小同buffer相同,每个字符串包含buffer中每个元素对应地址的信息:它包括函数名、函数的偏移地址、和实际的返回地址。

  • 当前只有使用ELF二进制格式的程序才能获取函数名称和偏移地址;
  • 函数返回值如果需要支持展示函数名功能需要在编译链接时添加-rdynamic选项;(eg:在GNU ld链接器的系统中-rdynamic可用来通知链接器将所有符号添加到动态符号表中);
  • 该函数的返回值是通过malloc函数申请的空间,因此调用结束必须使用free函数来释放指针,如果不能为字符串malloc足够的空间函数的返回值将会为NULL;

void backtrace_symbols_fd(void *const *buffer, int size, int fd);
该函数与backtrace_symbols 函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行,不需要调用malloc函数,因此适用于有可能调用该函数会失败的情况。

二:注意事项

  • gcc编译链接时不需要加任何非零的优化等级(-On参数)或栈指针优化参数-fomit-frame-pointer,否则会使得到程序的栈信息不正确。编译需要加入-rdynamic参数来通知链接器将所有符号添加到动态符号表中,否则不支持展示函数名功能。如果是交叉编译到ARM板还需要加-funwind-tables -ffunction-sections两个参数
  • “static”类型的函数名是不会输出的,也不会出现在函数调用列表里,即使指定了-rdynamic链接选项;
  • 尾调用优化(Tail-call Optimization)会复用当前函数栈, 而不再生成新的函数栈, 这将导致栈信息不能正确被获取。

三:捕获系统异常信号获取程序调用栈

程序异常退出往往都是内核发出某个异常信号导致进程终止,比如内存非法访问内核将发出SIGSEGV信号,错误的运算符如除以零内核将发出SIGFPE信号。利用signal机制,就可以在进程里捕获这些异常信号并输出程序调用栈再退出。从而可以判断程序在哪个位置发生异常。

  • 注意Linux下SIGKILL 和SIGSTOP是不能被应用程序所捕获、阻塞或忽略的。

四:backtrace分析定位问题

1、测试代码

//backtrace.cpp
#include 
#include 
#include 
#include 
#include 
#include 

void signal_handle(int signal)
{
	void *l_buffer[512];
	char **l_ptrace;

	printf("\r\n=========>>>catch signal %d <<<=========\r\n", signal);
	printf("Dump stack start...\n");

	int size = backtrace(l_buffer, 512);
	l_ptrace = backtrace_symbols(l_buffer, size);
	if(NULL == l_ptrace)
	{
	  perror("backtrace_symbols");  
	  exit(1);
	} 

	for(int i = 0; i < size; i++)
	{
	 fprintf(stdout,"  [%02d] %s\n", i, l_ptrace[i]);
	}

	printf("Dump stack end...\n");
	free(l_ptrace);
	exit(1);
}

int Test_SIGSEGV_signal(char *s8ptr)
{
	char l_s8Num = 10;
	*s8ptr += l_s8Num; //s8ptr==NULL这行将产生一个段错误
	printf("this Num:%d\r\n", *s8ptr);
	return *s8ptr;
}

int Test_SIGFPE_signal(int s32Num)
{
	int l_s32Num1 = 0;
	s32Num /= l_s32Num1; //这行将产生一个错误的运算符SIGFPE
	printf("this Num:%d\r\n", s32Num);
	return s32Num;
}
//main.cpp
#include 
#include 
#include 
#include 
#include 
#include "backtrace.h"

int main(int argc, char *argv[])
{
	signal(SIGSEGV, signal_handle);
	signal(SIGFPE, signal_handle);
	//signal(SIGTERM, signal_handle);

	if (argc >= 2)
	{
		if (0 == strcmp(argv[1], "SIGSEGV")){
			Test_SIGSEGV_signal(NULL);
		}
		else if (0 == strcmp(argv[1], "SIGFPE")){
			Test_SIGFPE_signal(20);
		}
	}

	while (1)
	{
		;
	}
	return 0;
}

2、错误信息分析定位

qiuhui@ubuntu:~/work/share/backtrace$ g++ backtrace.cpp main.cpp -I./ -g -rdynamic
qiuhui@ubuntu:~/work/share/backtrace$ ./a.out SIGFPE
=>>>catch signal 8 <<<=
Dump stack start…
[00] ./a.out(_Z13signal_handlei+0x55) [0x400b4b]
[01] /lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7f0ff12af4b0]
[02]./a.out(_Z18Test_SIGFPE_signali+0x16) [0x400c71]
[03] ./a.out(main+0x81) [0x400d11]
[04] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f0ff129a830]
[05] ./a.out(_start+0x29) [0x400a29]
Dump stack end…

以上输出信息可得到信号处理函数执行完的上一个函数是Test_SIGFPE_signal函数,由此可以定位到出现问题的函数是在Test_SIGFPE_signal,借助addr2line命令根据实际的返回地址[0x400c71]就可以定位到具体哪一行。

qiuhui@ubuntu:~/work/share/backtrace$ addr2line -e a.out 0x400c71
/home/qiuhui/work/share/backtrace/backtrace.cpp:47
qiuhui@ubuntu:~/work/share/backtrace$ 

3、动态库里的错误信息分析定位

【1】先编译动态库,编译动态库时“-g -rdynamic”记得带上。

qiuhui@ubuntu:~/work/share/backtrace$ g++ -g -rdynamic backtrace.cpp -fPIC -shared -o libbacktrace.so
qiuhui@ubuntu:~/work/share/backtrace$ ls -l libbacktrace.so 
-rwxrwxr-x 1 qiuhui qiuhui 11424 Jan 12 22:07 libbacktrace.so
qiuhui@ubuntu:~/work/share/backtrace$ 

【2】编译执行程序,“-Wl,-rpath=.”参数表示程序运行时优先去rpath指定路径寻找库文件,可加多个包含路径(-Wl,-rpath,/home/:/lib/:.),程序运行时的寻找顺序为添加的顺序。

qiuhui@ubuntu:~/work/share/backtrace$ g++ -g -rdynamic main.cpp -I ./ -L ./ -lbacktrace -Wl,-rpath=.

【3】运行程序

qiuhui@ubuntu:~/work/share/backtrace$ ./a.out SIGFPE
=>>>catch signal 8 <<<=
Dump stack start…
[00] ./libbacktrace.so(_Z13signal_handlei+0x59) [0x7fe1b2cfe9a9]
[01] /lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7fe1b29694b0]
[02]./libbacktrace.so(_Z18Test_SIGFPE_signali+0x16 [0x7fe1b2cfeada]
[03] ./a.out(main+0xc1) [0x400a47]
[04] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7fe1b2954830]
[05] ./a.out(_start+0x29) [0x4008b9]
Dump stack end…
qiuhui@ubuntu:~/work/share/backtrace$

由以上输出信息可得到实际结束地址是[0x7fe1b2cfeada],用addr2line命令来分析可以看到并不能定位到哪一行。

qiuhui@ubuntu:~/work/share/backtrace$ addr2line -e libbacktrace.so 0x7fe1b2cfeada
??:0

因为动态库是动态链接,每次链接的内存都是不一样,所以刚刚得到的结束地址[0x7fe1b2cfeada]实际是不准确的,它是一个内存地址,并不是库文件里面的偏移地址,如果将刚刚的地址减去libbacktrace.so库加载的开始地址,其实就可以定位正确了。在main函数中加入输出进程的maps文件的信息,如下:

char l_s8aBuff[128] = {0x00};
snprintf(l_s8aBuff, sizeof(l_s8aBuff), "cat /proc/%d/maps", getpid());
system(l_s8aBuff);

再看下输出信息,输出信息太多,只把相关的贴上来

7f9eddc84000-7f9eddc85000 r-xp 00000000 08:01 1843990 /home/qiuhui/work/share/backtrace/libbacktrace.so
7f9eddc85000-7f9edde84000 —p 00001000 08:01 1843990 /home/qiuhui/work/share/backtrace/libbacktrace.so
7f9edde84000-7f9edde85000 r–p 00000000 08:01 1843990 /home/qiuhui/work/share/backtrace/libbacktrace.so
7f9edde85000-7f9edde86000 rw-p 00001000 08:01 1843990 /home/qiuhui/work/share/backtrace/libbacktrace.so
=>>>catch signal 8 <<<=
Dump stack start…
[00] ./libbacktrace.so(_Z13signal_handlei+0x59) [0x7f9eddc849a9]
[01] /lib/x86_64-linux-gnu/libc.so.6(+0x354b0) [0x7f9edd8ef4b0]
[02] ./libbacktrace.so(_Z18Test_SIGFPE_signali+0x16 [0x7f9eddc84ada]
[03] ./a.out(main+0xd4) [0x400a9a]
[04] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0) [0x7f9edd8da830]
[05] ./a.out(_start+0x29) [0x4008f9]
Dump stack end…

咱把0x7f9eddc84ada减去0x7f9eddc84000得到0xada再分析就可以了

qiuhui@ubuntu:~/work/share/backtrace$ addr2line -e libbacktrace.so 0xada
/home/qiuhui/work/share/backtrace/backtrace.cpp:47

【4】通过反汇编代码段分析函数入口地址来定位行号,由上面信息可知实际返回地址是在函数([02] ./libbacktrace.so(_Z18Test_SIGFPE_signali+0x16 [0x7f9eddc84ada]) 偏移量0x16处。执行反汇编指令将其信息保存到log文件

qiuhui@ubuntu:~/work/share/backtrace$ objdump -d libbacktrace.so > log

在保存的log文件里搜索函数名_Z18Test_SIGFPE_signali就可以得到函数的入口地址(0xac4 ),信息如下:

0000000000000ac4 <_Z18Test_SIGFPE_signali>:
ac4: 55 push %rbp
ac5: 48 89 e5 mov %rsp,%rbp
ac8: 48 83 ec 20 sub $0x20,%rsp
acc: 89 7d ec mov %edi,-0x14(%rbp)
acf: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
ad6: 8b 45 ec mov -0x14(%rbp),%eax
ad9: 99 cltd
ada: f7 7d fc idivl -0x4(%rbp)
add: 89 45 ec mov %eax,-0x14(%rbp)
ae0: 8b 45 ec mov -0x14(%rbp),%eax
ae3: 89 c6 mov %eax,%esi
ae5: 48 8d 3d 8e 00 00 00 lea 0x8e(%rip),%rdi # b7a <_fini+0x7e>
aec: b8 00 00 00 00 mov $0x0,%eax
af1: e8 0a fd ff ff callq 800 printf@plt
af6: 8b 45 ec mov -0x14(%rbp),%eax
af9: c9 leaveq
afa: c3 retq
Disassembly of section .fini:

将(函数入口地址)0xac4 +(偏移量)0x16 = 0xada,再分析此地址就可以和前面得出的行号一致了。

qiuhui@ubuntu:~/work/share/backtrace$ addr2line -e libbacktrace.so 0xada
/home/qiuhui/work/share/backtrace/backtrace.cpp:47

【5】用g++编译生成map文件搜索函数名Test_SIGFPE_signal也可以获取函数入口地址,计算方式和上面提到的反汇编代码段一样,具体操作如下:

qiuhui@ubuntu:~/work/share/backtrace$ g++ -g -rdynamic backtrace.cpp -fPIC -shared -o libbacktrace.so -Wl,-Map,map.log

打开map.log得到如下信息:

.text 0x0000000000000950 0x1ab /tmp/ccjH9RPy.o
0x0000000000000950 signal_handle(int)
0x0000000000000a74 Test_SIGSEGV_signal(char*)
0x0000000000000ac4 Test_SIGFPE_signal(int)

Linux中mprotect()函数详解 请点击!!!
Linux程序内存越界定位分析总结 请点击!!!

你可能感兴趣的:(gdb)