【嵌入式】嵌入式开发中如何使用addr2line工具调试段错误

🧑 作者简介:阿里巴巴嵌入式技术专家,深耕嵌入式+人工智能领域,具备多年的嵌入式硬件产品研发管理经验。

📒 博客介绍:分享嵌入式开发领域的相关知识、经验、思考和感悟,欢迎关注。提供嵌入式方向的学习指导、简历面试辅导、技术架构设计优化、开发外包等服务,有需要可私信联系。

🗄️ 专栏介绍:本文归属于专栏《嵌入式开发工具》,专注嵌入式开发中的常用工具,持续更新中,欢迎大家免费订阅关注。

遇到段错误时,系统一般会给出出错时运行的指令地址,可以通过出错地址找到出错位置,一般情况下,我们可以直接精确到所在代码文件的哪一行。

可执行程序的段错误

首先,需要制造一个段错误现场;示例程序sigsegv.c如下:

#include <stdio.h>
#include <string.h>

int main(void)
{
    char *addr = NULL;
    
    strcpy(addr, "crash here");
    
    return 0;
}

上述程序制造了一个段错误的实例场景;接下来尝试就编译运行这个程序找到段错误所在的行号(即segment.c的第8行)。
编译:

$ mipsel-linux-gcc -g sigsegv.c -o sigsegv

运行:

root@SmartAudio:/tmp# ./sigsegv
[  944.352628] do_page_fault() #2: sending SIGSEGV to sigsegv for invalid write access to
[  944.352645] 00000003 (epc == 00400604, ra == 2ae675f4)
Segmentation fault

PC端需要手动执行dmesg命令(执行echo 1 > /proc/sys/kernel/userprocess_debug开启更多的调试信息)可以看到出错时的指令地址(ip),如下所示:

$ dmesg | grep segment
[ 6172.715338] segment[3427]: segfault at 0 ip 00000000004004cb sp 00007fffeb96fd60 error 6 in segment[400000+1000]

接下来就根据这个地址查找出错的代码位置。命令如下:

$ addr2line -e sigsegv 0x00400604 -aifp
0x00400604: main at /disk2/ylguo/wifi-audio/repo-m150/mozart/sigsegv.c:6

通过addr2line,就可以直接得到出错位置对应的就是sigsegv.c的第6行。上述命令中,

-e, 出现段错误的ELF文件,不使用-e选项时,默认解析./a.out文件,找不到a.out就会报错。
-f, 打印所在函数
-i, 果错误出在inline函数中,将递归往会打印调用信息,直到回到非inline函数(没有试出来效果,原因未知)。
-a, 在函数,文件,行号前添加地址
-p, 使输出结果更直观,更易读
-s, 去掉文件路径的前缀,仅显示文件名,不显示路径
-C, c++支持,解析C++里的符号,使得结果更易读,如c++里的符号_Z1fv会被解析成f(),其间的转换可参考'c++filt -n _Z1fv'命令

除了-e选项和pc地址之外,其它选项均可以忽略,各个选项的具体效果可自行尝试。

动态库的段错误

可执行程序根据指令地址就可以直接拿到出错文件的行号和函数名。大家都知道,动态库的符号地址是位置无关(-fPIC)的,每次被加载的地址都不固定,那么如果错误出在动态库里,应该怎麽查呢?
下面,再来模拟一个段错误出在动态库里的场景;源码如下:
api.c文件内容如下所示:

#include <string.h>

void test(void)
{
	char *addr = NULL;
	strcpy(addr, "dadas");
}

sigsegv.c文件内容如下所示:

#include <stdio.h>
#include <string.h>

extern void test(void);

int main(void)
{
	test();
	return 0;
}

将api.c编译为libtest.so,segment.c链接libtest.so进行编译,如下所示:

$ mipsel-linux-gcc -fPIC -shared -g api.c -o libtest.so
$ mipsel-linux-gcc sigsegv.c -o sigsegv -L. -ltest

运行:

root@SmartAudio:/tmp# ./sigsegv
[ 1404.275231] do_page_fault() #2: sending SIGSEGV to sigsegv for invalid write access to
[ 1404.275249] 00000003 (epc == 2b12e6cc, ra == 004007c8)
Segmentation fault

