如何在没有core文件的情况下用dmesg+addr2line定位段错误

前言

在现网环境下,程序奔溃后不一定会留下core文件,原因有很多,比如存储空间不足就是其中一个常见的原因。此时我们只能依据linux记录的错误日志来定位问题。

涉及linux命令

本文涉及以下几条命令

1. dmesg命令,用于获取程序出错时的堆栈地址

1)dmesg |grep -E 'segfault|general'
可以通过该命令过滤出发生崩溃的程序,以及对应的堆栈信息。之前看网上的其他文章仅过滤segfault,但我在实践中发现"general protection"的提示信息也在告诉我们进程崩了。目前我只遇到segfault和general这两种情况,如果还有其他的过滤条件可以给我留言。

举例:
[root@vmware ~] dmesg |grep -E 'segfault|general'
[  374.549753] a.out[57228]: segfault at 0 ip 00000000004004fd sp 00007ffe7296f610 error 6 in a.out[400000+1000]
[  429.110096] b.out[96783]: segfault at 0 ip 00000000004004fd sp 00007ffcc3e697c0 error 6 in b.out[400000+1000]

字段说明:
1)ip:指令指针寄存器,字段后面的数字就是test程序出错时程序执行的位置
2)sp:堆栈指针寄存器
3)error:错误码,由三个字位组成的,从高到底分别为bit2 bit1和bit0
bit2: 值为1表示是用户态程序内存访问越界,值为0表示是内核态程序内存访问越界
bit1: 值为1表示是写操作导致内存访问越界,值为0表示是读操作导致内存访问越界
bit0: 值为1表示没有足够的权限访问非法地址的内容,值为0表示访问的非法地址根本没有对应的页面,也就是无效地址
4)b.out后面紧跟着基地址(这里是400000),后面的+10000不太明白是什么,知道的给我留言

2)dmesg |grep 进(线)程名
通过进程或线程名来过滤。这里之所以强调线程,因为我在实践中发现dmesg里的信息可能只有线程名,所以推荐在给线程取名时使用统一前缀,比如你的主进程为Test,那么线程可以取Test_A,Test_A,这样过滤时 grep Test就能过滤出所有想要的信息
举例:
[root@vmware ~] dmesg |grep a.out
[  374.549753] a.out[57228]: segfault at 0 ip 00000000004004fd sp 00007ffe7296f610 error 6 in a.out[400000+1000]

3)dmesg -C
dmesg命令查看到的信息在重启后将会被清空,若当前错误信息太多也可以通过该命令手动清空dmesg信息,以便下次问题的定位。-C(大写)参数为静默清空,如果清空前还想打印一次,可以通过-c(小写)参数。
注:
cat /var/log/messages |grep xxx
这里也保存进程奔溃信息,且重启后依然存在。
举例:
[root@vmware ~] cat /var/log/messages|grep b.out
May  8 09:24:04 vmware kernel: b.out[96783]: segfault at 0 ip 00000000004004fd sp 00007ffcc3e697c0 error 6 in b.out[400000+1000]
May  8 09:24:04 vmware abrt-hook-ccpp: Process 96783 (b.out) of user 0 killed by SIGSEGV - dumping core
May  8 09:24:05 vmware abrt-server: Executable '/root/b.out' doesn't belong to any package and ProcessUnpackaged is set to 'no'

 

2. date 用于转换dmesg信息里的时间

date -d "1970-01-01 UTC `echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+时间"|bc `seconds"
举例:
[  672.091250] a.out[26520]: segfault at 0 ip 00000000004004fd sp 00007ffe51b27fe0 error 6 in a.out[400000+1000]
[root@vmware ~] date -d "1970-01-01 UTC `echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+672.091250"|bc `seconds"                
2019年 05月 08日 星期三 09:40:02 CST

 

3. ldd 用于获取进程所依赖的动态库,以及所在位置

举例:
~ $ ldd a.out
     linux-vdso.so.1 (0x00007ffc24b9a000)
     libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fde40b63000)
     /lib64/ld-linux-x86-64.so.2 (0x00007fde41104000)

4. addr2line,将dmesg获取到的地址转换为代码中发成错误的文件、行号及函数名

奔溃发生的位置不同,该命令的使用方式也有所不同。区别主要在于传入的地址参数。

addr2line -e 进程名 IP指令地址 -f
addr2line -e 进程名 IP指令地址-基地址 -f

在本例1中,addr2line的参数为IP的地址。而在其它例子里,需要将“IP地址-基地址作”的值作为参数。原本我以为区别在与是在主进程中奔溃,还是在动态库中奔溃,但我将例1放在虚拟机+debian9.11上进行测试,发现也需要传入“IP地址-基地址”作为参数。

目前我的做法是(供参考):
如果基地址显示的是400000,我就不用IP地址减它。
如果IP地址根据经验看特别长,就需要减去基地址。(见下文实例分析)
多试是最靠谱的,最多2次就出结果了。

下面举一些实际的例子:
1)addr2line -e 进程名 IP指令地址 -f
注:本例在虚拟机+centos7.3中测试
举例:
在主程序中奔溃

#include <stdio.h>
int main()
{
       int *p = NULL;
       *p = 0;

       return 0;
 }

[root@vmware ~] gcc a.c -g      
[root@vmware ~] ./a.out
段错误(吐核)
[root@vmware ~] dmesg |grep a.out
[ 1310.167335] a.out[122089]: segfault at 0 ip 00000000004004fd sp 00007ffcf08f3ab0 error 6 in a.out[400000+1000]
[root@vmware ~] addr2line -e a.out 00000000004004fd -f
func
/root/a.c:5

