1 背景

笔者早年写过一篇:《可恶的”Segmentation faults”之初级总结篇》,网络转载甚多。多年下来,关于段错误的讨论依旧很热烈,该问题也还是很常见。所以打算在这里再系统地梳理一下该问题的来龙去脉。

2 什么是段错误

下面是来自 Answers.com 的定义:

A segmentation fault (often shortened to segfault) is a particular error condition that can occur during the operation of computer software. In short, a segmentation fault occurs when a program attempts to access a memory location that it is not allowed to access, or attempts to access a memory location in a way that is not allowed (e.g., attempts to write to a read-only location, or to overwrite part of the operating system). Systems based on processors like the Motorola 68000 tend to refer to these events as Address or Bus errors.

Segmentation is one approach to memory management and protection in the operating system. It has been superseded by paging for most purposes, but much of the terminology of segmentation is still used, “segmentation fault” being an example. Some operating systems still have segmentation at some logical level although paging is used as the main memory management policy.

On Unix-like operating systems, a process that accesses invalid memory receives the SIGSEGV signal. On Microsoft Windows, a process that accesses invalid memory receives the STATUS_ACCESS_VIOLATION exception.

另外,网上还有个基本上对照的中文解释:

所谓的段错误就是指访问的内存超出了系统所给这个程序的内存空间,通常这个值是由 gdtr 来保存的,他是一个 48 位的寄存器,其中的 32 位是保存由它指向的 gdt 表,后 13 位保存相应于 gdt 的下标,最后 3 位包括了程序是否在内存中以及程序的在 cpu 中的运行级别,指向的 gdt 是由以 64 位为一个单位的表,在这张表中就保存着程序运行的代码段以及数据段的起始地址以及与此相应的段限和页面交换还有程序运行级别还有内存粒度等等的信息。一旦一个程序发生了越界访问,cpu 就会产生相应的异常保护,于是 segmentation fault 就出现了

通过上面的解释,段错误应该就是访问了不可访问的内存,这个内存区要么是不存在的,要么是受到系统保护的。

3 段错误日志分析

3.1 例子

一个典型的例子是 scanf 参数使用错误:

  1. #include <stdio.h>
  2. int main(int argc, char *argv[])
  3. {
  4. int i;
  5. scanf("%d\n", i);
  6. return 0;
  7. }

文件保存为 segfault-scanf.c。其中 &i 写成了 i

3.2 段错误信息

  1. $ make segfault-scanf
  2. $ ./segfault-scanf
  3. 100
  4. Segmentation fault (core dumped)

