addr2line 动态库[通俗易懂]

addr2line 动态库[通俗易懂]一、导读Backtrace中,一般都只有一些地址。但是利用addr2line这个工具,就可以找到对应的代码行。前提条件是可执行程序或者动态链接库编译的时候带-g选项。具体来说,分两种情况:如果关注的一行backtrace位于一个可执行文件中,那么直接addr2line-e<executable><address>如果关注的backtrace位于一个动态链接库中,那么麻烦一些,因为动态链接库的基地址不是固定的。这个时候,首先要把进程的memorymap找来。在L

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全家桶1年46,售后保障稳定

一、导读

Backtrace中,一般都只有一些地址。但是利用addr2line这个工具,就可以找到对应的代码行。前提条件是可执行程序或者动态链接库编译的时候带-g选项。

具体来说,分两种情况:

  1. 如果关注的一行backtrace位于一个可执行文件中,那么直接addr2line -e <executable> <address>

  2. 如果关注的backtrace位于一个动态链接库中,那么麻烦一些,因为动态链接库的基地址不是固定的。这个时候,首先要把进程的memory map找来。在Linux下,进程的memory map可以在/proc/<pid>/maps文件中得到。然后在这个文件中找到动态链接库的基地址,然后将backtrace中的地址 – 动态链接库的基地址,得到偏移地址offset address, 最后addr2line -e <shared library> <offset address>

当然,用GDB也可以找出地址对应的代码行。不过相比addr2line,GDB需要将BUG现象重现一遍,所以对于不好重现的BUG,或是随机重现的BUG来说,使用addr2line就可以直接从backtrace找到对应的代码行,不需要重现现象,比GDB使用起来更简单。

二、实践部分

1、获取程序的调用栈
在Linux上的C/C++编程环境下,我们可以通过如下三个函数来获取程序的调用栈信息。

#include <execinfo.h>
 
/* Store up to SIZE return address of the current program state in
   ARRAY and return the exact number of values stored.  */
int backtrace(void **array, int size);
 
/* Return names of functions from the backtrace list in ARRAY in a newly
   malloc()ed memory block.  */
char **backtrace_symbols(void *const *array, int size);
 
/* This function is similar to backtrace_symbols() but it writes the result
   immediately to a file.  */
void backtrace_symbols_fd(void *const *array, int size, int fd);

Jetbrains全家桶1年46,售后保障稳定

它们由GNU C Library提供,关于它们更详细的介绍可参考Linux Programmer’s Manual中关于backtrack相关函数的介绍。

使用它们的时候有一下几点需要我们注意的地方:

backtrace的实现依赖于栈指针(fp寄存器),在gcc编译过程中任何非零的优化等级(-On参数)或加入了栈指针优化参数-fomit-frame-pointer后多将不能正确得到程序栈信息;
backtrace_symbols的实现需要符号名称的支持,在gcc编译过程中需要加入-rdynamic参数;
内联函数没有栈帧,它在编译过程中被展开在调用的位置;
尾调用优化(Tail-call Optimization)将复用当前函数栈,而不再生成新的函数栈,这将导致栈信息不能正确被获取。

2、捕获系统异常信号输出调用栈
当程序出现异常时通常伴随着会收到一个由内核发过来的异常信号,如当对内存出现非法访问时将收到段错误信号SIGSEGV,然后才退出。利用这一点,当我们在收到异常信号后将程序的调用栈进行输出,它通常是利用signal()函数,关于系统信号的

三、从backtrace信息分析定位问题

1、测试程序
为了更好的说明和分析问题,我这里将举例一个小程序,它有三个文件组成分别是backtrace.c、dump.c、add.c,其中add.c提供了对一个数值进行加一的方法,我们在它的执行过程中故意使用了一个空指针并为其赋值,这样人为的造成段错误的发生;dump.c中主要用于输出backtrace信息,backtrace.c则包含了我们的man函数,它会先注册段错误信号的处理函数然后去调用add.c提供的接口从而导致发生段错误退出。它们的源程序分别如下:

