相关概念
Core
在使用半导体作为内存的材料前,人类是利用线圈当作内存的材料(发明者为王安),线圈就叫作 core
,用线圈做的内存就叫作 core memory
。如今,半导体工业澎勃发展,已经没有人用core memory
了,不过,在许多情况下,人们还是把记忆体叫作 core
。
Core dump
我们在开发(或使用)一个程序时,最怕的就是程序莫明其妙地宕掉。虽然系统没事,但我们下次仍可能遇到相同的问题。于是这时操作系统就会把程序宕掉时的内存内容 dump
出来(现在通常是写在一个叫 core
的 file
里面),让我们做为参考。这个动作就叫作 core dump
。
如何获取 Core 文件
1、在一些 Linux 版本下,默认是不产生core
文件的,首先可以查看一下系统core
文件的大小限制:
$:~/segfault$ ulimit -c
0
2、可以看到默认设置情况下,本机 Linux 环境下发生段错误时不会自动生成core
文件,下面设置下core
文件的大小限制(单位为 KB):
$:~/segfault$ ulimit -c 1024
$:~/segfault$ ulimit -c
1024
3、重新运行程序,如果发生段错误,就会生成core
文件。
出现段错误的可能原因
访问不存在的内存地址
#include<stdio.h>
#include<stdlib.h>
void main()
{
int *ptr = NULL;
*ptr = 0;
}
访问系统保护的内存地址
#include<stdio.h>
#include<stdlib.h>
void main()
{
int *ptr = (int *)0;
*ptr = 100;
}
访问只读的内存地址
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void main()
{
char *ptr = "test";
strcpy(ptr, "TEST");
}
栈溢出
#include<stdio.h>
#include<stdlib.h>
void main()
{
main();
}
段错误信息获取
程序发生段错误时,提示信息很少,下面有几种查看段错误的发生信息的途径。
dmesg
dmesg 可以在应用程序 crash 掉时,显示内核中保存的相关信息。如下所示,通过dmesg
命令可以查看发生段错误的程序名称、引起段错误发生的内存地址、指令指针地址、堆栈指针地址、错误代码、错误原因等。
$:~/segfault$ dmesg
[ 2329.479037] segfault3[2700]: segfault at 80484e0 ip 00d2906a sp bfbbec3c error 7 in libc-2.10.1.so[cb4000+13e000]
-g
使用 gcc 编译程序的源码时,加上-g
参数,这样可以使得生成的二进制文件中加入可以用于 gdb 调试的有用信息。
$:~/segfault$ gcc -g -o segfault3 segfault3.c
nm
使用 nm 命令列出二进制文件中的符号表,包括符号地址、符号类型、符号名等,这样可以帮助定位在哪里发生了段错误。
panfeng@ubuntu:~/segfault$ nm segfault3
08049f20 d _DYNAMIC
08049ff4 d _GLOBAL_OFFSET_TABLE_
080484dc R _IO_stdin_used
w _Jv_RegisterClasses
08049f10 d __CTOR_END__
08049f0c d __CTOR_LIST__
08049f18 D __DTOR_END__
08049f14 d __DTOR_LIST__
080484ec r __FRAME_END__
08049f1c d __JCR_END__
08049f1c d __JCR_LIST__
0804a014 A __bss_start
0804a00c D __data_start
08048490 t __do_global_ctors_aux
08048360 t __do_global_dtors_aux
0804a010 D __dso_handle
w __gmon_start__
0804848a T __i686.get_pc_thunk.bx
08049f0c d __init_array_end
08049f0c d __init_array_start
08048420 T __libc_csu_fini
08048430 T __libc_csu_init
U __libc_start_main@@GLIBC_2.0
0804a014 A _edata
0804a01c A _end
080484bc T _fini
080484d8 R _fp_hw
080482bc T _init
08048330 T _start
0804a014 b completed.6990
0804a00c W data_start
0804a018 b dtor_idx.6992
080483c0 t frame_dummy
080483e4 T main
U memcpy@@GLIBC_2.0
ldd
使用 ldd 命令查看二进制程序的共享链接库依赖,包括库的名称、起始地址,这样可以确定段错误到底是发生在了自己的程序中还是依赖的共享库中。
$:~/segfault$ ldd ./segfault3
linux-gate.so.1 => (0x00e08000)
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0x00675000)
/lib/ld-linux.so.2 (0x00482000)
调试方法和技巧
使用 gcc 和 gdb
调试流程
- 为了能够使用 gdb 调试程序,在编译阶段加上-g 参数,
$:~/segfault$ gcc -g -o segfault3 segfault3.c
- 使用 gdb 命令调试程序:
$:~/segfault$ gdb -q ./segfault3
Reading symbols from ./segfault3...done.
(gdb)
- 进入 gdb 后,运行程序:
(gdb) run
Starting program: ./segfault3
Program received signal SIGSEGV, Segmentation fault.
0x001a306a 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 文件
$:~/segfault$ gdb ./segfault3 ./core
Reading symbols from /home/panfeng/segfault/segfault3...done.
warning: Can't read pathname for load map: 输入/输出错误.
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 0x0018506a in memcpy () from /lib/tls/i686/cmov/libc.6
从输出看出,同上节中一样的段错误信息。
适用场景
适合于在实际生成环境下调试程序的段错误(即在不用重新发生段错误的情况下重现段错误)。
当程序很复杂,core 文件相当大时,该方法不可用。
使用 objdump
调试流程
- 使用 dmesg 命令,找到最近发生的段错误输出信息:
$:~/segfault$ dmesg
... ...
[17257.502808] segfault3[3320]: segfault at 80484e0 ip 0018506a sp bfc1cd6c error 7 in libc-2.10.1.so[110000+13e000]
其中,对我们接下来的调试过程有用的是发生段错误的地址:80484e0
和指令指针地址:0018506a
。
- 使用
objdump
生成二进制的相关信息,重定向到文件中:
$:~/segfault$ objdump -d ./segfault3 > segfault3Dump
其中,生成的segfault3Dump
文件中包含了二进制文件的segfault3
的汇编代码。
- 在
segfault3Dump
文件中查找发生段错误的地址:
panfeng@ubuntu:~/segfault$ grep -n -A 10 -B 10 "80484e0" ./segfault3Dump
121- 80483df: ff d0 call *%eax
122- 80483e1: c9 leave
123- 80483e2: c3 ret
124- 80483e3: 90 nop
125-
126-080483e4 <main>:
127- 80483e4: 55 push %ebp
128- 80483e5: 89 e5 mov %esp,%ebp
129- 80483e7: 83 e4 f0 and $0xfffffff0,%esp
130- 80483ea: 83 ec 20 sub $0x20,%esp
131: 80483ed: c7 44 24 1c e0 84 04 movl $0x80484e0,0x1c(%esp)
132- 80483f4: 08
133- 80483f5: b8 e5 84 04 08 mov $0x80484e5,%eax
134- 80483fa: c7 44 24 08 05 00 00 movl $0x5,0x8(%esp)
135- 8048401: 00
136- 8048402: 89 44 24 04 mov %eax,0x4(%esp)
137- 8048406: 8b 44 24 1c mov 0x1c(%esp),%eax
138- 804840a: 89 04 24 mov %eax,(%esp)
139- 804840d: e8 0a ff ff ff call 804831c <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)加载上,用于捕捉断错误的出错信息。
$:~/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.so
00273000-00274000 r--p 0001a000 08:01 157 /lib/ld-2.10.1.so
00274000-00275000 rw-p 0001b000 08:01 157 /lib/ld-2.10.1.so
003b4000-003b7000 r-xp 00000000 08:01 13105 /lib/libSegFault.so
003b7000-003b8000 r--p 00002000 08:01 13105 /lib/libSegFault.so
003b8000-003b9000 rw-p 00003000 08:01 13105 /lib/libSegFault.so
00c76000-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.so
00fb1000-00fb2000 ---p 0013e000 08:01 1800 /lib/tls/i686/cmov/libc-2.10.1.so
00fb2000-00fb4000 r--p 0013e000 08:01 1800 /lib/tls/i686/cmov/libc-2.10.1.so
00fb4000-00fb5000 rw-p 00140000 08:01 1800 /lib/tls/i686/cmov/libc-2.10.1.so
00fb5000-00fb8000 rw-p 00000000 00:00 0
08048000-08049000 r-xp 00000000 08:01 303895 /home/segfault/segfault3
08049000-0804a000 r--p 00000000 08:01 303895 /home/segfault/segfault3
0804a000-0804b000 rw-p 00001000 08:01 303895 /home/segfault/segfault3
09432000-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。
在使用数组时,注意数组是否被初始化,数组下标是否越界,数组元素是否存在等。
在访问变量时,注意变量所占地址空间是否已经被程序释放掉。
在处理变量时,注意变量的格式控制是否合理等。