3.3 段错误分析

  1. $ catchsegv ./segfault-scanf
  2. 100
  3. Segmentation fault (core dumped)
  4. *** Segmentation fault
  5. Register dump:
  6. RAX: 0000000000000ca0 RBX: 0000000000000040 RCX: 0000000000000010
  7. RDX: 0000000000000000 RSI: 0000000000000000 RDI: 1999999999999999
  8. RBP: 00007fffdbdf1010 R8 : 00007fbb45330060 R9 : 0000000000000000
  9. R10: 0000000000000ca0 R11: 0000000000000000 R12: 0000000000000004
  10. R13: 0000000000000000 R14: 00007fbb45330640 R15: 000000000000000a
  11. RSP: 00007fffdbdf0c20
  12. RIP: 00007fbb44fc761a EFLAGS: 00010212
  13. CS: 0033 FS: 0000 GS: 0000
  14. Trap: 0000000e Error: 00000006 OldMask: 00000000 CR2: 00000000
  15. FPUCW: 0000037f FPUSW: 00000000 TAG: 00000000
  16. RIP: 00000000 RDP: 00000000
  17. ST(0) 0000 0000000000000000 ST(1) 0000 0000000000000000
  18. ST(2) 0000 0000000000000000 ST(3) 0000 0000000000000000
  19. ST(4) 0000 0000000000000000 ST(5) 0000 0000000000000000
  20. ST(6) 0000 0000000000000000 ST(7) 0000 0000000000000000
  21. mxcsr: 1f80
  22. XMM0: 00000000000000000000000000000000 XMM1: 00000000000000000000000000000000
  23. XMM2: 00000000000000000000000000000000 XMM3: 00000000000000000000000000000000
  24. XMM4: 00000000000000000000000000000000 XMM5: 00000000000000000000000000000000
  25. XMM6: 00000000000000000000000000000000 XMM7: 00000000000000000000000000000000
  26. XMM8: 00000000000000000000000000000000 XMM9: 00000000000000000000000000000000
  27. XMM10: 00000000000000000000000000000000 XMM11: 00000000000000000000000000000000
  28. XMM12: 00000000000000000000000000000000 XMM13: 00000000000000000000000000000000
  29. XMM14: 00000000000000000000000000000000 XMM15: 00000000000000000000000000000000
  30. Backtrace:
  31. /lib/x86_64-linux-gnu/libc.so.6(_IO_vfscanf+0x303a)[0x7fbb44fc761a]
  32. /lib/x86_64-linux-gnu/libc.so.6(__isoc99_scanf+0x109)[0x7fbb44fce399]
  33. ??:?(main)[0x400587]
  34. /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5)[0x7fbb44f91ec5]
  35. ??:?(_start)[0x400499]
  36. Memory map:
  37. 00400000-00401000 r-xp 00000000 08:09 2903814 segfault-scanf
  38. 00600000-00601000 r--p 00000000 08:09 2903814 segfault-scanf
  39. 00601000-00602000 rw-p 00001000 08:09 2903814 segfault-scanf
  40. 01b98000-01bbd000 rw-p 00000000 00:00 0 [heap]
  41. 7fbb44d5a000-7fbb44d70000 r-xp 00000000 08:02 1710807 /lib/x86_64-linux-gnu/libgcc_s.so.1
  42. 7fbb44d70000-7fbb44f6f000 ---p 00016000 08:02 1710807 /lib/x86_64-linux-gnu/libgcc_s.so.1
  43. 7fbb44f6f000-7fbb44f70000 rw-p 00015000 08:02 1710807 /lib/x86_64-linux-gnu/libgcc_s.so.1
  44. 7fbb44f70000-7fbb4512b000 r-xp 00000000 08:02 1731685 /lib/x86_64-linux-gnu/libc-2.19.so
  45. 7fbb4512b000-7fbb4532b000 ---p 001bb000 08:02 1731685 /lib/x86_64-linux-gnu/libc-2.19.so
  46. 7fbb4532b000-7fbb4532f000 r--p 001bb000 08:02 1731685 /lib/x86_64-linux-gnu/libc-2.19.so
  47. 7fbb4532f000-7fbb45331000 rw-p 001bf000 08:02 1731685 /lib/x86_64-linux-gnu/libc-2.19.so
  48. 7fbb45331000-7fbb45336000 rw-p 00000000 00:00 0
  49. 7fbb45336000-7fbb4533a000 r-xp 00000000 08:02 1731696 /lib/x86_64-linux-gnu/libSegFault.so
  50. 7fbb4533a000-7fbb45539000 ---p 00004000 08:02 1731696 /lib/x86_64-linux-gnu/libSegFault.so
  51. 7fbb45539000-7fbb4553a000 r--p 00003000 08:02 1731696 /lib/x86_64-linux-gnu/libSegFault.so
  52. 7fbb4553a000-7fbb4553b000 rw-p 00004000 08:02 1731696 /lib/x86_64-linux-gnu/libSegFault.so
  53. 7fbb4553b000-7fbb4555e000 r-xp 00000000 08:02 1731686 /lib/x86_64-linux-gnu/ld-2.19.so
  54. 7fbb45729000-7fbb4572c000 rw-p 00000000 00:00 0
  55. 7fbb4575a000-7fbb4575d000 rw-p 00000000 00:00 0
  56. 7fbb4575d000-7fbb4575e000 r--p 00022000 08:02 1731686 /lib/x86_64-linux-gnu/ld-2.19.so
  57. 7fbb4575e000-7fbb4575f000 rw-p 00023000 08:02 1731686 /lib/x86_64-linux-gnu/ld-2.19.so
  58. 7fbb4575f000-7fbb45760000 rw-p 00000000 00:00 0
  59. 7fffdbdd2000-7fffdbdf3000 rw-p 00000000 00:00 0
  60. 7fffdbdfe000-7fffdbe00000 r-xp 00000000 00:00 0 [vdso]
  61. ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]

