热点新闻
如何定位Linux应用程序崩溃?
2023-07-10 19:00  浏览:180  搜索引擎搜索“爱农网”
温馨提示:信息一旦丢失不一定找得到,请务必收藏信息以备急用!本站所有信息均是注册会员发布如遇到侵权请联系文章中的联系方式或客服删除!
联系我时,请说明是在爱农网看到的信息,谢谢。
展会发布 展会网站大全 报名观展合作 软文发布

背景

最近项目中用到了一个库,在程序崩溃时可以生成exception文件,记录程序崩溃时的调用信息,对于定位问题比较有价值,因此整理下这个库涉及到的知识点。相关测试代码已经放到github可以下载调试。

基础知识

maps

maps用来描述进程的虚拟地址空间是如何使用的。总共包括六列,每列及其含义如下:

名字 含义
address 本段在虚拟内存中的地址范围。
perms 本段的权限,r-读,w-写,x-执行, p-私有,s-共享。
offset 即本段映射地址在文件中的偏移。
dev 主设备号与次设备号:所映射的文件所属设备的设备号。
inode 文件索引节点号。
pathname 映射的文件名。<br />对有名映射而言,是映射的文件名。<br />对匿名映射来说,是此段内存在进程中的作用。<br />[stack]表示本段内存作为栈来使用,[heap]作为堆来使用,其他情况则为无。

对于有名的内存区间而言,属性为r--p表示存放的是rodata;属性为rw-p存放的是bssdata;属性为r-xp表示存放的是text数据。没有文件名的内存区间则表示用mmap映射的匿名空间。

以下为./example/maps_test.c编译成的可执行文件mapstest的运行结果:

code addr = 0x55e1df08d6da A_global_addr = 0x55e1df28e034 B_global_init0_addr = 0x55e1df28e020 C_global_init_addr = 0x55e1df28e010 D_global_static_addr = 0x55e1df28e024 E_global_static_init0_addr = 0x55e1df28e028 F_global_static_init_addr = 0x55e1df28e014 G_global_const_addr = 0x55e1df08d998 a_addr = 0x7ffce299bb90 b_init0_addr = 0x7ffce299bb94 c_init_addr = 0x7ffce299bb98 d_static_addr = 0x55e1df28e02c e_static_init0_addr = 0x55e1df28e030 f_static_init_addr = 0x55e1df28e018 g_const_addr = 0x7ffce299bb9c h1_arr_addr = 0x7ffce299bbb2 h2_strconst_addr = 0x55e1df08d99c h2_point_addr = 0x7ffce299bba0 i_malloc_addr = 0x55e1dfd1d260