/*
 *   add.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 
int add1(int num)
{
	int ret = 0x00;
	int *pTemp = NULL;
	
	*pTemp = 0x01;  /* 这将导致一个段错误,致使程序崩溃退出 */
	
	ret = num + *pTemp;
	
	return ret;
}
 
int add(int num)
{
	int ret = 0x00;
 
	ret = add1(num);
	
	return ret;
}
/*
 *   dump.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>	    /* for signal */
#include <execinfo.h> 	/* for backtrace() */
 
#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)
{
	
#if 0	
	char buff[64] = {0x00};
		
	sprintf(buff,"cat /proc/%d/maps", getpid());
		
	system((const char*) buff);
#endif	
 
	printf("\n=========>>>catch signal %d <<<=========\n", signo);
	
	printf("Dump stack start...\n");
	dump();
	printf("Dump stack end...\n");
 
	signal(signo, SIG_DFL); /* 恢复信号默认处理 */
	raise(signo);           /* 重新发送信号 */
}
/*
 *   backtrace.c
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>	    /* for signal */
#include <execinfo.h> 	/* for backtrace() */
 
extern void dump(void);
extern void signal_handler(int signo);
extern int add(int num);
 
int main(int argc, char *argv[])
{
	int sum = 0x00;
	
	signal(SIGSEGV, signal_handler);  /* 为SIGSEGV信号安装新的处理函数 */
	
	sum = add(sum);
	
	printf(" sum = %d \n", sum);
	
	return 0x00;
}

2、静态链接情况下的错误信息分析定位

我们首先将用最基本的编译方式将他们编译成一个可执行文件并执行,如下:

zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ gcc -g -rdynamic backtrace.c add.c dump.c -o backtrace
zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ ./backtrace   
=========>>>catch signal 11 <<<=========
Dump stack start...
backtrace() returned 8 addresses
  [00] ./backtrace(dump+0x1f) [0x400a9b]
  [01] ./backtrace(signal_handler+0x31) [0x400b63]
  [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f86afc7e150]
  [03] ./backtrace(add1+0x1a) [0x400a3e]
  [04] ./backtrace(add+0x1c) [0x400a71]
  [05] ./backtrace(main+0x2f) [0x400a03]
  [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f86afc6976d]
  [07] ./backtrace() [0x400919]
Dump stack end...
段错误 (核心已转储)

由此可见在调用完函数add1后就开始调用段错误信号处理函数了,所以问题是出在函数add1中。这似乎还不够,更准确的位置应该是在地址0x400a3e处,但这到底是哪一行呢,我们使用addr2line命令来得到,执行如下:

zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ addr2line -e backtrace 0x400a3e
/home/share/work/backtrace/add.c:13

2、动态链接情况下的错误信息分析定位
然而我们通常调试的程序往往没有这么简单,通常会加载用到各种各样的动态链接库。如果错误是发生在动态链接库中那么处理将变得困难一些。下面我们将上述程序中的add.c编译成动态链接库libadd.so,然后再编译执行backtrace看会得到什么结果呢。

/* 编译生成libadd.so */
gcc -g -rdynamic add.c -fPIC -shared -o libadd.so

/* 编译生成backtrace可执行文件 */
gcc -g -rdynamic backtrace.c dump.c -L. -ladd -Wl,-rpath=. -o backtrace

其中参数 -L. -ladd为编译时链接当前目录的libadd.so;参数-Wl,-rpath=.为指定程序执行时动态链接库搜索路径为当前目录,否则会出现执行找不到libadd.so的错误。然后执行backtrace程序结果如下:

zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ ./backtrace
=========>>>catch signal 11 <<<=========
Dump stack start...
backtrace() returned 8 addresses
  [00] ./backtrace(dump+0x1f) [0x400a53]
  [01] ./backtrace(signal_handler+0x31) [0x400b1b]
  [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f8583672150]
  [03] ./libadd.so(add1+0x1a) [0x7f85839fa5c6]
  [04] ./libadd.so(add+0x1c) [0x7f85839fa5f9]
  [05] ./backtrace(main+0x2f) [0x400a13]
  [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f858365d76d]
  [07] ./backtrace() [0x400929]
Dump stack end...
段错误 (核心已转储)

此时我们再用前面的方法将得不到任何信息,如下:

zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ addr2line -e libadd.so 0x7f85839fa5c6
??:0

这是为什么呢?

出现这种情况是由于动态链接库是程序运行时动态加载的而其加载地址也是每次可能多不一样的,可见0x7f85839fa5c6是一个非常大的地址,和能得到正常信息的地址如0x400a13相差甚远,其也不是一个实际的物理地址(用户空间的程序无法直接访问物理地址),而是经过MMU(内存管理单元)映射过的。

有上面的认识后那我们就只需要得到此次libadd.so的加载地址然后用0x7f85839fa5c6这个地址减去libadd.so的加载地址得到的结果再利用addr2line命令就可以正确的得到出错的地方;另外我们注意到(add1+0x1a)其实也是在描述出错的地方,这里表示的是发生在符号add1偏移0x1a处的地方,也就是说如果我们能得到符号add1也就是函数add1在程序中的入口地址再加上偏移量0x1a也能得到正常的出错地址。

我们先利用第一种方法即试图得到libadd.so的加载地址来解决这个问题。我们可以通过查看进程的maps文件来了解进程的内存使用情况和动态链接库的加载情况,所以我们在打印栈信息前再把进程的maps文件也打印出来,加入如下代码:

char buff[64] = {0x00};
	
sprintf(buff,"cat /proc/%d/maps", getpid());
	
system((const char*) buff);

然后编译执行得到如下结果(打印比较多这里摘取关键部分):

....................................................
7f0962fb3000-7f0962fb4000 r-xp 00000000 08:01 2895572                    /home/share/work/backtrace/libadd.so
7f0962fb4000-7f09631b3000 ---p 00001000 08:01 2895572                    /home/share/work/backtrace/libadd.so
7f09631b3000-7f09631b4000 r--p 00000000 08:01 2895572                    /home/share/work/backtrace/libadd.so
7f09631b4000-7f09631b5000 rw-p 00001000 08:01 2895572                    /home/share/work/backtrace/libadd.so
.....................................................
=========>>>catch signal 11 <<<=========
Dump stack start...
backtrace() returned 8 addresses
  [00] ./backtrace(dump+0x1f) [0x400b7f]
  [01] ./backtrace(signal_handler+0x83) [0x400c99]
  [02] /lib/x86_64-linux-gnu/libc.so.6(+0x36150) [0x7f0962c2b150]
  [03] ./libadd.so(add1+0x1a) [0x7f0962fb35c6]
  [04] ./libadd.so(add+0x1c) [0x7f0962fb35f9]
  [05] ./backtrace(main+0x2f) [0x400b53]
  [06] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f0962c1676d]
  [07] ./backtrace() [0x400a69]
Dump stack end...
段错误 (核心已转储)

Maps信息第一项表示的为地址范围如第一条记录中的7f0962fb3000-7f0962fb4000,第二项r-xp分别表示只读、可执行、私有的,由此可知这里存放的为libadd.so的.text段即代码段,后面的栈信息0x7f0962fb35c6也正好是落在了这个区间。所有我们正确的地址应为0x7f0962fb35c6 – 7f0962fb3000 = 0x5c6,将这个地址利用addr2line命令得到如下结果:

zoulm@zoulm-VirtualBox:/home/share/work/backtrace$ addr2line -e libadd.so 0x5c6
/home/share/work/backtrace/add.c:13

可见也得到了正确的出错行号。

