本文最后更新于:5 个月前
相关概念 Core 在使用半导体作为内存的材料前,人类是利用线圈当作内存的材料(发明者为王安),线圈就叫作 core
,用线圈做的内存就叫作 core memory
。如今 ,半导体工业澎勃发展,已经没有人用core memory
了,不过,在许多情况下,人们还是把记忆体叫作 core
。
Core dump 我们在开发(或使用)一个程序时,最怕的就是程序莫明其妙地宕掉。虽然系统没事,但我们下次仍可能遇到相同的问题。于是这时操作系统就会把程序宕掉时的内存内容 dump
出来(现在通常是写在一个叫 core
的 file
里面),让我们做为参考。这个动作就叫作 core dump
。
如何获取Core文件 1、在一些Linux版本下,默认是不产生core
文件的,首先可以查看一下系统core
文件的大小限制:
1 2 $ :~/segfault$ ulimit -c0
2、可以看到默认设置情况下,本机Linux环境下发生段错误时不会自动生成core
文件,下面设置下core
文件的大小限制(单位为KB):
1 2 3 $ :~/segfault$ ulimit -c 1024 $ :~/segfault$ ulimit -c1024
3、重新运行程序,如果发生段错误,就会生成core
文件。
出现段错误的可能原因 访问不存在的内存地址 1 2 3 4 5 6 7 #include <stdio.h> #include <stdlib.h> void main () { int *ptr = NULL ; *ptr = 0 ; }
访问系统保护的内存地址 1 2 3 4 5 6 7 #include <stdio.h> #include <stdlib.h> void main () { int *ptr = (int *)0 ; *ptr = 100 ; }
访问只读的内存地址 1 2 3 4 5 6 7 8 #include <stdio.h> #include <stdlib.h> #include <string.h> void main () { char *ptr = "test" ; strcpy (ptr, "TEST" ); }
栈溢出 1 2 3 4 5 6 #include <stdio.h> #include <stdlib.h> void main () { main(); }
段错误信息获取 程序发生段错误时,提示信息很少,下面有几种查看段错误的发生信息的途径。
dmesg dmesg可以在应用程序crash掉时,显示内核中保存的相关信息。如下所示,通过dmesg
命令可以查看发生段错误的程序名称、引起段错误发生的内存地址、指令指针地址、堆栈指针地址、错误代码、错误原因等。
1 2 $:~/segfault$ dmesg[ 2329.479037] segfault3[2700] : segfault at 80484 e0 ip 00 d2906a sp bfbbec3c error 7 in libc-2.10 .1 .so [cb4000+13e000]
-g 使用gcc编译程序的源码时,加上-g
参数,这样可以使得生成的二进制文件中加入可以用于gdb调试的有用信息。
1 2 $ :~/segfault$ gcc -g -o segfault3 segfault3.c
nm 使用nm命令列出二进制文件中的符号表,包括符号地址、符号类型、符号名等,这样可以帮助定位在哪里发生了段错误。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 panfeng@ubuntu:~/segfault$ nm segfault308049 f20 d _DYNAMIC 08049 ff4 d _GLOBAL_OFFSET_TABLE_ 080484 dc R _IO_stdin_used w _Jv_RegisterClasses 08049 f10 d __CTOR_END__ 08049 f0c d __CTOR_LIST__ 08049 f18 D __DTOR_END__ 08049 f14 d __DTOR_LIST__ 080484 ec r __FRAME_END__ 08049 f1c d __JCR_END__ 08049 f1c d __JCR_LIST__ 0804 a014 A __bss_start 0804 a00c D __data_start 08048490 t __do_global_ctors_aux 08048360 t __do_global_dtors_aux 0804 a010 D __dso_handle w __gmon_start__ 0804848 a T __i686 .get_pc_thunk.bx08049 f0c d __init_array_end 08049 f0c d __init_array_start 08048420 T __libc_csu_fini 08048430 T __libc_csu_init U __libc_start_main @@GLIBC_2.0 0804 a014 A _edata 0804 a01c A _end 080484 bc T _fini 080484 d8 R _fp_hw 080482 bc T _init 08048330 T _start 0804 a014 b completed.6990 0804 a00c W data_start0804 a018 b dtor_idx.6992 080483 c0 t frame_dummy080483 e4 T main U memcpy@@GLIBC_2.0
ldd 使用ldd命令查看二进制程序的共享链接库依赖,包括库的名称、起始地址,这样可以确定段错误到底是发生在了自己的程序中还是依赖的共享库中。
1 2 3 4 $:~/segfault$ ldd ./ segfault3 linux-gate.so.1 => (0 x00e08000) libc.so.6 => /lib/ tls/i686/ cmov/libc.so.6 (0 x00675000) /lib/ ld-linux.so.2 (0 x00482000)
调试方法和技巧 使用gcc和gdb 调试流程
为了能够使用gdb调试程序,在编译阶段加上-g参数,
1 $ :~/segfault$ gcc -g -o segfault3 segfault3.c
使用gdb命令调试程序:
1 2 3 $:~/segfault$ gdb -q ./ segfault3 Reading symbols from ./segfault3...done. (gdb)
进入gdb后,运行程序:
1 2 3 4 5 6 (gdb) run Starting program: ./segfault3 Program received signal SIGSEGV, Segmentation fault.0 x001a306a in memcpy () from /lib/ tls/i686/ cmov/libc.so.6 (gdb)
从输出看出,程序收到SIGSEGV
信号,触发段错误,并提示地址0x001a306a
、调用memcpy报的错,位于/lib/tls/i686/cmov/libc.so.6
库中。
完成调试后,输入quit
命令退出gdb:
适用场景
仅当能确定程序一定会发生段错误的情况下使用。
当程序的源码可以获得的情况下,使用-g
参数编译程序。
一般用于测试阶段,生产环境下gdb会有副作用:使程序运行减慢,运行不够稳定,等等。
即使在测试阶段,如果程序过于复杂,gdb也不能处理。
使用core文件和gdb 在上节中提到段错误会触发SIGSEGV
信号,通过man 7 signal
,可以看到SIGSEGV
默认的handler
会打印段错误出错信息,并产生core
文件,由此我们可以借助于程序异常退出时生成的core
文件中的调试信息,使用gdb工具来调试程序中的段错误。
调试流程
运行有段错误的程序,生成core文件。
gdb加载core文件1 2 3 4 5 6 7 8 9 10 11 $:~/segfault$ gdb ./segfault3 ./core Reading symbols from /home/panfeng/segfault/segfault3...done.warning: Can Reading symbols from /lib /tls/i686/cmov/libc.so.6 ...(no debugging symbols found)...done. Loaded symbols for /lib /tls/i686/cmov/libc.so.6 Reading symbols from /lib /ld-linux.so.2 ...(no debugging symbols found)...done. Loaded symbols for /lib /ld-linux.so.2 Core was generated by `./segfault3 Program terminated with signal 11 , Segmentation fault. #0 0 x0018506a in memcpy () from /lib /tls/i686/cmov/libc.6
从输出看出,同上节中一样的段错误信息。
适用场景
适合于在实际生成环境下调试程序的段错误(即在不用重新发生段错误的情况下重现段错误)。
当程序很复杂,core文件相当大时,该方法不可用。
使用objdump 调试流程
使用dmesg命令,找到最近发生的段错误输出信息:
1 2 3 $:~/segfault $ dmesg... ... [17257.502808] segfault3[3320]: segfault at 80484e0 ip 0018506a sp bfc1cd 6c error 7 in libc-2.10.1.so[110000+13e000]
其中,对我们接下来的调试过程有用的是发生段错误的地址:80484e0
和指令指针地址:0018506a
。
使用objdump
生成二进制的相关信息,重定向到文件中:
1 $:~/segfault$ objdump -d ./ segfault3 > segfault3Dump
其中,生成的segfault3Dump
文件中包含了二进制文件的segfault3
的汇编代码。
在segfault3Dump
文件中查找发生段错误的地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 panfeng @ubuntu:~/segfault$ grep -n -A 10 -B 10 "80484e0" ./segfault3 Dump 121 - 80483 df: ff d0 call *%eax122 - 80483 e1 : c9 leave 123 - 80483 e2 : c3 ret 124 - 80483 e3 : 90 nop125 -126 -080483 e4 <main>:127 - 80483 e4 : 55 push %ebp128 - 80483 e5 : 89 e5 mov %esp,%ebp129 - 80483 e7 : 83 e4 f0 and $0 xfffffff0 ,%esp130 - 80483 ea: 83 ec 20 sub $0 x20 ,%esp131 : 80483 ed: c7 44 24 1 c e0 84 04 movl $0 x80484 e0 ,0 x1 c(%esp)132 - 80483 f4 : 08 133 - 80483 f5 : b8 e5 84 04 08 mov $0 x80484 e5 ,%eax134 - 80483 fa: c7 44 24 08 05 00 00 movl $0 x5 ,0 x8 (%esp)135 - 8048401 : 00 136 - 8048402 : 89 44 24 04 mov %eax,0 x4 (%esp)137 - 8048406 : 8 b 44 24 1 c mov 0 x1 c(%esp),%eax138 - 804840 a: 89 04 24 mov %eax,(%esp)139 - 804840 d: e8 0 a ff ff ff call 804831 c <memcpy@plt>140 - 8048412 : c9 leave 141 - 8048413 : c3 ret
通过对以上汇编代码分析,得知段错误发生main
函数,对应的汇编指令是movl $0x80484e0,0x1c(%esp)
,接下来打开程序的源码,找到汇编指令对应的源码,也就定位到段错误了。
适用场景
不需要-g
参数编译,不需要借助于core
文件,但需要有一定的汇编语言基础。
2、如果使用了gcc编译优化参数(-O1,-O2,-O3)的话,生成的汇编指令将会被优化,使得调试过程有些难度。
使用catchsegv catchsegv
命令专门用来扑获段错误,它通过动态加载器(ld-linux.so)的预加载机制(PRELOAD)把一个事先写好的库(/lib/libSegFault.so)加载上,用于捕捉断错误的出错信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 $:~/segfault$ catchsegv ./segfault3 Segmentation fault (core dumped) *** Segmentation fault Register dump: EAX: 00000000 EBX: 00fb3ff4 ECX: 00000002 EDX: 00000000 ESI: 080484e5 EDI: 080484e0 EBP: bfb7ad38 ESP: bfb7ad0c EIP: 00ee806a EFLAGS: 00010203 CS: 0073 DS: 007b ES: 007b FS: 0000 GS: 0033 SS: 007b Trap: 0000000e Error: 00000007 OldMask: 00000000 ESP/signal: bfb7ad0c CR2: 080484e0 Backtrace: /lib/libSegFault.so[0x3b606f ] ??:0 (??)[0xc76400 ] /lib/tls/i686/cmov/libc.so.6 (__libc_start_main+0xe6 )[0xe89b56 ] /build/buildd/eglibc-2.10 .1 /csu/../sysdeps/i386/elf/start.S:122 (_start)[0x8048351 ] Memory map :00258000 -00273000 r-xp 00000000 08 :01 157 /lib/ld-2.10 .1 .so00273000 -00274000 r--p 0001a000 08 :01 157 /lib/ld-2.10 .1 .so00274000 -00275000 rw-p 0001b000 08 :01 157 /lib/ld-2.10 .1 .so003b4000 -003b7000 r-xp 00000000 08 :01 13105 /lib/libSegFault.so003b7000 -003b8000 r--p 00002000 08 :01 13105 /lib/libSegFault.so003b8000 -003b9000 rw-p 00003000 08 :01 13105 /lib/libSegFault.so00c76000 -00c77000 r-xp 00000000 00 :00 0 [vdso]00e0d000 -00e29000 r-xp 00000000 08 :01 4817 /lib/libgcc_s.so.1 00e29000 -00e2a000 r--p 0001b000 08 :01 4817 /lib/libgcc_s.so.1 00e2a000 -00e2b000 rw-p 0001c000 08 :01 4817 /lib/libgcc_s.so.1 00e73000 -00fb1000 r-xp 00000000 08 :01 1800 /lib/tls/i686/cmov/libc-2.10 .1 .so00fb1000 -00fb2000 ---p 0013e000 08 :01 1800 /lib/tls/i686/cmov/libc-2.10 .1 .so00fb2000 -00fb4000 r--p 0013e000 08 :01 1800 /lib/tls/i686/cmov/libc-2.10 .1 .so00fb4000 -00fb5000 rw-p 00140000 08 :01 1800 /lib/tls/i686/cmov/libc-2.10 .1 .so00fb5000 -00fb8000 rw-p 00000000 00 :00 0 08048000 -08049000 r-xp 00000000 08 :01 303895 /home/segfault/segfault308049000 -0804a000 r--p 00000000 08 :01 303895 /home/segfault/segfault30804a000 -0804b000 rw-p 00001000 08 :01 303895 /home/segfault/segfault309432000 -09457000 rw-p 00000000 00 :00 0 [heap] b78cf000-b78d1000 rw-p 00000000 00 :00 0 b78df000-b78e1000 rw-p 00000000 00 :00 0 bfb67000-bfb7c000 rw-p 00000000 00 :00 0 [stack]
如何避免段错误
出现段错误时,首先应该想到段错误的定义,从它出发考虑引发错误的原因。
在使用指针时,定义了指针后记得初始化指针,在使用的时候记得判断是否为NULL。
在使用数组时,注意数组是否被初始化,数组下标是否越界,数组元素是否存在等。
在访问变量时,注意变量所占地址空间是否已经被程序释放掉。
在处理变量时,注意变量的格式控制是否合理等。
Reference