ps -ef| grep mapstest得到进程对应的pid号,maps文件如下:(路径为/proc/{pid}/maps

55e1df08d000-55e1df08e000 r-xp 00000000 08:01 24786338 /mapstest #text 55e1df28d000-55e1df28e000 r--p 00000000 08:01 24786338 /mapstest #rodata 55e1df28e000-55e1df28f000 rw-p 00001000 08:01 24786338 /mapstest #bss data 55e1dfd1d000-55e1dfd3e000 rw-p 00000000 00:00 0 [heap] #堆 7f64881e5000-7f64883cc000 r-xp 00000000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so 7f64883cc000-7f64885cc000 ---p 001e7000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so 7f64885cc000-7f64885d0000 r--p 001e7000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so 7f64885d0000-7f64885d2000 rw-p 001eb000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so 7f64885d2000-7f64885d6000 rw-p 00000000 00:00 0 7f64885d6000-7f64885fd000 r-xp 00000000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so 7f64887de000-7f64887e0000 rw-p 00000000 00:00 0 7f64887fd000-7f64887fe000 r--p 00027000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so 7f64887fe000-7f64887ff000 rw-p 00028000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so 7f64887ff000-7f6488800000 rw-p 00000000 00:00 0 7ffce297d000-7ffce299e000 rw-p 00000000 00:00 0 [stack] #栈 7ffce29ef000-7ffce29f2000 r--p 00000000 00:00 0 [vvar] 7ffce29f2000-7ffce29f4000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

对应地,我们可以找到每个变量在虚拟内存中的地址范围。其中动态链接库是程序运行时动态加载的而其加载地址也是每次可能不一样的

signal

Linux中的信号是一种消息处理机制, 它本质上是一个整数,不同的信号对应不同的值,由于信号的结构简单所以天生不能携带很大的信息量,但是信号在系统中的优先级是非常高的。

Linux中的很多常规操作中都会有相关的信号产生,先从我们最熟悉的场景说起: 通过键盘操作产生了信号:用户按下Ctrl-C,这个键盘输入产生一个硬件中断,使用这个快捷键会产生信号, 这个信号会杀死对应的某个进程。
通过shell命令产生了信号:通过kill命令终止某一个进程,kill -9 进程PID
通过函数调用产生了信号:如果CPU当前正在执行这个进程的代码调用,比如函数 sleep(),进程收到相关的信号,被迫挂起。
通过对硬件进行非法访问产生了信号:正在运行的程序访问了非法内存,发生段错误,进程退出。

信号也可以实现进程间通信,但是信号能传递的数据量很少,不能满足大部分需求,另外信号的优先级很高,并且它对应的处理动作是回调完成的,它会打乱程序原有的处理流程,影响到最终的处理结果。因此非常不建议使用信号进行进程间通信。

通过 kill -l 命令可以查看系统定义的信号列表。

进程对信号的处理可以有以下三种措施:

  1. 忽略这个信号;

  2. 执行用户定义相应操作;

  3. 执行默认的操作;

SIGKILLSIGTSTOP是不可以被信号处理函数捕捉或者忽略。

常用接口:

函数名 备注
signal 信号安装函数,但是有消息重入问题,不建议使用
sigaction 信号安装函数
kill 给指定进程发送信号
raise 给自己发送信号,和kill(getpid( ),sig)等价
alarm 设置定时器,定时器超时后,发送一个SIGALRM信号

例子:

#include <stdio.h> #include <unistd.h> #include <signal.h> void catch_signal(int sig) { switch(sig) { case SIGINT:printf("get SIGINT signal\n"); } } int main (int argc,char *argv[ ]) { signal(SIGINT,catch_signal); int i = 0; while(1) { sleep(100);//执行完信号后sleep()立即返回,不会一直休眠下去 printf("hello i = %d",i++); } return 0; }

#include <stdio.h> #include <unistd.h> #include <signal.h> int temp = 0; void handler_sigint(int signo) { printf("recv SIGINT\n"); sleep(5); temp += 1; printf("the value of temp is:%d\n",temp); printf("in handler_sigint, after sleep\n"); } int main() { struct sigaction act; act.sa_handler = handler_sigint; act.sa_flags = SA_NOMASK; sigaction(SIGINT, &act, NULL); while(1); return 0; }

objdump

objdump -d可以将目标文件、动态库、可执行文件反汇编,如下:

00000000000006da <swap>: 6da: 55 push %rbp 6db: 48 89 e5 mov %rsp,%rbp 6de: 48 89 7d e8 mov %rdi,-0x18(%rbp) 6e2: 48 89 75 e0 mov %rsi,-0x20(%rbp) 6e6: 48 8b 45 e8 mov -0x18(%rbp),%rax 6ea: 8b 00 mov (%rax),%eax 6ec: 89 45 fc mov %eax,-0x4(%rbp) 6ef: 48 8b 45 e0 mov -0x20(%rbp),%rax 6f3: 8b 10 mov (%rax),%edx 6f5: 48 8b 45 e8 mov -0x18(%rbp),%rax 6f9: 89 10 mov %edx,(%rax) 6fb: 48 8b 45 e0 mov -0x20(%rbp),%rax 6ff: 8b 55 fc mov -0x4(%rbp),%edx 702: 89 10 mov %edx,(%rax) 704: 90 nop 705: 5d pop %rbp 706: c3 retq

00000000000006da是函数的地址,<swap>是函数名,整个汇编文件分为三列,分别是指令地址、指令机器码、指令机器码反汇编得到的指令。

strip

实际项目中,许多组件编译后,都会使用strip命令减小目标文件的大小,处理后的文件依然可以正常运行,但是其中的符号信息(比如函数名)会失去。出问题后不利于定位。

解决方法是在编译(gcc -c)阶段加入-rdynamic选项,此方法会将函数名加入到*.dyn节中,strip对其无效。

backtrace

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

#include <execinfo.h> int backtrace(void **array, int size); char **backtrace_symbols(void *const *array, int size); void backtrace_symbols_fd(void *const *array, int size, int fd);

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

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

崩溃定位

在程序崩溃时,系统会发送信号,在注册的信号处理函数中,将进程的maps文件保存下来,同时记录此时的函数调用链,利用这些信息就可以进行故障定位。前提是需要添加编译选项-g(不加也没事,不过用addr2line获得崩溃代码的行号需要),链接选项-rdynamic一定要加) 。

在可执行文件中崩溃

在64位Linux上编译运行example下的test,程序崩溃:

hello world segmentfault addr 0x55daa333903e =========>>>maps <<<========= 55daa3338000-55daa333a000 r-xp 00000000 08:01 24786361 /example/test 55daa3539000-55daa353a000 r--p 00001000 08:01 24786361 /example/test 55daa353a000-55daa353b000 rw-p 00002000 08:01 24786361 /example/test 55daa3e40000-55daa3e61000 rw-p 00000000 00:00 0 [heap] 7f09107eb000-7f09109d2000 r-xp 00000000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so 7f09109d2000-7f0910bd2000 ---p 001e7000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so 7f0910bd2000-7f0910bd6000 r--p 001e7000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so 7f0910bd6000-7f0910bd8000 rw-p 001eb000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so 7f0910bd8000-7f0910bdc000 rw-p 00000000 00:00 0 7f0910bdc000-7f0910c03000 r-xp 00000000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so 7f0910de4000-7f0910de6000 rw-p 00000000 00:00 0 7f0910e03000-7f0910e04000 r--p 00027000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so 7f0910e04000-7f0910e05000 rw-p 00028000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so 7f0910e05000-7f0910e06000 rw-p 00000000 00:00 0 7ffc3e767000-7ffc3e788000 rw-p 00000000 00:00 0 [stack] 7ffc3e79e000-7ffc3e7a1000 r--p 00000000 00:00 0 [vvar] 7ffc3e7a1000-7ffc3e7a3000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] =========>>>catch signal 11 <<<========= # 信号11是段错误 Dump stack start... backtrace() returned 7 addresses [00] ./test(dump+0x2e) [0x55daa3338d98] [01] ./test(signal_handler+0xb8) [0x55daa3338f2a] [02] /lib/x86_64-linux-gnu/libc.so.6(+0x3ef20) [0x7f0910829f20] [03] ./test(segmentfault+0x3a) [0x55daa3339078] #这里崩溃 [04] ./test(main+0x36) [0x55daa3338f9c] [05] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f091080cb97] [06] ./test(_start+0x2a) [0x55daa3338c8a] Dump stack end... Segmentation fault (core dumped)

由于64位系统运行的可执行文件的符号表地址和实际运行时地址差异甚大。

崩溃地址0x55daa3339078是动态映射的虚拟地址,该虚拟地址是通过符号表地址+该代码段映射区间(maps里面有)的地址得来的。

0x55daa3339078落在区间55daa3338000-55daa333a000

得到真正的符号表地址0x55daa3339078-0x55daa3338000=0x1078

daniel@daniel:~/example$ addr2line -e test 1078 ~/example/calc.c:44

32位系统显示的是实际地址,可以不用转换。

上面是获得符号表地址的一种方法,也可以使用objdump -d testtest反汇编找到segmentfault的地址:

000000000000103e <segmentfault>: 103e: 55 push %rbp 103f: 48 89 e5 mov %rsp,%rbp 1042: 48 83 ec 10 sub $0x10,%rsp 1046: 48 8d 35 f1 ff ff ff lea -0xf(%rip),%rsi # 103e <segmentfault> 104d: 48 8d 3d b1 01 00 00 lea 0x1b1(%rip),%rdi # 1205 <_IO_stdin_used+0xe5> 1054: b8 00 00 00 00 mov $0x0,%eax 1059: e8 a2 fb ff ff callq c00 <printf@plt> 105e: c7 45 f0 0a 00 00 00 movl $0xa,-0x10(%rbp) 1065: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%rbp) 106c: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp) 1073: 00 1074: 48 8b 45 f8 mov -0x8(%rbp),%rax 1078: c7 00 01 00 00 00 movl $0x1,(%rax) 107e: 48 8b 45 f8 mov -0x8(%rbp),%rax 1082: 8b 10 mov (%rax),%edx 1084: 8b 45 f0 mov -0x10(%rbp),%eax 1087: 01 d0 add %edx,%eax 1089: 89 45 f4 mov %eax,-0xc(%rbp) 108c: 8b 45 f4 mov -0xc(%rbp),%eax 108f: c9 leaveq 1090: c3 retq 1091: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 1098: 00 00 00 109b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

真正的符号表地址为000000000000103e + 0x3a = 0x1078

在动态库中崩溃