接下来我们再用提到的第二种方法即想办法得到函数add的入口地址再上偏移量来得到正确的地址。要得到一个函数的入口地址我们多种途径和方法,比如生成查看程序的map文件;使用gcc的nm、readelif等命令直接对libadd.so分析等。在这里我们只介绍生成查看程序的map文件的方法,其他方法可通过查看gcc手册和google找到。

1)利用gcc编译生成的map文件,用如下命令我们将编译生成libadd.so对应的map文件如下:

gcc -g -rdynamic add.c -fPIC -shared -o libadd.so -Wl,-Map,add.map

Map文件中将包含关于libadd.so的丰富信息,我们搜索函数名add1就可以找到其在.text段的地址如下:

................................... 
.text          0x00000000000005ac       0x55 /tmp/ccCP0hNf.o
                0x00000000000005ac                add1
                0x00000000000005dd                add
...................................

由此可知我们的add1的地址为0x5ac,然后加上偏移地址0x1a即0x5ac + 0x1a = 0x5c6,由前面可知这个地址是正确的。

扩展:

通过在编译过程中生成so包的map文件,其中存放的是堆栈的各种信息,实际上除了生成map之外,还可以通过readelf -s 和nm两个命令来查询逻辑地址,实现对问题的定位。
例如:
在这里插入图片描述
其中libtest_lib,so对应的是libadd.so这个包,libtest_lib.so是我在自己电脑上打包起的名字。可以看出其实都是一样的地址。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/230834.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • Spring通过SchedulerFactoryBean实现调度任务的配置(定时器)

    Spring通过SchedulerFactoryBean实现调度任务的配置(定时器)<?xmlversion=”1.0″encoding=”UTF-8″?><beansxmlns=”http://www.springframework.org/schema/beans”xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”xmlns:contex…

  • settimeout时间误差_采集终端和电能表日计时误差

    settimeout时间误差_采集终端和电能表日计时误差setInterval指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterval指定每100ms执行一次,每次执行需要5ms,那么第一次执行结束后95毫秒,第二次执行就会开始。如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。为了确保两次执行之间有固定的间隔,可以不用setInterval,而是每次执行结束后,使用setTimeout指定下一次执行的具体时间。

  • mysql 根据时间范围查询

    mysql 根据时间范围查询时间格式为第一种写法:select*fromtestwherecreate_timebetween’2019-03-0513:04:07’and’2019-03-0813:04:07′;第二种写法:select*fromtestwherecreate_time&gt;=’2019-03-0513:04:07’anddate&lt…

  • java分前端后端吗_Java Web属于前端还是后端

    java分前端后端吗_Java Web属于前端还是后端JavaWeb属于前端还是后端发布时间:2020-06-1513:39:15来源:亿速云阅读:325作者:鸽子JavaWeb是前端还是后端?JavaWeb是属于后端,JavaWeb就是用Java技术开发的Web应用,而Java是一种可以编写跨平台应用软件、完全面向对象的高级程序设计语言,一般常用于后端服务器的开发和Android软件的开发。Java语言特点1、简单性Java看起来设计…

  • 10.Vuex组件中的mapState、mapGetters、mapMutations、mapActions等辅助函数

    10.Vuex组件中的mapState、mapGetters、mapMutations、mapActions等辅助函数State1.state中的数据,组件中如何获取this.$store.state.xxx属性2.mapState辅助函数1.引入mapStateimport{mapState}from’vuex’2.在computed计算属性中使用exportdefault{computed:mapState(…

  • Java Jsoup 解析处理百度谷歌搜索结果的示例代码

    Java Jsoup 解析处理百度谷歌搜索结果的示例代码本文主要介绍Java中,通过Jsoup来解析百度和谷歌中的搜索结果,获取搜索到的链接url和标题title的方法,以及相关的示例代码。原文地址:JavaJsoup解析处理百度谷歌搜索结果的示例代码

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号