一、什么是段错误?

一旦一个程序发生了越界访问,cpu 就会产生相应的保护,于是 segmentation fault 就出现了,通过上面的解释,段错误应该就是访问了不可访问的内存,这个内存区要么是不存在的,要么是受到系统保护的,还有可能是缺少文件或者文件损坏。

二、段错误产生的原因

  • 非关联化空指针——这是特殊情况由内存管理硬件
  • 试图访问一个不存在的内存地址(在进程的地址空间)
  • 试图访问内存的程序没有权利(如内核结构流程上下文)
  • 试图写入只读存储器(如代码段)

    2.1 访问不存在的内存地址

    在C代码,分割错误通常发生由于指针的错误使用,特别是在C动态内存分配。非关联化一个空指针总是导致段错误,但野指针和悬空指针指向的内存,可能会或可能不会存在,而且可能或不可能是可读的还是可写的,因此会导致瞬态错误。 ```c /*
    • @Author : Aloys Zhang
    • @Date : 2020-11-29 16:45:31
    • @LastEditors : Aloys Zhang
    • @LastEditTime : 2020-11-29 17:41:12
    • @FilePath : /test/test.c */

include

include

int main() { printf(“test\n”); char * test=NULL; printf(“%s\n”,test); }

  1. 运行结果:
  2. ```basic
  3. [ 1806.242951] test[4639]: segfault at 0 ip 000014f3a1dff7c6 sp 00007ffd6c63ccf8 error 4 in libc-2.23.so[14f3a1d74000+1c0000]
  4. [ 1806.242955] Code: ff ff ff 90 66 0f ef c0 66 0f ef c9 66 0f ef d2 66 0f ef db 48 89 f8 48 89 f9 48 81 e1 ff 0f 00 00 48 81 f9 cf 0f 00 00 77 6a <f3> 0f 6f 20 66 0f 74 e0 66 0f d7 d4 85 d2 74 04 0f bc c2 c3 48 83

现在,非关联化这些变量可能导致段错误:非关联化空指针通常会导致段错误,阅读时从野指针可能导致随机数据但没有段错误,和阅读从悬空指针可能导致有效数据,然后随机数据覆盖。

2.2 访问系统保护的内存地址


#include <stdio.h>

int main (void)
{
    int *ptr = (int *)0;
    *ptr = 100;
    return 0;
}

2.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。

2.5 操作空指针

2.5 堆栈溢出

#include <stdio.h>
#include <string.h>

int main (void)
{
    main ();
    return 0;
}

上述例子的无限递归,导致的堆栈溢出会导致段错误,但无线递归未必导致堆栈溢出,优化执行的编译器和代码的确切结构。在这种情况下,遥不可及的代码(返回语句)行为是未定义的。因此,编译器可以消除它,使用尾部调用优化,可能导致没有堆栈使用。其他优化可能包括将递归转换成迭代,给出例子的结构功能永远会导致程序运行,虽然可能不是其他堆栈溢出。

2.6 内存越界(数组越界,变量类型不一致等)

#include <stdio.h>

int main (void)
{
    char test[10];
    printf ("%c\n", test[100000]);
    return 0;
}

三、段错误信息的获取

3.1 dmesg

dmesg 可以在应用程序崩溃时,显示内存中保存的相关信息。如下所示,通过 dmesg 命令可以查看发生段错误的程序名称、引起段错误发生的内存地址、指令指针地址、堆栈指针地址、错误代码、错误原因等。

[ 1806.242951] test[4639]: segfault at 0 ip 000014f3a1dff7c6 sp 00007ffd6c63ccf8 error 4 in libc-2.23.so[14f3a1d74000+1c0000]
[ 1806.242955] Code: ff ff ff 90 66 0f ef c0 66 0f ef c9 66 0f ef d2 66 0f ef db 48 89 f8 48 89 f9 48 81 e1 ff 0f 00 00 48 81 f9 cf 0f 00 00 77 6a <f3> 0f 6f 20 66 0f 74 e0 66 0f d7 d4 85 d2 74 04 0f bc c2 c3 48 83

3.2 添加 -g,生成调试信息

使用gcc编译程序的源码时,加上 -g 参数,这样可以使得生成的二进制文件中加入可以用于 gdb 调试的有用信息

  • GCC 编译
  • 静态库与共享库
  • C 编译流程

    3.3 nm

    使用 nm 命令列出二进制文件中符号表,包括符号地址、符号类型、符号名等。这样可以帮助定位在哪里发生了段错误。

    3.4 ldd

    使用 ldd 命令查看二进制程序的共享链接库依赖,包括库的名称、起始地址,这样可以确定段错误到底是发生在了自己的程序中还是依赖的共享库中。

    四、段错误的调试方法

    4.1 使用 printf 输出信息

    4.2 使用 gcc 和 gdb

    gcc -g test.c
    

    适用场景

    A、仅当能确定程序一定会发生段错误的情况下适用。
    B、当程序的源码可以获得的情况下,使用 -g 参数编译程序
    C、一般用于测试阶段,生产环境下 gdb 会有副作用:使程序运行减慢,运行不够稳定,等等。
    D、即使在测试阶段,如果程序过于复杂,gdb 也不能处理。

    4.3 使用 core 文件和 gdb

  • 在一些Linux版本下,默认是不产生 core 文件的,首先可以查看一下系统 core 文件的大小限制:

    ulimit -a
    ulimit -c unlimited
    ulimit -s unlimited
    
  • 加载 core 文件,使用 gdb 工具进行调试

    gdb a.out core
    

    适用场景

    A、适合于在实际生成环境下调试程度的段错误(即在不用重新发生段错误的情况下重现段错误)
    B、当程序很复杂,core 文件相当大时,该方法不可用

    4.4 使用 objdump

  • 使用 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;,创建了一个空指针,然后试图访问它的值(读值)

  • 使用 objdump 生成二进制的相关信息,重定向到文件中

    objdump -d a.out > a.outDump 
    ls
    a.out  a.outDump  core  test.c
    

    适用场景

    A、不需要 -g 参数编译,不需要借助于core文件,但需要有一定的汇编语言基础。
    B、如果使用 gcc 编译优化参数(-O1,-O2,-O3)的话,生成的汇编指令将会被优化,使得调试过程有些难度

    4.5 使用catchsegv

    catchsegv ./a.out
    

    五、内核如何打印用户段错信息

  • 配置内核支持DEBUG_USER (勾选 Kernel hacking -> Verbose user fault messages[*] 即可)

  • 设置bootargs,添加参数 user_debug = 0xFF 即可。 user_debug的每一位代表设置不同的模式,具体模式可参考文件:include/asm-arm/System.h下的UDBG_XXX。

开起内核打印用户应用 oops 详细流程