Linux程序coredump地址显示问号的调试方法 - 基于map文件

coredump即Linux系统上,应用程序崩溃时的运行栈快照,已便于定位崩溃问题

正确使用coredump需要几个条件:

第一,coredump本身的配置

设置coredump文件路径及名称

 echo "$dir/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

记录pid

 echo 1 > /proc/sys/kernel/core_uses_pid

设置coredump文件大小:

ulimit -c unlimited

ulimit设置需要注意环境的问题,必须在程序执行的环境中设置此值,一般ulimit设置在bashrc或系统启动时/etc/profile

第二,程序编译选项

-g编译,加入调试符号

-O0,不进行编译优化

no strip,不进行strip,这一点很重要

有了以上几点,就可以保证coredump的正常运行,如下:

Program terminated with signal SIGABRT, Aborted.
#0  0x00007f66a4dcf438 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
54      ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
--Type for more, q to quit, c to continue without paging--
[Current thread is 1 (Thread 0x7f66a4d99700 (LWP 3991))]
(gdb) bt
#0  0x00007f66a4dcf438 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007f66a4dd1197 in __GI_abort () at abort.c:118
#2  0x00007f66a4e117fa in __libc_message (do_abort=do_abort@entry=2, fmt=fmt@entry=0x7f66a4f2afd8 "*** Error in `%s': %s: 0x%s ***\n")
    at ../sysdeps/posix/libc_fatal.c:175
#3  0x00007f66a4e1a38a in malloc_printerr (ar_ptr=, ptr=,
    str=0x7f66a4f2b0a0 "double free or corruption (fasttop)", action=3) at malloc.c:5020
#4  _int_free (av=, p=, have_lock=0) at malloc.c:3874
#5  0x00007f66a4e1e58c in __GI___libc_free (mem=) at malloc.c:2975
#6  0x0000000000400a59 in free_glb_resource (fmt=0x400ef0 <__FUNCTION__.4467> "_handle1") at coredump.c:69
#7  0x0000000000400b99 in _handle1 (arg=0x0) at coredump.c:146
#8  0x00007f66a516b6ba in start_thread (arg=0x7f66a4d99700) at pthread_create.c:333
#9  0x00007f66a4ea151d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:109

第6帧 #6  0x0000000000400a59 in free_glb_resource (fmt=0x400ef0 <__FUNCTION__.4467> "_handle1") at coredump.c:69 即程序崩溃的位置,在free_glb_resource 函数中

下面看一下显示问号的情况:

(gdb) bt
#0  0x00007fb29e01c438 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1  0x00007fb29e01e197 in __GI_abort () at abort.c:118
#2  0x00007fb29e05e7fa in __libc_message (do_abort=do_abort@entry=2, fmt=fmt@entry=0x7fb29e177fd8 "*** Error in `%s': %s: 0x%s ***\n")
    at ../sysdeps/posix/libc_fatal.c:175
#3  0x00007fb29e06738a in malloc_printerr (ar_ptr=, ptr=,
    str=0x7fb29e1780a0 "double free or corruption (fasttop)", action=3) at malloc.c:5020
#4  _int_free (av=, p=, have_lock=0) at malloc.c:3874
#5  0x00007fb29e06b58c in __GI___libc_free (mem=) at malloc.c:2975
#6  0x0000000000400a59 in ?? ()
#7  0x0000000000400b99 in ?? ()
#8  0x00007fb29e3b86ba in start_thread (arg=0x7fb29dfe6700) at pthread_create.c:333
#9  0x00007fb29e0ee51d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:109

同样第6帧 #6  0x0000000000400a59 in ?? ()

地址相同,都是400a59,但没有符号显示

为什么会显示问号?

分析此问题之前,我们先看一下程序结构,linux平台下,可执行文件即elf文件,借用一张图描述,如下:

Linux程序coredump地址显示问号的调试方法 - 基于map文件_第1张图片

 注意其中的.symtab段,即符号表,存放着我们程序执行时所需要的变量及函数

通过readelf -s 可以查看内容,如下:

Symbol table '.symtab' contains 101 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name

 ...
    42: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS coredump.c
    ...
    64: 00000000006020a0     4 OBJECT  GLOBAL DEFAULT   26 glb_var
    65: 0000000000400946    28 FUNC    GLOBAL DEFAULT   14 crash_div0
    66: 0000000000602094     0 NOTYPE  GLOBAL DEFAULT   25 _edata
    67: 0000000000400c68    54 FUNC    GLOBAL DEFAULT   14 _sig_handle
    68: 00000000004009a5    25 FUNC    GLOBAL DEFAULT   14 crash_rwnull
    69: 0000000000400e74     0 FUNC    GLOBAL DEFAULT   15 _fini
    70: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND backtrace@@GLIBC_2.2.5
    71: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@@GLIBC_2
    72: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@@GLIBC_2.2.5
    73: 0000000000400a5c   263 FUNC    GLOBAL DEFAULT   14 dump_stack
    74: 0000000000400ba0   105 FUNC    GLOBAL DEFAULT   14 _handle2
    75: 00000000006020a8     8 OBJECT  GLOBAL DEFAULT   26 glb_str
    76: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_
    77: 0000000000602080     0 NOTYPE  GLOBAL DEFAULT   25 __data_start
    78: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND signal@@GLIBC_2.2.5
    79: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    80: 0000000000602088     0 OBJECT  GLOBAL HIDDEN    25 __dso_handle
    81: 0000000000400e80     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used
    82: 00000000004009be    39 FUNC    GLOBAL DEFAULT   14 fun1
    83: 00000000004009ff    26 FUNC    GLOBAL DEFAULT   14 fun3
    84: 0000000000400e00   101 FUNC    GLOBAL DEFAULT   14 __libc_csu_init
    85: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND malloc@@GLIBC_2.2.5
    86: 0000000000400962    67 FUNC    GLOBAL DEFAULT   14 crash_doublefree
    87: 00000000006020b0     0 NOTYPE  GLOBAL DEFAULT   26 _end
    88: 0000000000400850    42 FUNC    GLOBAL DEFAULT   14 _start
    89: 0000000000602094     0 NOTYPE  GLOBAL DEFAULT   26 __bss_start
    90: 0000000000400c9e   354 FUNC    GLOBAL DEFAULT   14 main
    91: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
    92: 0000000000602090     4 OBJECT  GLOBAL DEFAULT   25 glb_var_init
    93: 0000000000400c09    95 FUNC    GLOBAL DEFAULT   14 _handle3
    94: 0000000000400b63    61 FUNC    GLOBAL DEFAULT   14 _handle1
    95: 0000000000602098     0 OBJECT  GLOBAL HIDDEN    25 __TMC_END__
    96: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
    97: 0000000000400a19    67 FUNC    GLOBAL DEFAULT   14 free_glb_resource
    98: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND sleep@@GLIBC_2.2.5
    99: 0000000000400740     0 FUNC    GLOBAL DEFAULT   11 _init
   100: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND usleep@@GLIBC_2.2.5

 此段会在程序加载时,被加载到内存中,在程序崩溃时,coredump即从此段读取相应的符号并记录到core文件中

再回到之前的问题,为什么显示问号?从.symtab段即可分析出,这个段为空时即没有任何的符号记录时,coredump就得不到相关符号,因此显示问号

那么为什么.symtab段会为空?

首先我们可以考虑一下我们为什么需要.symtab段?这个段只记录相关的符号,没有它,也不对程序运行产生影响。那么它的作用在哪里?它其实就是-g编译时才被加入的,目的就是为了调试用,但有个缺点,就是占空间,我们可以看一下带.symtab和不带的elf文件大小:

10504 4月  22 08:40 testcore*
18344 4月  22 08:40 testcore-smb*

增加了80%的空间,所以我们正常软件量产发布时,都不带此符号表,在程序编译的最后都会加上strip命令,即去掉此符号表

除了体积增加,其实还有个问题,就是不安全,采取反编译手段,通过此符号段基本上就能还原我们的源代码,这么看,软件release时必须得干掉

那既然必须得干掉,而我们程序又出了问题,怎么办?coredump有是有,但是只记录地址,符号都显示问号,我们怎么才能找到对应的函数?

解决办法就是配合MAP文件

MAP文件即记录程序的各种段起始和大小,以及各种变量,函数符号的编译地址信息等的文件

编译时加上-Wl,-Map,coredump.map 或者 链接时添加-Map coredump.map 可生成MAP文件