上述日志包含了寄存器、回调以及内存映像信息。其中回调部分的 _IO_vfscanf 即指出了 scanf 的问题。不过咋一看不明显,可以用 gdb 单步跟踪进行确认。

关于寄存器我们最关心的信息:

  1. Trap: 0000000e Error: 00000006

从 arch/x86/include/asm/traps.h 和 arch/x86/kernel/traps.c 找到 SIGSEGV 的类型有:

  1. /* Interrupts/Exceptions */
  2. enum {
  3. ...
  4. X86_TRAP_OF, /* 4, Overflow */
  5. X86_TRAP_BR, /* 5, Bound Range Exceeded */
  6. X86_TRAP_TS, /* 10, Invalid TSS */
  7. X86_TRAP_GP, /* 13, General Protection Fault */
  8. X86_TRAP_PF, /* 14, Page Fault */
  9. ...
  10. }

Trap 为 0xe,即 14,也就是 Page Fault。

而 arch/x86/mm/fault.c 则详细解释了错误码(Error):

  1. /*
  2. * Page fault error code bits:
  3. *
  4. * bit 0 == 0: no page found 1: protection fault
  5. * bit 1 == 0: read access 1: write access
  6. * bit 2 == 0: kernel-mode access 1: user-mode access
  7. * bit 3 == 1: use of reserved bit detected
  8. * bit 4 == 1: fault was an instruction fetch
  9. */
  10. enum x86_pf_error_code {
  11. PF_PROT = 1 << 0,
  12. PF_WRITE = 1 << 1,
  13. PF_USER = 1 << 2,
  14. PF_RSVD = 1 << 3,
  15. PF_INSTR = 1 << 4,
  16. };

上面的错误码:6,二进制为 110 即:

  • 1: user-mode access
  • 1: write access
  • 0: no page found

也可以用 在线查看工具,例如,输入错误码 6 即可获得:

The cause was a user-mode write resulting in no page being found.

4 常见段错误举例

这里列举一下常见的段错误例子。

4.1 scanf 参数:把 &i 写为 i

  1. int i;
  2. scanf("%d", i);

分析:i 被定义后,数值是不确定的,而 scanf 把 i 的值当作参数传入 scanf,而 scanf 则会把 i 当成了地址,把用户输入的内容存入该处。而该地址因为随机,可能根本就不存在或者不合法。

4.2 sprintf/printf 参数:%d/%c 写成 %s

  1. int i = 10;
  2. printf("%s", i);

分析:打印字串时,实际上是打印某个地址开始的所有字符,而这里把整数作为参数传递过去,这个整数被当成了一个地址,然后 printf 从这个地址开始打印字符,直到某个位置上的值为 \0。如果这个整数代表的地址不存在或者不可访问,自然也是访问了不该访问的内存 —— segmentation fault。

4.3 数组访问越界

  1. char test[1];
  2. printf("%c", test[1000000000]);

注:也可能报告为 Bus Error,可能存在对未对齐的地址读或写。

4.4 写只读内存

  1. char *ptr = "test";
  2. strcpy(ptr, "TEST");