运行同样出现段错误,但是此时还无法定位段错误。原因是0x2b12e6cc这个地址为库加载地址,也就是说这个错误出在动态库中。动态库跟可执行程序不一样,可支持程序编译完成之后入口地址就固定了,而动态库是被可执行程序加载时指配的,在进程生命周期内该地址是有效的,下次动态库被加载时会重新指配加载地址,上次的加载地址已经无效了,这就是上面虽然打印了epc地址但是无法定位段错误的原因。

动态库中各个符号的的真正地址是根据库加载地址加上各个符号的偏移地址得来的。所以只要知道了指令地址,再知道了出错时库的加载地址,两个地址做差即可得到出错指令在动态库中的偏移地址。可支持程序编译完成之后地址就已经固定下来了,所以可以通过指令地址直接查找代码行。相对的,动态库的入口地址虽然不固定,但是其偏移地址却是固定的,也就是说可以通过偏移地址找到出错位置。那么,怎么查库的加载地址呢?修改sigsegv.c实现出现段错误时打印进程当时虚拟地址空间使用情况,从中找到库的加载地址。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

extern void test(void);

void sig_handler(int signo)
{
        char cmd[32] = {};

        sprintf(cmd, "cat /proc/%d/maps", getpid());

        system(cmd);

        exit(-1);
}

int main(int argc, char *argv[])
{
        signal(SIGSEGV, sig_handler);
        
        test();
        
        return 0;
}

重新编译sigsegv.c:

$ mipsel-linux-gcc sigsegv.c -o sigsegv -L. -ltest

运行:

root@SmartAudio:/tmp# ./sigsegv
[ 6537.854674] do_page_fault() #2: sending SIGSEGV to sigsegv for invalid write access to
[ 6537.854691] 00000003 (epc == 2ba3e6cc, ra == 00400900)
00400000-00401000 r-xp 00000000 00:0b 171912     /tmp/sigsegv
00410000-00411000 rw-p 00000000 00:0b 171912     /tmp/sigsegv
2ba0b000-2ba2c000 r-xp 00000000 1f:03 627580     /lib/ld-2.16.so
2ba2c000-2ba2e000 rw-p 00000000 00:00 0 
2ba3c000-2ba3d000 r--p 00021000 1f:03 627580     /lib/ld-2.16.so
2ba3d000-2ba3e000 rw-p 00022000 1f:03 627580     /lib/ld-2.16.so
**2ba3e000-2ba3f000 r-xp 00000000 00:0b 34536      /tmp/libtest.so**
2ba3f000-2ba4e000 ---p 00001000 00:0b 34536      /tmp/libtest.so
2ba4e000-2ba4f000 rw-p 00000000 00:0b 34536      /tmp/libtest.so
2ba4f000-2bbae000 r-xp 00000000 1f:03 723232     /lib/libc-2.16.so
2bbae000-2bbbe000 ---p 0015f000 1f:03 723232     /lib/libc-2.16.so
2bbbe000-2bbc1000 r--p 0015f000 1f:03 723232     /lib/libc-2.16.so
2bbc1000-2bbc4000 rw-p 00162000 1f:03 723232     /lib/libc-2.16.so
2bbc4000-2bbc6000 rw-p 00000000 00:00 0 
7fecd000-7feee000 rw-p 00000000 00:00 0          [stack]
7fff7000-7fff8000 r-xp 00000000 00:00 0          [vdso]

通过上述结果,我们可知如下信息:

  1. 出段错误的地址地址为0x2ba3e6cc
  2. 0x2ba3e6cc位于/tmp/libtest.so2ba3e000-2ba3f000加载空间段内。

libtest.sosigsegv加载到了0x2ba3e000地址处,出错指令地址0x2ba3e6cc,两者做差0x2ba3e6cc - 0x2ba3e000得到出错指令位于libtest.so的偏移地址0x6cc处。尝试通过addr2line命令来查找0x6cc位于libtest.so的哪一行,如下所示:

$ addr2line -e libtest.so 0x6cc -aifp
0x000006cc: test at /disk2/ylguo/wifi-audio/repo-m150/mozart/api.c:6

api.c的第6行正好就是出现段错误的那一行。

c++程序