hello world add(1,2)=3 segmentfault addr 0x7f7335fb483a =========>>>maps <<<========= 5631f8167000-5631f8169000 r-xp 00000000 08:01 24786364 /example/test_dynamic 5631f8368000-5631f8369000 r--p 00001000 08:01 24786364 /example/test_dynamic 5631f8369000-5631f836a000 rw-p 00002000 08:01 24786364 /example/test_dynamic 5631f8706000-5631f8727000 rw-p 00000000 00:00 0 [heap] 7f7335bc3000-7f7335daa000 r-xp 00000000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so 7f7335daa000-7f7335faa000 ---p 001e7000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so 7f7335faa000-7f7335fae000 r--p 001e7000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so 7f7335fae000-7f7335fb0000 rw-p 001eb000 08:01 10490449 /lib/x86_64-linux-gnu/libc-2.27.so 7f7335fb0000-7f7335fb4000 rw-p 00000000 00:00 0 7f7335fb4000-7f7335fb5000 r-xp 00000000 08:01 24786363 /example/libcalc.so 7f7335fb5000-7f73361b4000 ---p 00001000 08:01 24786363 /example/libcalc.so 7f73361b4000-7f73361b5000 r--p 00000000 08:01 24786363 /example/libcalc.so 7f73361b5000-7f73361b6000 rw-p 00001000 08:01 24786363 /example/libcalc.so 7f73361b6000-7f73361dd000 r-xp 00000000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so 7f73363bb000-7f73363be000 rw-p 00000000 00:00 0 7f73363db000-7f73363dd000 rw-p 00000000 00:00 0 7f73363dd000-7f73363de000 r--p 00027000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so 7f73363de000-7f73363df000 rw-p 00028000 08:01 10490421 /lib/x86_64-linux-gnu/ld-2.27.so 7f73363df000-7f73363e0000 rw-p 00000000 00:00 0 7fffe6dfd000-7fffe6e1e000 rw-p 00000000 00:00 0 [stack] 7fffe6fd4000-7fffe6fd7000 r--p 00000000 00:00 0 [vvar] 7fffe6fd7000-7fffe6fd9000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] =========>>>catch signal 11 <<<========= # 信号11是段错误 Dump stack start... backtrace() returned 7 addresses [00] ./test_dynamic(dump+0x2e) [0x5631f8167cc8] [01] ./test_dynamic(signal_handler+0xb8) [0x5631f8167e5a] [02] /lib/x86_64-linux-gnu/libc.so.6(+0x3ef20) [0x7f7335c01f20] [03] ./libcalc.so(segmentfault+0x3d) [0x7f7335fb4877] #在动态库里面崩溃 [04] ./test_dynamic(main+0x58) [0x5631f8167eee] [05] /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xe7) [0x7f7335be4b97] [06] ./test_dynamic(_start+0x2a) [0x5631f8167bba] Dump stack end... Segmentation fault (core dumped)

同样地获得符号表地址:0x7f7335fb4877 - 0x7f7335fb4000 = 0x877

daniel@daniel:~/example$ addr2line -e libcalc.so 877 /example/calc.c:44

objdump -d libcalc.so获得segmentfault符号地址000000000000083a,加上偏移0x3d得到 0x877

000000000000083a <segmentfault>: 83a: 55 push %rbp 83b: 48 89 e5 mov %rsp,%rbp 83e: 48 83 ec 10 sub $0x10,%rsp 842: 48 8b 05 9f 07 20 00 mov 0x20079f(%rip),%rax # 200fe8 <segmentfault@@base+0x2007ae> 849: 48 89 c6 mov %rax,%rsi 84c: 48 8d 3d 46 00 00 00 lea 0x46(%rip),%rdi # 899 <_fini+0x9> 853: b8 00 00 00 00 mov $0x0,%eax 858: e8 43 fe ff ff callq 6a0 <printf@plt> 85d: c7 45 f0 0a 00 00 00 movl $0xa,-0x10(%rbp) 864: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%rbp) 86b: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp) 872: 00 873: 48 8b 45 f8 mov -0x8(%rbp),%rax 877: c7 00 01 00 00 00 movl $0x1,(%rax) 87d: 48 8b 45 f8 mov -0x8(%rbp),%rax 881: 8b 10 mov (%rax),%edx 883: 8b 45 f0 mov -0x10(%rbp),%eax 886: 01 d0 add %edx,%eax 888: 89 45 f4 mov %eax,-0xc(%rbp) 88b: 8b 45 f4 mov -0xc(%rbp),%eax 88e: c9 leaveq 88f: c3 retq

参考文献

  1. 跟我一起写Makefile
  2. linux进程地址空间
  3. /proc/{pid}/maps简要分析
  4. Linux利用maps文件和反汇编定位崩溃地址
  5. Linux信号处理程序的一个应用定位故障
  6. 在Linux中如何利用backtrace信息解决问题
  7. objdump(Linux)反汇编命令使用指南
  8. addr2line 动态库
  9. linux64位系统 addr2line使用
  10. strip命令介绍
发布人:d1d5****    IP:101.229.83.***     举报/删稿
展会推荐
让朕来说2句
评论
收藏
点赞
转发