分析:ptr 被定义成了 “test”,是一个只读的内存段,不能直接写入,要写入需要用 malloc 从堆中分配或者定义成一个字符串数组。

4.5 堆栈溢出

  1. void main()
  2. {
  3. main();
  4. }

分析:上面实际上是一个死循环的递归调用,会造成堆栈溢出。

4.6 pthread_create() 失败后 pthread_join()

  1. #define THREAD_MAX_NUM
  2. pthread_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 调试

  1. gdb ./segfault-scanf
  2. ...
  3. Reading symbols from ./segfault-scanf...done.
  4. (gdb) r
  5. Starting program: segfault-scanf
  6. 100
  7. Program received signal SIGSEGV, Segmentation fault.
  8. 0x00007ffff7a6b61a in _IO_vfscanf_internal (s=<optimized out>,
  9. format=<optimized out>, argptr=argptr@entry=0x7fffffffddc8,
  10. errp=errp@entry=0x0) at vfscanf.c:1857
  11. 1857 vfscanf.c: No such file or directory.
  12. (gdb) bt
  13. #0 0x00007ffff7a6b61a in _IO_vfscanf_internal (s=<optimized out>,
  14. format=<optimized out>, argptr=argptr@entry=0x7fffffffddc8,
  15. errp=errp@entry=0x0) at vfscanf.c:1857
  16. #1 0x00007ffff7a72399 in __isoc99_scanf (format=<optimized out>)
  17. at isoc99_scanf.c:37
  18. #2 0x0000000000400580 in main ()

