linux下利用backtrace()定位Segmentation fault错误
0x00 前言文章中的文字可能存在语法错误以及标点错误,请谅解;如果在文章中发现代码错误或其它问题请告知,感谢!0x01 Segmentation fault出错原因当我们在运行一个程序,有时会碰到终端打印出“Segmentation fault (core dumped)”提示,出现这种错误主要是对内存操作不当导致,属于代码编写逻辑的问题,比如在代码中对空指针或野指针进行了读写操作,数据...
0x00 前言
文章中的文字可能存在语法错误以及标点错误,请谅解;
如果在文章中发现代码错误或其它问题请告知,感谢!
0x01 Segmentation fault出错原因
当我们在运行一个程序,有时会碰到终端打印出“Segmentation fault (core dumped)”提示,出现这种错误主要是对内存操作不当导致,属于代码编写逻辑的问题,比如在代码中对空指针或野指针进行了读写操作,数据越界访问等等。
这个问题在代码编程中很常见,而且出现该段错误(Segmentation fault)和编译时出现的错误不一样,不会定位到出错的具体代码行,所以我们一般可以利用signal()和backtrace()来定位出现段错误位置。
0x02 定位Segmentation fault思路
程序(进程)产生Segmentation fault错误时,系统内核程序会发送一个SIGSEGV信号通知程序有不合法内存引用的事件发生。如何处理这个信号,需要由该程序本身决定。若我们在程序中没有编写任何针对该信号的处理函数,系统则按照默认的方式处理传过来的信号(终止程序运行)。
所以,我们要想在收到SIGSEGV信号后,将程序的堆栈帧数据进行输出,从而对Segmentation fault进行错误定位,就应该注册针对该信号的信号处理函数,该函数含有backtrace()以及 backtrace_symbols()函数(用来获取函数调用的堆栈帧数据),这样一旦发生了内存引用错误,在我们在信号处理函数中,通过得到的堆栈帧数据就可以知道是程序的哪个位置出现了段错误。
0x03 backtrace()、 backtrace_symbols()函数简介
int backtrace(void * * buffer,int size)
用来获取当前程序调用堆栈,获取到的信息存放在buffer中,它是一个指针数。参数size用来指定buffer中可以保存多少个void* 元素。函数返回值是实际获取的指针个数,最大不超过size大小。在buffer中的指针实际是从堆栈中获取的返回地址,每一个调用堆栈有一个返回地址。
char * * backtrace_symbols (void * const * buffer, int size)
将从 backtrace()中获取的信息转化成 一个字符串数组。参数buffer应该是从backtrace()获取的数组指针,size是该数组中的元素个数(backtrace()返回值),该函数返回值是一个指向字符串数组的指针,它的大小和buffer相同。每个字符串包含一个先对于buffer中对应元素可打印信息。包括函数名,函数的偏移地址,和实际的返回地址。
void backtrace_symbols_fd (void * const * buffer, int size, int fd)
backtrace_symbols_fd与backtrace_symbols 函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行.它不需要调用malloc函数,因此适用于有可能调用该函数会失败的情况。
它们由GNU C Library提供,关于它们更详细的介绍可参考Linux Programmer’s Manual中关于backtrack相关函数的介绍。
0x04 静态链接情况下的段错误定位
1.测试代码
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <execinfo.h>
#include <signal.h>
#define BACKTRACE_SIZE 16
void dump(void)
{
int j, nptrs;
void *buffer[BACKTRACE_SIZE];
char **strings;
nptrs = backtrace(buffer, BACKTRACE_SIZE);
printf("backtrace() returned %d addresses\n", nptrs);
strings = backtrace_symbols(buffer, nptrs);
if (strings == NULL) {
perror("backtrace_symbols");
exit(EXIT_FAILURE);
}
for (j = 0; j < nptrs; j++)
printf(" [%02d] %s\n", j, strings[j]);
free(strings);
}
void signal_handler(int signo)
{
printf("\n=========>>>catch signal %d <<<=========\n", signo);
printf("Dump stack start...\n");
dump();
printf("Dump stack end...\n");
signal(signo, SIG_DFL); //对该信号进行默认处理
raise(signo); // 向自身发信号
}
void func_c()
{
*((volatile char *)0x0) = 0x9999;
}
void func_b()
{
func_c();
}
void func_a()
{
func_b();
}
int main()
{
signal(SIGSEGV, signal_handler); //为SIGSEGV信号安装新的处理函数
func_a();
return 0;
}
2. 编译及运行
使用gcc将上述测试程序编译成可执行文件并执行:
3.段错误定位分析
由此可见是调用func_c后就调用了段错误处理函数,所以问题出现在func_c中,准确的定位到 0x400c11处,但到底对应的是程序的具体行,使用addr2line
命令得到:
0x05 动态链接情况下的错误信息分析定位
上例为使用静态库程序对进行段错误进行定位,但是通常我们调试的程序中会加载很多的动态链接库,若错误发生在动态链接库里的话那么定位处理会比较麻烦,不过也可以解决。现在把上例中的func. c函数拿出来封装成一个动态库libfunc. so, 再看看段错误定位时会出现什么情况:
1.测试代码
func.c:
#include <stdio.h>
void func_b()
{
func_c();
}
test.c:
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <execinfo.h>
#include <signal.h>
#define BACKTRACE_SIZE 16
void dump(void)
{
int j, nptrs;
void *buffer[BACKTRACE_SIZE];
char **strings;
nptrs = backtrace(buffer, BACKTRACE_SIZE);
printf("backtrace() returned %d addresses\n", nptrs);
strings = backtrace_symbols(buffer, nptrs);
if (strings == NULL) {
perror("backtrace_symbols");
exit(EXIT_FAILURE);
}
for (j = 0; j < nptrs; j++)
printf(" [%02d] %s\n", j, strings[j]);
free(strings);
}
void signal_handler(int signo)
{
printf("\n=========>>>catch signal %d <<<=========\n", signo);
printf("Dump stack start...\n");
dump();
printf("Dump stack end...\n");
signal(signo, SIG_DFL); //对该信号进行默认处理
raise(signo); // 向自身发信号
}
void func_b()
{
func_c();
}
void func_a()
{
func_b();
}
int main()
{
#if 0
char buff[64] = {0x00};
sprintf(buff,"cat /proc/%d/maps", getpid());
system((const char*) buff);
#endif
signal(SIGSEGV, signal_handler); //为SIGSEGV信号安装新的处理函数
func_a();
return 0;
}
2. 编译及运行
将func.c生成编译libfunc. so:
gcc -g -rdynamic func.c -fPIC -shared -o libfunc.so
其中:
-shared :指定生成动态链接库。
-fPIC :表示编译为位置独立的代码,用于编译共享库。目标文件需要创建成位置无关码,就是在可执行程序装载它们的时候,它们可以放在可执行程序的内存里的任何地方。
将test.c编译test可执行文件:
gcc -g -rdynamic test.c -L. -lfunc -Wl,-rpath=. -o test
其中:
-L.:编译时链接当前目录的. so。
-Wl,-rpath=.:指定程序执行时动态链接库搜索路径为当前目录,否则会出现执行找不到.so的错误。
执行test结果如下:
3.段错误定位分析
然后我们使用addr2line
命令会发现无法定位段错误:
出现这种情况是因为动态链接库是程序运行时动态加载的,0xb77124f3不是一个实际的物理地址,而是经过MMU(内存管理单元)映射过的。
有上面的认识后那我们就只需要得到此次libfunc.so的加载地址然后用0xb77124f3这个地址减去libfunc.so的加载地址得到的结果后利用addr2line命令就可以正确的得到出错的地方;
另外我们注意到func_c+0x8其实也是在描述出错的地方,这里表示的是发生在func偏移0x1a处的地方,也就是说如果我们能得到func在程序中的入口地址再加上偏移量0x8也能得到正常的出错地址。
我们先利用第一种方法即试图得到libfunc.so的加载地址来解决这个问题。可以使用查看进程的maps文件来了解进程的内存使用情况和动态链接库的加载情况,所以我们在打印栈信息前再把进程的maps文件也打印出来,在test.c的main函数中加入如下代码:
char buff[64] = {0x00};
sprintf(buff,"cat /proc/%d/maps", getpid());
system((const char*) buff);
编译后执行:
Maps信息第一项表示的为地址范围如第一条记录中的08048000-08049000,第二项r-xp分别表示只读、可执行、私有的,由此可知这里存放的为libfunc.so的text段为代码段,后面的栈信息0xb776d4f3也正好是落在了这个区间。所有我们正确的地址应为0xb776d4f3- 0xb776d000= 0x4f3,将这个地址利用addr2line命令得到如下结果:
然后我们看一下第二种方法:使用func函数的入口地址加上偏移来得到正确得段错误地址。要得到一个函数的入口地址的方法有很多,比如查看map文件gcc的nm或readelif等命令。本篇文章使用map方法。
利用gcc编译将libfunc.so生成的map文件:
gcc -g -rdynamic func.c -fPIC -shared -o libfunc.so -Wl,-Map,add.map
Map文件中将包含关于libfunc.so新,我们该文件中包含func名就可以找到其在.text段的地址:
红框中地址为0x4eb,加上刚才偏移0x8得到4F3,和第一种方法等到得值一样,然后将这个地址利用addr2line命令得到如下结果:
另外,需要注意的是,当程序执行到signal()的时候,并不意味着这一行程序要对该信号立刻做什么操作,而是告诉系统当有对应信号传来时对这个信号用什么程序处理。
0x06 其它解决Segmentation fault方法
对于Segmentation fault,我们还可以使用core和gdb方法定位出错位置。可参考以下链接:
1.https://blog.csdn.net/kevinguozuoyong/article/details/6596336
2.https://blog.csdn.net/qq_39666638/article/details/77099284
以上。
参考文档:
1.http://velep.com/archives/1032.html
2.http://www.cnblogs.com/mickole/p/3246702.html
3.https://www.linuxidc.com/Linux/2012-11/73470p2.htm
4.https://blog.csdn.net/jxgz_leo/article/details/53458366
更多推荐
所有评论(0)