一、什么是段错误?
一旦一个程序发生了越界访问,cpu 就会产生相应的保护,于是 segmentation fault 就出现了,通过上面的解释,段错误应该就是访问了不可访问的内存,这个内存区要么是不存在的,要么是受到系统保护的,还有可能是缺少文件或者文件损坏。
二、段错误产生的原因
下面是一些典型的段错误的原因:
非关联化空指针——这是特殊情况由内存管理硬件
试图访问一个不存在的内存地址(在进程的地址空间)
试图访问内存的程序没有权利(如内核结构流程上下文)
试图写入只读存储器(如代码段)
1、访问不存在的内存地址
在C代码,分割错误通常发生由于指针的错误使用,特别是在C动态内存分配。非关联化一个空指针总是导致段错误,但野指针和悬空指针指向的内存,可能会或可能不会存在,而且可能或不可能是可读的还是可写的,因此会导致瞬态错误。
- #include <stdio.h>
-
- int main (void)
- {
- int *ptr = NULL;
- *ptr = 0;
- return 0;
- }
- 输出结果:
- 段错误(核心已转储)
现在,非关联化这些变量可能导致段错误:非关联化空指针通常会导致段错误,阅读时从野指针可能导致随机数据但没有段错误,和阅读从悬空指针可能导致有效数据,然后随机数据覆盖。
2、访问系统保护的内存地址
- #include <stdio.h>
-
- int main (void)
- {
- int *ptr = (int *)0;
- *ptr = 100;
- return 0;
- }
- 输出结果:
- 段错误(核心已转储)
3、访问只读的内存地址
写入只读存储器提出了一个 segmentation fault,这个发生在程序写入自己的一部分代码段或者是只读的数据段,这些都是由操作系统加载到只读存储器。
- #include <stdio.h>
- #include <string.h>
-
- int main (void)
- {
- char *ptr = "test";
- strcpy (ptr, "TEST");
- return 0;
- }
- 输出结果:
- 段错误(核心已转储)
- #include <stdio.h>
-
- int main (void)
- {
- char *ptr = "hello";
- *ptr = 'H';
- return 0;
- }
- 输出结果:
- 段错误(核心已转储)
上述例子ANSI C代码通常会导致段错误和内存保护平台。它试图修改一个字符串文字,这是根据ANSI C标准未定义的行为。大多数编译器在编译时不会抓,而是编译这个可执行代码,将崩溃。
包含这个代码被编译程序时,字符串“hello”位于rodata部分程序的可执行文件的只读部分数据段。当加载时,操作系统与其他字符串和地方常数只读段的内存中的数据。当执行时,一个变量 ptr 设置为指向字符串的位置,并试图编写一个H字符通过变量进入内存,导致段错误。编译程序的编译器不检查作业的只读的位置在编译时,和运行类unix操作系统产生以下运行时发生 segmentation fault。
可以纠正这个代码使用一个数组而不是一个字符指针,这个栈上分配内存并初始化字符串的值:
- #include <stdio.h>
-
- int main (void)
- {
- char ptr[] = "hello";
- ptr[0] = 'H';
- return 0;
- }
即使不能修改字符串(相反,这在C标准未定义行为),在C char *类型,所以没有隐式转换原始代码,在c++的 const char *类型,因此有一个隐式转换,所以编译器通常会抓住这个特定的错误。
4、空指针废弃
因为是一个很常见的程序错误空指针废弃(读或写在一个空指针,用于C的意思是“没有对象指针”作为一个错误指示器),大多数操作系统内存访问空指针的地址,这样它会导致段错误。
- #include <stdio.h>
-
- int main (void)
- {
- int *ptr = NULL;
- printf ("%d\n", *ptr);
- return 0;
- }
- 输出结果:
- 段错误(核心已转储)
这个示例代码创建了一个空指针,然后试图访问它的值(读值)。在运行时在许多操作系统中,这样做会导致段错误。
非关联化一个空指针,然后分配(写一个值到一个不存在的目标)也通常会导致段错误。
- #include <stdio.h>
-
- int main (void)
- {
- int *ptr = NULL;
- *ptr = 1;
- return 0;
- }
- 输出结果:
- 段错误(核心已转储)
下面的代码包含一个空指针,但当编译通常不会导致段错误,值是未使用的。因此,废弃通常会被优化掉,死代码消除。
- #include <stdio.h>
-
- int main (void)
- {
- int *ptr = NULL;
- *ptr;
- return 0;
- }
还有,比如malloc 动态分配内存,释放、置空完成后,不可再使用该指针。
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
-
- int main()
- {
- char* str=(char* )malloc(100);
- if(*str)
- {
- return;
- }
- strcpy(str,"hello");
- printf("%s\n",str);
- free(str);
- str=NULL;
- strcpy(str,"abcdef");
- return 0;
- }
- 输出结果:
- hello
- 段错误 (核心已转储)<strong>
- </strong>
5、堆栈溢出
- #include <stdio.h>
- #include <string.h>
-
- int main (void)
- {
- main ();
- return 0;
- }
- 输出结果:
- 段错误(核心已转储)
上述例子的无限递归,导致的堆栈溢出会导致段错误,但无线递归未必导致堆栈溢出,优化执行的编译器和代码的确切结构。在这种情况下,遥不可及的代码(返回语句)行为是未定义的。因此,编译器可以消除它,使用尾部调用优化,可能导致没有堆栈使用。其他优化可能包括将递归转换成迭代,给出例子的结构功能永远会导致程序运行,虽然可能不是其他堆栈溢出。
6、内存越界(数组越界,变量类型不一致等)
- #include <stdio.h>
-
- int main (void)
- {
- char test[10];
- printf ("%c\n", test[100000]);
- return 0;
- }
- 输出结果:
- 段错误(核心已转储)
三、段错误信息的获取
程序发生段错误时,提示信息很少,下面有几种查看段错误的发生信息的途径。
1、dmesg
dmesg 可以在应用程序崩溃时,显示内存中保存的相关信息。如下所示,通过 dmesg 命令可以查看发生段错误的程序名称、引起段错误发生的内存地址、指令指针地址、堆栈指针地址、错误代码、错误原因等。
- root@#dmesg
- [ 6357.422282] a.out[3044]: segfault at 806851c ip b75cd668 sp bf8b2100 error 4 in libc-2.15.so[b7559000+19f000]
2、-g
使用gcc编译程序的源码时,加上 -g 参数,这样可以使得生成的二进制文件中加入可以用于 gdb 调试的有用信息。
参看:C语言再学习 -- GCC编译过程
可产生供gdb调试用的可执行文件,大小明显比只用-o选项编译汇编连接后的文件大。
- root@ubuntu:/home/tarena/project/c_test# gcc hello.c
- root@ubuntu:/home/tarena/project/c_test# ls -la a.out
- -rwxr-xr-x 1 root root 7159 Nov 26 23:32 a.out
- root@ubuntu:/home/tarena/project/c_test# gcc -g hello.c
- root@ubuntu:/home/tarena/project/c_test# ls -la a.out
- -rwxr-xr-x 1 root root 8051 Nov 26 23:32 a.out
gdb的简单使用:
(gdb)l 列表(list)
(gdb)r 执行(run)
(gdb)n 下一个(next)
(gdb)q 退出(quit)
(gdb)p 输出(print)
(gdb)c 继续(continue)
(gdb)b 4 设置断点(break)
(gdb)d 删除断点(delete)
- root@ubuntu:/home/tarena/project/c_test# gdb a.out
- GNU gdb (Ubuntu/Linaro 7.4-2012.02-0ubuntu2) 7.4-2012.02
- Copyright (C) 2012 Free Software Foundation, Inc.
- License GPLv3+: GNU GPL version 3 or later <http:
- This is free software: you are free to change and redistribute it.
- There is NO WARRANTY, to the extent permitted by law. Type "show copying"
- and "show warranty" for details.
- This GDB was configured as "i686-linux-gnu".
- For bug reporting instructions, please see:
- <http:
- Reading symbols from /home/tarena/project/c_test/a.out...done.
- (gdb) l
- 1 #include <stdio.h>
- 2 int main (void)
- 3 {
- 4 printf ("hello world!\n");
- 5 return 0;
- 6 }
- (gdb) r
- Starting program: /home/tarena/project/c_test/a.out
- hello world!
- [Inferior 1 (process 6906) exited normally]
- (gdb) q
- root@ubuntu:/home/tarena/project/c_test#
-
3、nm
使用 nm 命令列出二进制文件中符号表,包括符号地址、符号类型、符号名等。这样可以帮助定位在哪里发生了段错误。
- root@# nm a.out
- 08049f28 d _DYNAMIC
- 08049ff4 d _GLOBAL_OFFSET_TABLE_
- 080484ac R _IO_stdin_used
- w _Jv_RegisterClasses
- 08049f18 d __CTOR_END__
- 08049f14 d __CTOR_LIST__
- 08049f20 D __DTOR_END__
- 08049f1c d __DTOR_LIST__
- 080485a4 r __FRAME_END__
- 08049f24 d __JCR_END__
- 08049f24 d __JCR_LIST__
- 0804a010 A __bss_start
- 0804a008 D __data_start
- 08048460 t __do_global_ctors_aux
- 08048330 t __do_global_dtors_aux
- 0804a00c D __dso_handle
- w __gmon_start__
- 08048452 T __i686.get_pc_thunk.bx
- 08049f14 d __init_array_end
- 08049f14 d __init_array_start
- 08048450 T __libc_csu_fini
- 080483e0 T __libc_csu_init
- U __libc_start_main@@GLIBC_2.0
- 0804a010 A _edata
- 0804a018 A _end
- 0804848c T _fini
- 080484a8 R _fp_hw
- 08048294 T _init
- 08048300 T _start
- 0804a010 b completed.6159
- 0804a008 W data_start
- 0804a014 b dtor_idx.6161
- 08048390 t frame_dummy
- 080483b4 T main
4、ldd
使用 ldd 命令查看二进制程序的共享链接库依赖,包括库的名称、起始地址,这样可以确定段错误到底是发生在了自己的程序中还是依赖的共享库中。
- root@t# ldd a.out
- linux-gate.so.1 => (0xb7765000)
- libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75ac000)
- /lib/ld-linux.so.2 (0xb7766000)
四、段错误的调试方法
接下来的讲解是围绕下面的代码进行的:
- #include <stdio.h>
-
- int main (void)
- {
- int *ptr = NULL;
- *ptr = 10;
- return 0;
- }
- 输出结果:
- 段错误(核心已转储)
1、使用 printf 输出信息
这个是看似最简单,但往往很多情况下十分有效的调试方式,也许可以说是程序员用的最多的调试方式。简单来说,就是在程序的重要代码附近加上像 printf 这类输出信息,这样可以跟踪并打印出段错误在代码中可能出现的位置。
为了方便使用这种方法,可以使用条件编译指令 #define DEBUG 和 #endif 把 printf 函数包起来。这样在程序编译时,如果加上 -DDEBUG 参数就可以查看调试信息;否则不加上参数就不会显示调试信息。
参看:C语言再学习 -- C 预处理器
2、使用 gcc 和 gdb
1)调试步骤
A、为了能够使用 gdb 调试程序,在编译阶段加上 -g 参数。
B、使用 gdb 命令调试程序
- root@# gdb a.out
- GNU gdb (Ubuntu/Linaro 7.4-2012.02-0ubuntu2) 7.4-2012.02
- Copyright (C) 2012 Free Software Foundation, Inc.
- License GPLv3+: GNU GPL version 3 or later <http:
- This is free software: you are free to change and redistribute it.
- There is NO WARRANTY, to the extent permitted by law. Type "show copying"
- and "show warranty" for details.
- This GDB was configured as "i686-linux-gnu".
- For bug reporting instructions, please see:
- <http:
- Reading symbols from /home/tarena/project/c_test/a.out...done.
- (gdb)
C、进入 gdb 后,运行程序
- (gdb) r
- Starting program: /home/tarena/project/c_test/a.out
-
- Program received signal SIGSEGV, Segmentation fault.
- 0x080483c4 in main () at test.c:6
- 6 *ptr = 10;
- (gdb)
从输出看出,程序收到 SIGSEGV 信号,触发段错误,并提示地址 0x080483c4、创建了一个空指针,然后试图访问它的值(读值)。
可以通过man 7 signal查看SIGSEGV的信息
- Signal Value Action Comment
- ──────────────────────────────────────────────────────────────────────
- SIGHUP 1 Term Hangup detected on controlling terminal
- or death of controlling process
- SIGINT 2 Term Interrupt from keyboard
- SIGQUIT 3 Core Quit from keyboard
- SIGILL 4 Core Illegal Instruction
- SIGABRT 6 Core Abort signal from abort(3)
- SIGFPE 8 Core Floating point exception
- SIGKILL 9 Term Kill signal
- SIGSEGV 11 Core Invalid memory reference
- SIGPIPE 13 Term Broken pipe: write to pipe with no
- readers
- SIGALRM 14 Term Timer signal from alarm(2)
- SIGTERM 15 Term Termination signal
- SIGUSR1 30,10,16 Term User-defined signal 1
- SIGUSR2 31,12,17 Term User-defined signal 2
- SIGCHLD 20,17,18 Ign Child stopped or terminated
- SIGCONT 19,18,25 Cont Continue if stopped
- SIGSTOP 17,19,23 Stop Stop process
- SIGTSTP 18,20,24 Stop Stop typed at tty
- SIGTTIN 21,21,26 Stop tty input for background process
- SIGTTOU 22,22,27 Stop tty output for background process
-
- The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
D、完成调试后,输入 q 命令退出 gdb
- (gdb) q
- A debugging session is active.
-
- Inferior 1 [process 3483] will be killed.
-
- Quit anyway? (y or n) y
2)适用场景
A、仅当能确定程序一定会发生段错误的情况下适用。
B、当程序的源码可以获得的情况下,使用 -g 参数编译程序
C、一般用于测试阶段,生产环境下 gdb 会有副作用:使程序运行减慢,运行不够稳定,等等。
D、即使在测试阶段,如果程序过于复杂,gdb 也不能处理。
3、使用 core 文件和 gdb
上面有提到段错误触发SIGSEGV信号,通过man 7 signal,可以看到SIGSEGV默认的处理程序(handler)会打印段错误信息,并产生 core 文件,由此我们可以借助于程序异常退出生成的 core 文件中的调试信息,使用 gdb 工具来调试程序中的段错误。
1)调试步骤
A、在一些Linux版本下,默认是不产生 core 文件的,首先可以查看一下系统 core 文件的大小限制:
B、可以看到默认设置情况下,本机Linux环境下发生段错误不会自动生成 core 文件,下面设置下 core 文件的大小限制(单位为KB)
- root@# ulimit -c 1024
- root@# ulimit -c
- 1024
C、运行程序,发生段错误生成的 core 文件
- root@# ./a.out
- 段错误 (核心已转储)
D、加载 core 文件,使用 gdb 工具进行调试
- root@ubuntu:/home/tarena/project/c_test# gdb a.out core
- GNU gdb (Ubuntu/Linaro 7.4-2012.02-0ubuntu2) 7.4-2012.02
- Copyright (C) 2012 Free Software Foundation, Inc.
- License GPLv3+: GNU GPL version 3 or later <http:
- This is free software: you are free to change and redistribute it.
- There is NO WARRANTY, to the extent permitted by law. Type "show copying"
- and "show warranty" for details.
- This GDB was configured as "i686-linux-gnu".
- For bug reporting instructions, please see:
- <http:
- Reading symbols from /home/tarena/project/c_test/a.out...done.
- [New LWP 3491]
-
- warning: Can't read pathname for load map: 输入/输出错误.
- Core was generated by `./a.out'.
- Program terminated with signal 11, Segmentation fault.
- #0 0x080483c4 in main () at test.c:6
- 6 *ptr = 10;
- (gdb)
从输出看出,可以显示出异样的段错误信息
E、完成调试后,输入 q 命令退出 gdb
2)适用场景
A、适合于在实际生成环境下调试程度的段错误(即在不用重新发生段错误的情况下重现段错误)
B、当程序很复杂,core 文件相当大时,该方法不可用
4、使用 objdump
1)调试步骤
A、使用 dmesg 命令,找到最近发生的段错误输入信息
- root@# dmesg
- [ 372.350652] a.out[2712]: segfault at 0 ip 080483c4 sp bfd1f7b8 error 6 in a.out[8048000+1000]
其中,对我们接下来的调试过程有用的是发生段错误的地址 0 和指令指针地址 080483c4。
有时候,“地址引起的错”可以告诉你问题的根源。看到上面的例子,我们可以说,int *ptr = NULL; *ptr = 10;,创建了一个空指针,然后试图访问它的值(读值)。
B、使用 objdump 生成二进制的相关信息,重定向到文件中
- root@# objdump -d a.out > a.outDump
- root@# ls
- a.out a.outDump core test.c
其中,生成的 a.outDump 文件中包含了二进制文件的 a.out 的汇编代码
C、在 a.outDump 文件中查找发生段错误的地址
- root@ubuntu:/home/tarena/project/c_test# grep -n -A 10 -B 10 "0" a.outDump
- 1-
- 2-a.out: file format elf32-i386
-
- 118:080483b4 <main>:
- 119: 80483b4: 55 push %ebp
- 120: 80483b5: 89 e5 mov %esp,%ebp
- 121: 80483b7: 83 ec 10 sub $0x10,%esp
- 122: 80483ba: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%ebp)
- 123: 80483c1: 8b 45 fc mov -0x4(%ebp),%eax
- 124: 80483c4: c7 00 0a 00 00 00 movl $0xa,(%eax)
- 125: 80483ca: b8 00 00 00 00 mov $0x0,%eax
- 126: 80483cf: c9 leave
- 127: 80483d0: c3 ret
- 128: 80483d1: 90 nop
- 129: 80483d2: 90 nop
- 130: 80483d3: 90 nop
- 131: 80483d4: 90 nop
- 132: 80483d5: 90 nop
- 133: 80483d6: 90 nop
- 134: 80483d7: 90 nop
- 135: 80483d8: 90 nop
- 136: 80483d9: 90 nop
- 137: 80483da: 90 nop
- 138: 80483db: 90 nop
- 139: 80483dc: 90 nop
- 140: 80483dd: 90 nop
- 141: 80483de: 90 nop
- 142: 80483df: 90 nop
通过对以上汇编代码分析,得知段错误发生main函数,对应的汇编指令是movl $0xa,(%eax),接下来打开程序的源码,找到汇编指令对应的源码,也就定位到段错误了。
2)适用场景
A、不需要 -g 参数编译,不需要借助于core文件,但需要有一定的汇编语言基础。
B、如果使用 gcc 编译优化参数(-O1,-O2,-O3)的话,生成的汇编指令将会被优化,使得调试过程有些难度。
5、使用catchsegv
catchsegv 命令专门用来补货段错误,它通过动态加载器(ld-linux.so)的预加载机制(PRELOAD)把一个事先写好的 库(/lib/libSegFault.so)加载上,用于捕捉段错误的出错信息。
- root@t# catchsegv ./a.out
- Segmentation fault (core dumped)
- *** Segmentation fault
- Register dump:
-
- EAX: 00000000 EBX: b77a1ff4 ECX: bfd8a0e4 EDX: bfd8a074
- ESI: 00000000 EDI: 00000000 EBP: bfd8a048 ESP: bfd8a038
-
- EIP: 080483c4 EFLAGS: 00010282
-
- CS: 0073 DS: 007b ES: 007b FS: 0000 GS: 0033 SS: 007b
-
- Trap: 0000000e Error: 00000006 OldMask: 00000000
- ESP/signal: bfd8a038 CR2: 00000000
-
- Backtrace:
- ??:0(main)[0x80483c4]
- /lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0xb761a4d3]
- ??:0(_start)[0x8048321]
-
- Memory map:
-
- 08048000-08049000 r-xp 00000000 08:01 2102158 /home/tarena/project/c_test/a.out
- 08049000-0804a000 r--p 00000000 08:01 2102158 /home/tarena/project/c_test/a.out
- 0804a000-0804b000 rw-p 00001000 08:01 2102158 /home/tarena/project/c_test/a.out
- 09467000-0948c000 rw-p 00000000 00:00 0 [heap]
- b75e1000-b75fd000 r-xp 00000000 08:01 1704884 /lib/i386-linux-gnu/libgcc_s.so.1
- b75fd000-b75fe000 r--p 0001b000 08:01 1704884 /lib/i386-linux-gnu/libgcc_s.so.1
- b75fe000-b75ff000 rw-p 0001c000 08:01 1704884 /lib/i386-linux-gnu/libgcc_s.so.1
- b75ff000-b7601000 rw-p 00000000 00:00 0
- b7601000-b77a0000 r-xp 00000000 08:01 1704863 /lib/i386-linux-gnu/libc-2.15.so
- b77a0000-b77a2000 r--p 0019f000 08:01 1704863 /lib/i386-linux-gnu/libc-2.15.so
- b77a2000-b77a3000 rw-p 001a1000 08:01 1704863 /lib/i386-linux-gnu/libc-2.15.so
- b77a3000-b77a6000 rw-p 00000000 00:00 0
- b77b8000-b77bb000 r-xp 00000000 08:01 1704847 /lib/i386-linux-gnu/libSegFault.so
- b77bb000-b77bc000 r--p 00002000 08:01 1704847 /lib/i386-linux-gnu/libSegFault.so
- b77bc000-b77bd000 rw-p 00003000 08:01 1704847 /lib/i386-linux-gnu/libSegFault.so
- b77bd000-b77bf000 rw-p 00000000 00:00 0
- b77bf000-b77c0000 r-xp 00000000 00:00 0 [vdso]
- b77c0000-b77e0000 r-xp 00000000 08:01 1704843 /lib/i386-linux-gnu/ld-2.15.so
- b77e0000-b77e1000 r--p 0001f000 08:01 1704843 /lib/i386-linux-gnu/ld-2.15.so
- b77e1000-b77e2000 rw-p 00020000 08:01 1704843 /lib/i386-linux-gnu/ld-2.15.so
- bfd6b000-bfd8c000 rw-p 00000000 00:00 0 [stack]
五、一些注意事项
1)出现段错误时,首先应该想到段错误的定义,从它出发考虑引发错误的原因。
2)在使用指针时,定义了指针后记得初始化指针,在使用的时候记得判断是否为 NULL
3)在使用数组时,注意数组是否被初始化,数组下标是否越界,数组元素是否存在等
4)在访问变量,注意变量所占地址空间是否已经被程序释放掉
5)在处理变量时,注意变量的格式控制是否合理等