c++程序编译得到的ELF文件中,因为c++重载的特性,所以各符号都会被重新编码,为了更容易的得到函数名,addr2line需要添加-C选项。其它操作步骤不变。

课后作业

现有segment.cpp文件,使用上述工具得到出错行号。

#include <iostream>
#include <cstring>

using namespace std;

int a()
{
	int i = 0;
	
	strcpy(0x0, "djka");
}

int main(void)
{
	a();
	
	return 0;
}

注意事项

  1. 通过进程地址空间使用情况,可以确定出错指令出在可执行程序中还是动态库中;

  2. 编译所有源文件都要添加-g选项。如果可执行文件中没有包括调试符号(编译时没有添加-g选项),addr2line找不到相关信息,直接返回??:0。如下所示:

$ addr2line -e sigsegv 0x6cc -aifp
0x000006cc: ??
??:0
  1. 调试c++程序时,在addr2line命令中添加-C选项可使输出结果更加易读(可以在使用和不用-C选项两种情况下比较输出结果)。

  2. 程序运行时允许执行strip操作,但是addr2line命令必须使用添加-g选项编译并且没有strip的ELF文件。

  3. 尽量提前注册信号处理函数以便截获段错误信号。

  4. 内核没有打印do_page_fault() 却因为SIGSEGV进入了段错误信号处理函数的情况说明可能出错的真正出错原因并不是段错误(一般都是内存使用有问题),但是内核使用了段错误信号。此时得不到pc值,无法查找出错位置。

  5. 有些段错误比较隐蔽,不容易复现,所以建议在应用程序中尽量的都添加上述信号处理函数,以备不时之需。另外还要注意保留一份带有调试信息的ELF文件。

原理分析

使用-g选项编译得到的LEF文件会添加一些debug段,如下所示:

$ gcc segment.c -o segment
$ readelf --sections segment  | grep debug
$
$ gcc segment.c -g -o segment
$ readelf --sections segment  | grep debug
  [27] .debug_aranges    PROGBITS         0000000000000000  00001042
  [28] .debug_info       PROGBITS         0000000000000000  00001072
  [29] .debug_abbrev     PROGBITS         0000000000000000  00001100
  [30] .debug_line       PROGBITS         0000000000000000  00001141
  [31] .debug_str        PROGBITS         0000000000000000  00001180
  [32] .debug_loc        PROGBITS         0000000000000000  000011f3

其中,.debug_line这个段里面就包含了每条汇编指令的地址和对应的源代码行数,对于.debug_line段而言,是没有函数这一概念的,它只有文件概念,如下所示:

$ gcc segment.c -g -o segment
$ readelf -wl segment
Raw dump of debug contents of section .debug_line:

  Offset:                      0x0
  Length:                      59
  DWARF Version:               2
  Prologue Length:             32
  Minimum Instruction Length:  1
  Initial value of 'is_stmt':  1
  Line Base:                   -5
  Line Range:                  14
  Opcode Base:                 13
  
 Opcodes:
  Opcode 1 has 0 args
  Opcode 2 has 1 args
  Opcode 3 has 1 args
  Opcode 4 has 1 args
  Opcode 5 has 1 args
  Opcode 6 has 0 args
  Opcode 7 has 0 args
  Opcode 8 has 0 args
  Opcode 9 has 1 args
  Opcode 10 has 0 args
  Opcode 11 has 0 args
  Opcode 12 has 1 args

 The Directory Table is empty.

 The File Name Table:
  EntryDirTimeSizeName
  1000segment.c

 Line Number Statements:
  Extended opcode 2: set Address to 0x4004b4
  Special opcode 11: advance Address by 0 to 0x4004b4 and Line by 6 to 7
  Special opcode 63: advance Address by 4 to 0x4004b8 and Line by 2 to 9
  Advance PC by constant 17 to 0x4004c9
  Special opcode 119: advance Address by 8 to 0x4004d1 and Line by 2 to 11
  Special opcode 76: advance Address by 5 to 0x4004d6 and Line by 1 to 12
  Advance PC by 2 to 0x4004d8
  Extended opcode 1: End of Sequence

可以发现,debug段里的The File Name Table部分保存了文件名信息,还有一部分是 Line Number Statements,这里就包含了地址和行号的对应关系。objdump,addr2linegdb等debug工具会解析这些debug段,进而提供丰富的debug功能。详细的addr2line的工作原理见参考资料1

