1 背景
笔者早年写过一篇:《可恶的”Segmentation faults”之初级总结篇》,网络转载甚多。多年下来,关于段错误的讨论依旧很热烈,该问题也还是很常见。所以打算在这里再系统地梳理一下该问题的来龙去脉。
2 什么是段错误
下面是来自 Answers.com 的定义:
另外,网上还有个基本上对照的中文解释:
通过上面的解释,段错误应该就是访问了不可访问的内存,这个内存区要么是不存在的,要么是受到系统保护的。
3 段错误日志分析
3.1 例子
一个典型的例子是 scanf 参数使用错误:
#include <stdio.h>int main(int argc, char *argv[]){int i;scanf("%d\n", i);return 0;}
文件保存为 segfault-scanf.c。其中 &i 写成了 i。
3.2 段错误信息
$ make segfault-scanf$ ./segfault-scanf100Segmentation fault (core dumped)
3.3 段错误分析
$ catchsegv ./segfault-scanf100Segmentation fault (core dumped)*** Segmentation faultRegister dump:RAX: 0000000000000ca0 RBX: 0000000000000040 RCX: 0000000000000010RDX: 0000000000000000 RSI: 0000000000000000 RDI: 1999999999999999RBP: 00007fffdbdf1010 R8 : 00007fbb45330060 R9 : 0000000000000000R10: 0000000000000ca0 R11: 0000000000000000 R12: 0000000000000004R13: 0000000000000000 R14: 00007fbb45330640 R15: 000000000000000aRSP: 00007fffdbdf0c20RIP: 00007fbb44fc761a EFLAGS: 00010212CS: 0033 FS: 0000 GS: 0000Trap: 0000000e Error: 00000006 OldMask: 00000000 CR2: 00000000FPUCW: 0000037f FPUSW: 00000000 TAG: 00000000RIP: 00000000 RDP: 00000000ST(0) 0000 0000000000000000 ST(1) 0000 0000000000000000ST(2) 0000 0000000000000000 ST(3) 0000 0000000000000000ST(4) 0000 0000000000000000 ST(5) 0000 0000000000000000ST(6) 0000 0000000000000000 ST(7) 0000 0000000000000000mxcsr: 1f80XMM0: 00000000000000000000000000000000 XMM1: 00000000000000000000000000000000XMM2: 00000000000000000000000000000000 XMM3: 00000000000000000000000000000000XMM4: 00000000000000000000000000000000 XMM5: 00000000000000000000000000000000XMM6: 00000000000000000000000000000000 XMM7: 00000000000000000000000000000000XMM8: 00000000000000000000000000000000 XMM9: 00000000000000000000000000000000XMM10: 00000000000000000000000000000000 XMM11: 00000000000000000000000000000000XMM12: 00000000000000000000000000000000 XMM13: 00000000000000000000000000000000XMM14: 00000000000000000000000000000000 XMM15: 00000000000000000000000000000000Backtrace:/lib/x86_64-linux-gnu/libc.so.6(_IO_vfscanf+0x303a)[0x7fbb44fc761a]/lib/x86_64-linux-gnu/libc.so.6(__isoc99_scanf+0x109)[0x7fbb44fce399]??:?(main)[0x400587]/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5)[0x7fbb44f91ec5]??:?(_start)[0x400499]Memory map:00400000-00401000 r-xp 00000000 08:09 2903814 segfault-scanf00600000-00601000 r--p 00000000 08:09 2903814 segfault-scanf00601000-00602000 rw-p 00001000 08:09 2903814 segfault-scanf01b98000-01bbd000 rw-p 00000000 00:00 0 [heap]7fbb44d5a000-7fbb44d70000 r-xp 00000000 08:02 1710807 /lib/x86_64-linux-gnu/libgcc_s.so.17fbb44d70000-7fbb44f6f000 ---p 00016000 08:02 1710807 /lib/x86_64-linux-gnu/libgcc_s.so.17fbb44f6f000-7fbb44f70000 rw-p 00015000 08:02 1710807 /lib/x86_64-linux-gnu/libgcc_s.so.17fbb44f70000-7fbb4512b000 r-xp 00000000 08:02 1731685 /lib/x86_64-linux-gnu/libc-2.19.so7fbb4512b000-7fbb4532b000 ---p 001bb000 08:02 1731685 /lib/x86_64-linux-gnu/libc-2.19.so7fbb4532b000-7fbb4532f000 r--p 001bb000 08:02 1731685 /lib/x86_64-linux-gnu/libc-2.19.so7fbb4532f000-7fbb45331000 rw-p 001bf000 08:02 1731685 /lib/x86_64-linux-gnu/libc-2.19.so7fbb45331000-7fbb45336000 rw-p 00000000 00:00 07fbb45336000-7fbb4533a000 r-xp 00000000 08:02 1731696 /lib/x86_64-linux-gnu/libSegFault.so7fbb4533a000-7fbb45539000 ---p 00004000 08:02 1731696 /lib/x86_64-linux-gnu/libSegFault.so7fbb45539000-7fbb4553a000 r--p 00003000 08:02 1731696 /lib/x86_64-linux-gnu/libSegFault.so7fbb4553a000-7fbb4553b000 rw-p 00004000 08:02 1731696 /lib/x86_64-linux-gnu/libSegFault.so7fbb4553b000-7fbb4555e000 r-xp 00000000 08:02 1731686 /lib/x86_64-linux-gnu/ld-2.19.so7fbb45729000-7fbb4572c000 rw-p 00000000 00:00 07fbb4575a000-7fbb4575d000 rw-p 00000000 00:00 07fbb4575d000-7fbb4575e000 r--p 00022000 08:02 1731686 /lib/x86_64-linux-gnu/ld-2.19.so7fbb4575e000-7fbb4575f000 rw-p 00023000 08:02 1731686 /lib/x86_64-linux-gnu/ld-2.19.so7fbb4575f000-7fbb45760000 rw-p 00000000 00:00 07fffdbdd2000-7fffdbdf3000 rw-p 00000000 00:00 07fffdbdfe000-7fffdbe00000 r-xp 00000000 00:00 0 [vdso]ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
上述日志包含了寄存器、回调以及内存映像信息。其中回调部分的 _IO_vfscanf 即指出了 scanf 的问题。不过咋一看不明显,可以用 gdb 单步跟踪进行确认。
关于寄存器我们最关心的信息:
Trap: 0000000e Error: 00000006
从 arch/x86/include/asm/traps.h 和 arch/x86/kernel/traps.c 找到 SIGSEGV 的类型有:
/* Interrupts/Exceptions */enum {...X86_TRAP_OF, /* 4, Overflow */X86_TRAP_BR, /* 5, Bound Range Exceeded */X86_TRAP_TS, /* 10, Invalid TSS */X86_TRAP_GP, /* 13, General Protection Fault */X86_TRAP_PF, /* 14, Page Fault */...}
Trap 为 0xe,即 14,也就是 Page Fault。
而 arch/x86/mm/fault.c 则详细解释了错误码(Error):
/** Page fault error code bits:** bit 0 == 0: no page found 1: protection fault* bit 1 == 0: read access 1: write access* bit 2 == 0: kernel-mode access 1: user-mode access* bit 3 == 1: use of reserved bit detected* bit 4 == 1: fault was an instruction fetch*/enum x86_pf_error_code {PF_PROT = 1 << 0,PF_WRITE = 1 << 1,PF_USER = 1 << 2,PF_RSVD = 1 << 3,PF_INSTR = 1 << 4,};
上面的错误码:6,二进制为 110 即:
- 1: user-mode access
- 1: write access
- 0: no page found
也可以用 在线查看工具,例如,输入错误码 6 即可获得:
4 常见段错误举例
这里列举一下常见的段错误例子。
4.1 scanf 参数:把 &i 写为 i
int i;scanf("%d", i);
分析:i 被定义后,数值是不确定的,而 scanf 把 i 的值当作参数传入 scanf,而 scanf 则会把 i 当成了地址,把用户输入的内容存入该处。而该地址因为随机,可能根本就不存在或者不合法。
4.2 sprintf/printf 参数:%d/%c 写成 %s
int i = 10;printf("%s", i);
分析:打印字串时,实际上是打印某个地址开始的所有字符,而这里把整数作为参数传递过去,这个整数被当成了一个地址,然后 printf 从这个地址开始打印字符,直到某个位置上的值为 \0。如果这个整数代表的地址不存在或者不可访问,自然也是访问了不该访问的内存 —— segmentation fault。
4.3 数组访问越界
char test[1];printf("%c", test[1000000000]);
注:也可能报告为 Bus Error,可能存在对未对齐的地址读或写。
4.4 写只读内存
char *ptr = "test";strcpy(ptr, "TEST");
分析:ptr 被定义成了 “test”,是一个只读的内存段,不能直接写入,要写入需要用 malloc 从堆中分配或者定义成一个字符串数组。
4.5 堆栈溢出
void main(){main();}
分析:上面实际上是一个死循环的递归调用,会造成堆栈溢出。
4.6 pthread_create() 失败后 pthread_join()
#define THREAD_MAX_NUMpthread_t thread[THREAD_MAX_NUM];
分析:用 pthread_create() 创建了各个线程,然后用 pthread_join() 来等待线程的结束。刚开始直接等待,在创建线程都成功时,pthread_join() 能顺利等到各个线程结束,但是一旦创建线程失败,用 pthread_join() 来等待那个本不存在的线程时自然会存在未知内存的情况,从而导致段错误的发生。解决办法是:在创建线程之前,先初始化线程数组,在等待线程结束时,判断线程是否为初始值,如果是的话,说明线程并没有创建成功,所以就不能等拉。
4.7 小结
综上所有例子,
- 定义了指针后记得初始化,在使用时记得判断是否为 NULL
- 在使用数组时记得初始化,使用时要检查数组下标是否越界,数组元素是否存在等
- 在变量处理时变量的格式控制是否合理等
其他的就需要根据经验不断积累,更多例子会不断追加到上述列表中。
另外,也务必掌握一些基本的分析和调试手段,即使在遇到新的这类问题时也知道如何应对。
5 分析和调试手段
分析方法除了最简便的 catchsegv 外,还有诸多办法,它们的应用场景各异。
5.1 catchsegv 原理
该工具就是用来扑获段错误的,它通过动态加载器(ld-linux.so)的预加载机制(PRELOAD)把一个事先写好的库(/lib/libSegFault.so)加载上,用于捕捉断错误的出错信息。
5.2 gdb 调试
gdb ./segfault-scanf...Reading symbols from ./segfault-scanf...done.(gdb) rStarting program: segfault-scanf100Program received signal SIGSEGV, Segmentation fault.0x00007ffff7a6b61a in _IO_vfscanf_internal (s=<optimized out>,format=<optimized out>, argptr=argptr@entry=0x7fffffffddc8,errp=errp@entry=0x0) at vfscanf.c:18571857 vfscanf.c: No such file or directory.(gdb) bt#0 0x00007ffff7a6b61a in _IO_vfscanf_internal (s=<optimized out>,format=<optimized out>, argptr=argptr@entry=0x7fffffffddc8,errp=errp@entry=0x0) at vfscanf.c:1857#1 0x00007ffff7a72399 in __isoc99_scanf (format=<optimized out>)at isoc99_scanf.c:37#2 0x0000000000400580 in main ()
5.3 coredump 分析
$ ulimit -c 1024$ gdb segfault-scanf ./coreReading symbols from segfault-scanf...done.[New LWP 16913]Core was generated by `./segfault-scanf'.Program terminated with signal SIGSEGV, Segmentation fault.#0 0x00007fd2d24ec61a in _IO_vfscanf_internal (s=<optimized out>,format=<optimized out>, argptr=argptr@entry=0x7fff14dfa668,errp=errp@entry=0x0) at vfscanf.c:18571857 vfscanf.c: No such file or directory.
5.4 程序内捕获 SIGSEGV 信号并启动 gdb
#include <stdio.h>#include <stdlib.h>#include <signal.h>#include <string.h>void dump(int signo){char buf[1024];char cmd[1024];FILE *fh;snprintf(buf, sizeof(buf), "/proc/%d/cmdline", getpid());if(!(fh = fopen(buf, "r")))exit(0);if(!fgets(buf, sizeof(buf), fh))exit(0);fclose(fh);if(buf[strlen(buf) - 1] == '\n')buf[strlen(buf) - 1] = '\0';snprintf(cmd, sizeof(cmd), "gdb %s %d", buf, getpid());system(cmd);exit(0);}int main(int argc, char *argv[]){int i;signal(SIGSEGV, &dump);scanf("%d\n", i);return 0;}
用法如下:
$ gcc -g -rdynamic -o segfault-scanf segfault-scanf.c$ sudo ./segfault-scanf100(gdb) bt#0 0x00007fb743e065cc in __libc_waitpid (pid=16988,stat_loc=stat_loc@entry=0x7fffb51d8fe0, options=options@entry=0)at ../sysdeps/unix/sysv/linux/waitpid.c:31#1 0x00007fb743d8b1d2 in do_system (line=<optimized out>)at ../sysdeps/posix/system.c:148#2 0x0000000000400ba1 in dump (signo=11) at segfault-scanf.c:21#3 <signal handler called>#4 0x00007fb743d9c61a in _IO_vfscanf_internal (s=<optimized out>,format=<optimized out>, argptr=argptr@entry=0x7fffb51da318,errp=errp@entry=0x0) at vfscanf.c:1857#5 0x00007fb743da3399 in __isoc99_scanf (format=<optimized out>)at isoc99_scanf.c:37#6 0x0000000000400bdd in main (argc=1, argv=0x7fffb51da508)at segfault-scanf.c:31
5.5 程序内捕获 SIGSEGV 信号并调用 backtrace 获取回调
#include <stdio.h>#include <stdlib.h>#include <signal.h>#include <string.h>void dump(int signo){void *array[10];size_t size;char **strings;size_t i;size = backtrace (array, 10);strings = backtrace_symbols (array, size);printf ("Obtained %zd stack frames.\n", size);for (i = 0; i < size; i++)printf ("%s\n", strings[i]);free (strings);exit(0);}int main(int argc, char *argv[]){int i;signal(SIGSEGV, &dump);scanf("%d\n", i);return 0;}
用法如下:
$ ./segfault-scanf100Obtained 7 stack frames../segfault-scanf() [0x40077e]/lib/x86_64-linux-gnu/libc.so.6(+0x36c30) [0x7f249fa43c30]/lib/x86_64-linux-gnu/libc.so.6(_IO_vfscanf+0x303a) [0x7f249fa6461a]/lib/x86_64-linux-gnu/libc.so.6(__isoc99_scanf+0x109) [0x7f249fa6b399]./segfault-scanf-call-backtrace() [0x400837]/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f249fa2eec5]./segfault-scanf-call-backtrace() [0x400699]
除此之外,还可以通过 dmesg 查看内核信息并通过 objdump 或者 addr2line 把 IP 地址转化为代码行,不过用法没有 catchsegv 来得简单。dmesg 获取的内核信息由 arch/x86/mm/fault.c: show_signal_msg() 打印。
6 总结
段错误是 Linux 下 C 语言开发常见的 Bug,本文从原理、案例、分析和调试方法等各个方面进行了详细分析,希望有所帮助。
如果希望了解更多,推荐阅读如下参考资料。
3735

被折叠的 条评论
为什么被折叠?