5.3 coredump 分析

  1. $ ulimit -c 1024
  2. $ gdb segfault-scanf ./core
  3. Reading symbols from segfault-scanf...done.
  4. [New LWP 16913]
  5. Core was generated by `./segfault-scanf'.
  6. Program terminated with signal SIGSEGV, Segmentation fault.
  7. #0 0x00007fd2d24ec61a in _IO_vfscanf_internal (s=<optimized out>,
  8. format=<optimized out>, argptr=argptr@entry=0x7fff14dfa668,
  9. errp=errp@entry=0x0) at vfscanf.c:1857
  10. 1857 vfscanf.c: No such file or directory.

5.4 程序内捕获 SIGSEGV 信号并启动 gdb

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <signal.h>
  4. #include <string.h>
  5. void dump(int signo)
  6. {
  7. char buf[1024];
  8. char cmd[1024];
  9. FILE *fh;
  10. snprintf(buf, sizeof(buf), "/proc/%d/cmdline", getpid());
  11. if(!(fh = fopen(buf, "r")))
  12. exit(0);
  13. if(!fgets(buf, sizeof(buf), fh))
  14. exit(0);
  15. fclose(fh);
  16. if(buf[strlen(buf) - 1] == '\n')
  17. buf[strlen(buf) - 1] = '\0';
  18. snprintf(cmd, sizeof(cmd), "gdb %s %d", buf, getpid());
  19. system(cmd);
  20. exit(0);
  21. }
  22. int main(int argc, char *argv[])
  23. {
  24. int i;
  25. signal(SIGSEGV, &dump);
  26. scanf("%d\n", i);
  27. return 0;
  28. }

用法如下:

  1. $ gcc -g -rdynamic -o segfault-scanf segfault-scanf.c
  2. $ sudo ./segfault-scanf
  3. 100
  4. (gdb) bt
  5. #0 0x00007fb743e065cc in __libc_waitpid (pid=16988,
  6. stat_loc=stat_loc@entry=0x7fffb51d8fe0, options=options@entry=0)
  7. at ../sysdeps/unix/sysv/linux/waitpid.c:31
  8. #1 0x00007fb743d8b1d2 in do_system (line=<optimized out>)
  9. at ../sysdeps/posix/system.c:148
  10. #2 0x0000000000400ba1 in dump (signo=11) at segfault-scanf.c:21
  11. #3 <signal handler called>
  12. #4 0x00007fb743d9c61a in _IO_vfscanf_internal (s=<optimized out>,
  13. format=<optimized out>, argptr=argptr@entry=0x7fffb51da318,
  14. errp=errp@entry=0x0) at vfscanf.c:1857
  15. #5 0x00007fb743da3399 in __isoc99_scanf (format=<optimized out>)
  16. at isoc99_scanf.c:37
  17. #6 0x0000000000400bdd in main (argc=1, argv=0x7fffb51da508)
  18. at segfault-scanf.c:31

5.5 程序内捕获 SIGSEGV 信号并调用 backtrace 获取回调

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <signal.h>
  4. #include <string.h>
  5. void dump(int signo)
  6. {
  7. void *array[10];
  8. size_t size;
  9. char **strings;
  10. size_t i;
  11. size = backtrace (array, 10);
  12. strings = backtrace_symbols (array, size);
  13. printf ("Obtained %zd stack frames.\n", size);
  14. for (i = 0; i < size; i++)
  15. printf ("%s\n", strings[i]);
  16. free (strings);
  17. exit(0);
  18. }
  19. int main(int argc, char *argv[])
  20. {
  21. int i;
  22. signal(SIGSEGV, &dump);
  23. scanf("%d\n", i);
  24. return 0;
  25. }

用法如下:

  1. $ ./segfault-scanf
  2. 100
  3. Obtained 7 stack frames.
  4. ./segfault-scanf() [0x40077e]
  5. /lib/x86_64-linux-gnu/libc.so.6(+0x36c30) [0x7f249fa43c30]
  6. /lib/x86_64-linux-gnu/libc.so.6(_IO_vfscanf+0x303a) [0x7f249fa6461a]
  7. /lib/x86_64-linux-gnu/libc.so.6(__isoc99_scanf+0x109) [0x7f249fa6b399]
  8. ./segfault-scanf-call-backtrace() [0x400837]
  9. /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf5) [0x7f249fa2eec5]
  10. ./segfault-scanf-call-backtrace() [0x400699]

除此之外,还可以通过 dmesg 查看内核信息并通过 objdump 或者 addr2line 把 IP 地址转化为代码行,不过用法没有 catchsegv 来得简单。dmesg 获取的内核信息由 arch/x86/mm/fault.c: show_signal_msg() 打印。

6 总结

段错误是 Linux 下 C 语言开发常见的 Bug,本文从原理、案例、分析和调试方法等各个方面进行了详细分析,希望有所帮助。

如果希望了解更多,推荐阅读如下参考资料。

7 参考资料

  • Segmentation Fault in Linux —— 原因与避免
  • Linux环境下段错误的产生原因及调试方法小结
  • 段错误bug的调试
  • Segmentation fault
  • Segmentation fault到底是何方妖孽
  • linux内核之trap.c文件分析
  • linux内核中断、异常
  • linux下X86架构IDT解析
  • Segmentation fault error decoder
  • Understanding a Kernel Oops!

Read More:

  • 智能手机系统优化的演进与实践
  • 如何更新远程主机上的 Linux 内核
  • 使用 JDB 调试 Android 应用程序
  • 在 Android Native 程序中输出 LOG
  • 如何快速定位 Linux Panic 出错的代码行

Linux 段错误详解相关推荐

  1. ora01033是什么错误linux,ora_01033错误详解

    原因就是因为我把介质文件给删掉了 解决步骤: 以DBA(通常有好几个sys或system)用户sqlplus登录 请输入用户名:  connect system/sys as sysdba 如果上面登 ...

  2. 【Linux】Linux crontab 命令详解

    原文来自:http://ir.hit.edu.cn/~wsong/development/crontab.html Linux crontab 命令详解 在 Linux 中,任务可以被配置在指定的时间 ...

  3. Linux串口编程详解

    Linux串口编程详解(阻塞模式.非阻塞模式.select函数) 之前一直觉得串口编程很简单,这两天仔细研究后发现串口里的各种参数还挺复杂,稍不注意就容易出错,这里总结一下网上的各种文章及自己的理解与 ...

  4. linux防火墙ddos,Linux iptables防火墙详解 + 配置抗DDOS***策略实战

    Linux iptables防火墙详解 + 配置抗DDOS***策略实战 Linux 内核中很早就实现了网络防火墙功能,在不同的Linux内核版本中,使用了不同的软件实现防火墙功能. 在2.0内核中, ...

  5. linux interfaces配置文件详解

    linux interfaces配置文件详解 配置文件基本格式 一个基本的配置大概是下面这个样子: 1 auto lo2 iface lo inet loopback34 # The primary ...

  6. Linux操作命令分类详解 - 用户权限(三)

    转自:Linux操作命令分类详解 - 用户权限(三),更优阅读体验:http://www.kongzid.com/ 目录 系列文章 1.用户账号管理 1.1 useradd/adduser 添加用户账 ...

  7. 【网络编程】Linux tcpdump命令详解---编辑中

    目录 即看即用 详细说明 简介 输出信息含义 链路层头 TCP 数据包 UDP 数据包 SMB/CIFS 解码 AFS 请求和回应 KIP AppleTalk协议 IP 数据包破碎 时间戳 反向过滤 ...

  8. 19. linux中权限详解,Linux权限位,读写执行权限真正含义,chmod详解

    linux中权限详解,Linux权限位,读写执行权限真正含义,chmod详解 文章目录 Linux权限位 读写执行 三种权限真正含义和作用 权限对文件的作用 权限对目录的作用 示例 chmod 使用数 ...

  9. linux下wait函数,Linux wait函数详解

    wait和waitpid出现的原因 SIGCHLD --当子进程退出的时候,内核会向父进程SIGCHLD信号,子进程的退出是个异步事件(子进程可以在父进程运行的任何时刻终止) --子进程退出时,内核将 ...

最新文章

  1. linux mysql 卸载,安装,測试全过程
  2. 独家 | 我们扒出了这家中国创业公司,竟比苹果iPhone X早两年推出黑科技,还不用借助深度摄像头
  3. Android NDK开发-3-环境搭建
  4. ViewPager,TabLayout,Fragment实现tabs滑动
  5. 【采用】【科技金融】CART树现金贷风控策略
  6. 第一次使用 Blog
  7. AutoCAD VBA天圆地方的放样展开图
  8. maven + sonar, gradle + sonar
  9. 信息安全完全参考手册之信息安全概述(第一章)
  10. 视觉里程计02 基于特征匹配的位姿估计
  11. python如何操作oracle数据库_Python连接oracle数据库的基本操作
  12. 回归 ----一元线性回归
  13. 40个布局排版好看的网页设计作品
  14. 朴素贝叶斯算法——拼写检查器
  15. docker mysql redis 镜像详解
  16. vue3 kepp-alive 的使用
  17. .Net6.0系列-6 .Net 6LinQ(二)常用扩展方法
  18. CS0533隐藏继承的抽条成员/CS0534不实现继承的抽象成员
  19. static全局变量与普通的全局变量
  20. ganache命令行安装

热门文章

  1. sscanf用法(转)
  2. Windows下Eclipse和PyDev搭建完美Python开发环境
  3. 使用 Windows 服务器备份执行 Exchange 2010 备份
  4. nuxt 头部引入js文件 第一次进入页面不加载js文件的解决方法
  5. 【巧妙算法系列】【Uva 11464】 - Even Parity 偶数矩阵
  6. FTP服务器之vsftp
  7. Main-Accounts-and-Financial-Dimensions-Data-Model_thumb
  8. Zend Framework 跳转方法(render, forward, redirect)区...
  9. 如何在修改checkbox状态,不触发事件
  10. Oracle 9i学习日志(9)--数据字典与动态性能视图及练习