延伸使用

程序运行遇到段错误时,会收到SIGSEGV信号,该信号默认动作会结束当前进程运行;我们可以给SIGSEGV这个信号定义一个信号处理函数,在信号处理函数中,打印函数调用栈信息(包含函数调用和指令地址),通过函数调用可以分析程序的执行流程,根据指令地址可以精确的跟踪程序执行到哪一行调用的其它函数,通过出错时的指令地址,得到出错文件文件的行号。测试程序callltrace.c如下所示:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <execinfo.h>
#include <signal.h>

void sigsegv_handler(int signo)
{
	printf("recieved SIGSEGV signal, Call Trace:\n");
	
	void *array[10];
	size_t size; 
	char **strings;
	size_t i;

	size = backtrace (array, 10);
	strings = backtrace_symbols (array, size);

	for (i = 0; i < size; i++)
		printf ("   %s\n", strings[i]);
	free (strings);
	
	exit(1);
}

void aa(void)
{
	char *addr = NULL;

	strcpy(addr, "crash_at_here");
}

void test(void)
{
	aa();
}

int main(void)
{
	signal(SIGSEGV, &sigsegv_handler);

	test();

	return 0;
}

编译运行,如下所示:

$ gcc calltrace.c -g -rdynamic -o calltrace #-rdynamic选项会使得backtrace的结果会更直观
$ ./calltrace
recieved SIGSEGV signal, Call Trace:
   ./calltrace(sigsegv_handler+0x26) [0x40094a]
   /lib/x86_64-linux-gnu/libc.so.6(+0x364a0) [0x7f57c334b4a0]
   ./calltrace(aa+0x17) [0x4009d2]
   ./calltrace(test+0x9) [0x4009e7]
   ./calltrace(main+0x18) [0x400a01]
   /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7f57c333676d]
   ./calltrace() [0x400869]

可以看到,程序执行流程(不考虑libc部分)为:
0x400869 --> 0x400a01 --> 0x4009e7 --> 0x4009d2 --> 0x40094a,通过addr2line可以依次反查代码所在行,如下所示:

$ addr2line -e calltrace 0x400869 0x400a01 0x4009e7 0x4009d2 0x40094a -f -a -p -s
0x0000000000400869: _start at ??:0
0x0000000000400a01: main at calltrace.c:43
0x00000000004009e7: test at calltrace.c:36
0x00000000004009d2: aa at calltrace.c:30
0x000000000040094a: sigsegv_handler at calltrace.c:16

到这里,需要的信息就都展示出来了。

关于gdb

如果运行环境有gdb,并且程序运行能够稳定复现段错误,可以直接使用gdb segment-r命令执行程序,到了段错误的地方,gdb会自动打印位置,如果gdb能够找到源码文件,那么连出错位置的代码都有可以直接打印出来。如下所示:

$ gdb ./calltrace 
GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://bugs.launchpad.net/gdb-linaro/>...
Reading symbols from /home/gyl/segment/calltrace...done.
(gdb) r
Starting program: /home/gyl/segment/calltrace 

Program received signal SIGSEGV, Segmentation fault.
0x00000000004009d2 in aa () at calltrace.c:30
30		strcpy(addr, "crash");
(gdb) 

参考资料

  1. addr2line工具实现原理以及dwraf格式分析:http://blog.chinaunix.net/uid-26339466-id-3301922.html

  2. 用Graphviz可视化函数调用:http://www.ibm.com/developerworks/cn/linux/l-graphvis/

  3. [英]使用gdb调试段错误(segment fault):http://www.unknownroad.com/rtfm/gdbtut/gdbsegfault.html

  4. [译]使用gdb调试段错误(segment fault) :http://blog.csdn.net/deutschester/article/details/6739861

  5. Linux debug : addr2line追踪出错地址:http://www.linuxidc.com/Linux/2011-05/35780.htm

  6. addr2line源码:http://sourceforge.net/p/elftoolchain/code/HEAD/tree/trunk/addr2line/addr2line.c

  7. __builtin_return_address可以得出函数返回地址

  • 33
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

I'mAlex

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值