看下其主要内容:

 *(.text.exit .text.exit.*)
 *(.text.startup .text.startup.*)
 *(.text.hot .text.hot.*)
 *(.text .stub .text.* .gnu.linkonce.t.*)
 .text          0x0000000000400850       0x2a /usr/lib/gcc/x86_64-linux-gnu/6/../../../x86_64-linux-gnu/crt1.o
                0x0000000000400850                _start
 .text          0x000000000040087a        0x0 /usr/lib/gcc/x86_64-linux-gnu/6/../../../x86_64-linux-gnu/crti.o
 *fill*         0x000000000040087a        0x6
 .text          0x0000000000400880       0xc6 /usr/lib/gcc/x86_64-linux-gnu/6/crtbegin.o
 .text          0x0000000000400946      0x4ba /tmp/ccm3K1rD.o
                0x0000000000400946                crash_div0
                0x0000000000400962                crash_doublefree
                0x00000000004009a5                crash_rwnull
                0x00000000004009be                fun1
                0x00000000004009e5                fun2
                0x00000000004009ff                fun3
                0x0000000000400a19                free_glb_resource
                0x0000000000400a5c                dump_stack
                0x0000000000400b63                _handle1
                0x0000000000400ba0                _handle2
                0x0000000000400c09                _handle3
                0x0000000000400c68                _sig_handle
                0x0000000000400c9e                main
 .text          0x0000000000400e00       0x72 /usr/lib/x86_64-linux-gnu/libc_nonshared.a(elf-init.oS)
                0x0000000000400e00                __libc_csu_init
                0x0000000000400e70                __libc_csu_fini
 .text          0x0000000000400e72        0x0 /usr/lib/gcc/x86_64-linux-gnu/6/crtend.o
 .text          0x0000000000400e72        0x0 /usr/lib/gcc/x86_64-linux-gnu/6/../../../x86_64-linux-gnu/crtn.o
 *(.gnu.warning)

 此时我们注意两个地址,崩溃时的地址#6  0x0000000000400a59 in ?? ()

和MAP文件符号地址 0x0000000000400a19                free_glb_resource

这2个就是同一个崩溃地方,那么怎么才能联系起来?

我们可以在gdb bt中继续查看,既然崩溃在400a59,先看看其前后的汇编指令,通过x命令打印:

(gdb) x/40i 0x0000000000400a59-0x50
   0x400a09:    rex.RB clc
   0x400a0b:    movl   $0x21,(%rax)
   0x400a11:    mov    -0x8(%rbp),%rax
   0x400a15:    mov    (%rax),%eax
   0x400a17:    pop    %rbp
   0x400a18:    retq   
   0x400a19:    push   %rbp
   0x400a1a:    mov    %rsp,%rbp
   0x400a1d:    sub    $0x20,%rsp
   0x400a21:    mov    %rdi,-0x18(%rbp)
   0x400a25:    movl   $0x0,-0x4(%rbp)
   0x400a2c:    mov    -0x18(%rbp),%rax
   0x400a30:    mov    %rax,%rsi
   0x400a33:    mov    $0x400e88,%edi
   0x400a38:    mov    $0x0,%eax
   0x400a3d:    callq  0x4007e0
   0x400a42:    addl   $0x1,-0x4(%rbp)
   0x400a46:    subl   $0x1,-0x4(%rbp)
   0x400a4a:    mov    0x201657(%rip),%rax        # 0x6020a8
   0x400a51:    mov    %rax,%rdi
   0x400a54:    callq  0x400770
=> 0x400a59:    nop
   0x400a5a:    leaveq
   0x400a5b:    retq   
   0x400a5c:    push   %rbp
   0x400a5d:    mov    %rsp,%rbp
   0x400a60:    sub    $0x360,%rsp

与400a59最接近的push %rbp指令即函数入口,一般情况下此pc地址与MAP文件地址是不相同的,因为一个是动态运行地址,一个是静态编译地址,但在我这pc测试,两个地址是一样的,暂且先不管它,就算不一样,我们找到加载地址,通过偏移也可以定位到map地址,就是多加了一个运算。

与400a59最接近的push %rbp指令(对于x86平台来说是rbp寄存器)即为400a19,对于map文件为0x0000000000400a19                free_glb_resource,即free_glb_resource中崩溃

通过此种方式,个人认为是一种最合适的方法,既不对外暴露太多符号,也不会增加程序体积,同时也不需要依赖太多的编译选项

最后再思考几个问题:

Q1: 既然定位到了free_glb_resource函数中崩溃,但是程序里有很多地方都调用了此函数,那到底是哪一个调用后崩溃,这又怎么定位?

A1: 看上级崩溃地址,或者当前位置的rbp内容(rbp存放着调用者信息),再查看位置前后汇编定位push %rbp即可

Q2:对于嵌入式环境,没有gdb的情况,该怎么办?

A2:通过backtrace函数,打印调用栈信息,得到崩溃地址,再配合objdump -d exename反汇编。 但是它有几个依赖,-O0和不优化栈指针 with no -fomit-frame-pointer以及没有尾调用优化

Q3:strip可以去掉symtab符号表,那还有没有办法可以保留符号表?

A3:使用动态符号表dynsym,它不会被strip掉,也不会被加载,主要供dlopen,backtrace等函数使用,但是缺乏安全性,会对外暴露太多符号信息

Q4:如果是在动态库中崩溃,怎么定位?

A4:通过程序本身的内存映射文件/proc/pid/maps,maps文件可以查看某个进程的代码段、栈区、堆区、动态库、内核区对应的虚拟地址

你可能感兴趣的:(Linux,coredump,backtrace,程序调用栈,gdb,MAP文件)