coredump即Linux系统上,应用程序崩溃时的运行栈快照,已便于定位崩溃问题
设置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.
--Typefor 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文件,借用一张图描述,如下:
注意其中的.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段?这个段只记录相关的符号,没有它,也不对程序运行产生影响。那么它的作用在哪里?它其实就是-g编译时才被加入的,目的就是为了调试用,但有个缺点,就是占空间,我们可以看一下带.symtab和不带的elf文件大小:
10504 4月 22 08:40 testcore*
18344 4月 22 08:40 testcore-smb*
增加了80%的空间,所以我们正常软件量产发布时,都不带此符号表,在程序编译的最后都会加上strip命令,即去掉此符号表
除了体积增加,其实还有个问题,就是不安全,采取反编译手段,通过此符号段基本上就能还原我们的源代码,这么看,软件release时必须得干掉
那既然必须得干掉,而我们程序又出了问题,怎么办?coredump有是有,但是只记录地址,符号都显示问号,我们怎么才能找到对应的函数?
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
我们可以在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中崩溃
通过此种方式,个人认为是一种最合适的方法,既不对外暴露太多符号,也不会增加程序体积,同时也不需要依赖太多的编译选项
A1: 看上级崩溃地址,或者当前位置的rbp内容(rbp存放着调用者信息),再查看位置前后汇编定位push %rbp即可
A2:通过backtrace函数,打印调用栈信息,得到崩溃地址,再配合objdump -d exename反汇编。 但是它有几个依赖,-O0和不优化栈指针 with no -fomit-frame-pointer以及没有尾调用优化
A3:使用动态符号表dynsym,它不会被strip掉,也不会被加载,主要供dlopen,backtrace等函数使用,但是缺乏安全性,会对外暴露太多符号信息
A4:通过程序本身的内存映射文件/proc/pid/maps,maps文件可以查看某个进程的代码段、栈区、堆区、动态库、内核区对应的虚拟地址