通过该例子我们可以看到,程序发生段错误的函数以及具体位置。
需要注意的是如果编译程序时没有加上-g参数,就只能显示出函数名,显示不出具体所在文件的位置.

2)addr2line -e 进程名 IP指令地址-基地址 -f
举例-1:
在主程序中奔溃
注:本例在虚拟机+debian9.11中测试,代码与例1相同,但addr2line传入的地址参数不同。

#include <stdio.h>
int main()
{
       int *p = NULL;
       *p = 0;

       return 0;
 }


  [root@ ~] $ dmesg |grep a.out
[1281876.289005] a.out[1794281]: segfault at 0 ip 0000561801e55670 sp 00007ffec7a47c20 error 6 in a.out[561801e55000+1000]
  [root@ ~] $ addr2line -e ./a.out 0x670 -f
main
/root/test.c:5

这里0x670是0000561801e55670-561801e55000的值。在前面我提到说,如果IP地址根据经验看特别长,就需要减去基地址。那么一般是什么样的地址?我们可以通过readelf命令查看一下,我们可以看到执行代码从第三行开始,对应的地址是0x660。0x670在0x66c~0x676之间,也是第5行代码。和addr2line分析的结果一样。

[root@ ~] $readelf -w a.out |grep opcode
   [0x000000bd]  Extended opcode 2: set Address to 0x660
   [0x000000c8]  Special opcode 7: advance Address by 0 to 0x660 and Line by 2 to 3
   [0x000000c9]  Special opcode 62: advance Address by 4 to 0x664 and Line by 1 to 4
   [0x000000ca]  Special opcode 118: advance Address by 8 to 0x66c and Line by 1 to 5
   [0x000000cb]  Special opcode 147: advance Address by 10 to 0x676 and Line by 2 to 7
   [0x000000cc]  Special opcode 76: advance Address by 5 to 0x67b and Line by 1 to 8
   [0x000000cf]  Extended opcode 1: End of Sequence

举例-2:
在动态库中奔溃

#include<stdio.h>
#include<string.h>
void func()
{
        int *p = NULL;
        memcpy(p, "test", 4);
}

int main()
{
        func();
        return 0;
 }

[root@vmware ~] dmesg |grep a.out
[ 6807.501481] a.out[72684]: segfault at 0 ip 00007f6559bc7463 sp 00007fff80625b18 error 6 in libc-2.17.so[7f6559a7c000+1b6000]
[root@vmware ~] ldd a.out
         linux-vdso.so.1 =>  (0x00007ffc643f6000)
         libc.so.6 => /lib64/libc.so.6 (0x00007f83ef206000)
         /lib64/ld-linux-x86-64.so.2 (0x00007f83ef5e2000)
[root@vmware ~] addr2line -e /lib64/libc.so.6 14B463 -f
__memcpy_ssse3_back
:? 

这个这个例子我们可以看到,段错误发生的位置是在a.out进程调用的libc库里,我们先使用ldd找到动态库的位置,addr2line传入的地址参数使用14B463(00007f6559bc7463 - 7f6559a7c000),这里我们看到的是__memcpy_ssse3_back导致的,在glibc源码中查找,确定了是memcpy干的。

image
同样的代码在虚拟机+debian9.11中测试,并不能看出是memcpy,为了确定结果是正确的,我们使用gdb启动,发现段错误的位置是一样的。至少说明我们定位的方法是正确的,对于glibc这样的库定位起来是有些费劲。
[1288936.502170] a.out[1874406]: segfault at 0 ip 00007f9dce8800ba sp 00007ffe0a299bc8 error 6 in libc-2.24.so[7f9dce757000+195000]
[root@ ~]  $ addr2line -e /lib/x86_64-linux-gnu/libc-2.24.so 0x1290BA -f
__nss_passwd_lookup
/build/glibc-77giwP/glibc-2.24/string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:301

image


5、catchsegv,捕获段错误
catchsegv命令专门用来扑获段错误,它通过动态加载器(ld-linux.so)的预加载机制(PRELOAD),把一个事先写好的库(/lib/libSegFault.so)加载上,用于捕捉断错误的出错信息。需要在启动程序时就使用该命令,因此比较适合在复现问题时使用。此时各种堆栈等调试信息会打印屏幕上(类似于我们在程序里写代码捕获段错误信号,再将堆栈打印到日志里),需要注意种方法dmesg就看不到段错误信息了。

[root@ ~]  $ catchsegv ./a.out
我这里这列出了堆栈信息:

image

[root@ ~]  $ addr2line -e ./a.out 0x6f8 -f
func
/root/a.c:7
[root@ ~]  $ addr2line -e /lib/x86_64-linux-gnu/libc-2.24.so 0x1290ba -f
__nss_passwd_lookup
/build/glibc-77giwP/glibc-2.24/string/../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:301

 

附1:常见段错误产生的原因

1)访问不存在的内存地址

#include<stdio.h>
#include<stdlib.h>
void main()
{
        int *ptr = NULL;
        *ptr = 0;
}


2)访问系统保护的内存地址

#include<stdio.h>
#include<stdlib.h>
 void main()
 {
         int *ptr = (int *)0;
         *ptr = 100;
 }


3)访问只读的内存地址

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void main()
{
        char *ptr = "test";
         strcpy(ptr, "TEST");
 }


4)栈溢出

#include<stdio.h>
#include<stdlib.h>
void main()
{
        main();
}

附2:其他定位问题可能用到的工具

eu-readelf、objdump、nm

posted @ 2020-05-08 15:45  一觉醒来写程序  阅读(3280)  评论(2编辑  收藏  举报