zoukankan      html  css  js  c++  java
  • 如何在没有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

  • 相关阅读:
    2-5
    2-3
    2-2
    2-1
    1-1
    实验6-1 求数组及其下标
    实验4-2 关于求阶乘的运算
    作业 3-5 switch语句的应用
    作业3-6 查询水果单价
    作业3-4 判断是不是闰年
  • 原文地址:https://www.cnblogs.com/realjimmy/p/12850884.html
Copyright © 2011-2022 走看看