C中的总线错误和段错误

最近写了个基于linux的日志系统,中途遇到了两个错误: bus error(core dumped)和segmentation fault(core dumped)。 这两个错误非常的折磨人,错误信息对引起这两种错误的源代码错误并未作简单的解释,上面的信息并未提供如何从代码中寻找错误的线索。所以往往很难定位到具体出错在哪里。

大多数的问题都出于这样一个事实:错误就是操作系统(OS)所检测到的异常,而这个异常是尽可能地以OS方便的原则来报告的。总线错误和段错误的准确原因在不同的OS版本上各不相同。

当OS检测到一个有问题的内存引用时,就会出现这两种错误。OS通过向出错的进程发送一个信号(signal)与之交流。信号就是一种事件通知或一个软件中断,在默认情况下,进程在收到“总线错误”或“段错误”信号后将进行信息转储并中止运行。不过也可以为这些信号设置一个信号处理程序(signal handler),用于修改进程的默认反应。

1. 总线错误

事实上,总线错误几乎都是由于未对齐的读或写引起的。它之所以称为总线错误,是因为出现未对齐的内存访问请求时,被阻塞(block)的组件就是地址总线。对齐(alignment)的意思就是数据项只能存储在地址是数据项大小的整数倍的内存上。在现代的计算机架构中,尤其是RISC架构,都需要数据对齐,因为与任意的对齐有关的有关的额外逻辑会使整个内存系统更大且更慢。通过迫使每个内存访问局限在一个cache行或一个单独的页面内,可以极大地简化并加速如cache控制器和内存管理单元(MMU)这样的硬件。

我们用地址对齐这个术语来陈述这个问题,而不是直截了当地说是禁止内存跨页访问,但它们说但是同一回事。例如,访问一个8字节的double数据时,地址只允许是8的整数倍。所以一个double数据可以存储于地址24,地址8008或32768,但不能存储于地址1006(因为它无法被8整除)。

页和cache的大小都是经过精心设计的,这样只要遵守对齐规则就可以保证一个原子数据项不会跨过一个页或cache块的边界。

《c专家编程》一书给了个关于总线错误的实例,

#include<stdio.h>

union {
    char a[10];
    int i;
} u;

int main(void)
{
#if defined(__GNUC__)
# if defined(__i386__)
    /* Enable Alignment Checking on x86 */
    __asm__("pushf\norl $0x40000,(%esp)\npopf");
# elif defined(__x86_64__)
    /* Enable Alignment Checking on x86_64 */
    __asm__("pushf\norl $0x40000,(%rsp)\npopf");
# endif
#endif

    int *p = (int *) (&(u.a[1]));
    
    /**
     * p中未对齐的地址将会引起总线错误,
     * 因为数组和int的联合确保了a是按照int的4字节来对齐的,
     * 所以“a+1”肯定不是int对齐的
     */
    *p = 17; 
    printf("%d %p %p %p\n", *p, &(u.a[0]), &(u.a[1]), &(u.i));
    printf("%lu %lu\n", sizeof(char), sizeof(int));
    return 0;
}

复制代码

运行结果:

Bus error (core dumped)
复制代码

main函数开始的那段条件编译括起来的汇编,是使能x86平台的对齐核对的,默认x86平台是不进行对齐核对的。如果把那段代码去掉,可执行文件将不会报错,得到的运行结果是:

17 0x601030 0x601031 0x601030
1 4
复制代码

这是因为x86体系结构会把地址对齐之后,访问两次,然后把第一次的尾巴和第二次的头拼起来。所以造成了不对齐也可以访问的假象。

测试的过程中发现,如果在编译的过程中给gcc加上-O3选项,代码也可以正常运行。

测试环境:Ubuntu 12.04.5 LTS, x86_64, gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3

2. 段错误

段错误是由于内存管理单元(MMU)的异常导致的,而该异常通常是由于引用一个未初始化或非法值的指针引起的。如果指针引用一个并不在进程地址空间的地址,便会引发该错误。

《c专家编程》一书给了个关于段错误最简单的实例,

int *p = 0;
*p = 17;
复制代码

指针p指向了一个空地址,所以该赋值语句是空地址写入17,所以会报Segmentation fault。

一个微妙之处是,导致指针具有非法值通常是由于不同的编程错误引起的。

一个更糟糕的情况是,如果未初始化的指针恰好具有非对齐的值,它将会产生总线错误,而不是段错误。对于绝大多数架构的计算机而言确实如此,因为cpu先看到地址,然后再把它发送给MMU。

通常导致段错误的几个直接原因:

  • 引用一个包含非法值的指针

  • 引用一个空指针(常常是由于从函数中返回空指针,未经检查就使用造成的)

  • 在未得到正确的权限时进行访问。例如,试图往一个只读的文本段存储值就会引起段错误。

  • 用完了堆或栈空间。

以发生频率为序,最终可能导致段错误的常见编程错误是:

  • 坏指针值错误

    • 未给指针赋值,就引用指针指向的内存
    • 向库函数传递一个坏指针
    • 对指针指向的内存释放了之后再访问改内存。可以像下面这样做,这样在指针释放之后继续使用该指针的话,至少程序能在终止之前进行core dump。
      free(p);
      p = NULL;
      复制代码
  • overwrite错误

    • 越过数组边界使用该指针

    • 在动态分配的内存两端之外写入数据,比如,动态分配的内存,用户得到的内存地址p前面包含heap管理的数据结构,如果往地址p前面一点写入值,很可能会破坏heap管理结构。或者在地址p之后写入值,导致heap中下一块内存被破坏。这两种情况都会导致heap内部出错。

      p = malloc(256);
      p[-1] = 0;
      p[256] = 0
      复制代码
  • 指针释放引起的错误

    • free同一块内存两次

    • free一块不是使用malloc分配的内存

    • free仍在使用中的内存

    • free一个无效的指针

      比如,像下面这样迭代一个链表时,在下一次循环迭代时,程序对已经释放的内存进行再次引用时,会发生不可预料的结果。

      for(p=start;p;p=p->next)
        free(p);
      复制代码

      应该引入一个tmp指针保存。

      for(p=start;p;) {
         tmp = p;
         p = p->next;
         free(tmp);
      }
      复制代码

下面再举个我遇到的一个段错误的例子:

#include <stdio.h>

#define SZ (64*1024*1024)

void func(void)
{
    char buf[SZ];
    printf("sizeof buf=%lu\n", sizeof(buf));
}

int main(void)
{
    func();
    return 0;
}
复制代码

这个是栈空间用尽的错误。

其实我当初遇到的问题没有这么明显,我分配的空间SZ没有那么大,所以一般情况下是正常的。当我程序运行过程中,数据越来越多,超过这个SZ的时候,再把数据写到buf就报段错误了。所以严格来说,我的这个错误是破坏了栈空间。

3. 怎么排查这种难缠的错误

这种错误非常难排查,记得当初没有经验的时候,通过在源码里不断加printf来调试,现在想想这种方法是有多低效,如果遇到概率性的问题,那就基本没有办法。后来查阅过后才知道充分利用core文件。内核转储的最大好处是能够保存问题发生时的状态。即使问题没有复现,只要获取内核转储,也能调试,通过可执行文件和内核转储,就可以知道进程当时的状态,知道发生问题时的现场,甚至定位到出问题的语句。

在Ubuntu下,默认是不开启core dump的。

3.1 开启core文件

可通过在终端输入下面的命令查看:

ulimit -c
复制代码

显示为零,表示core文件的大小限制在0,即不生成core文件。

设置core file size限制为1G blocks,可在终端输入:

ulimit -c 1073741824
复制代码

或者不限制core file size:

ulimit -c unlimited
复制代码

3.2 通过gdb调试

在终端输入下面的命令即可调试

gdb executable-file core-file
复制代码

以上面往空地址赋值17的例子为例,当在当前目录下生产core文件后,在终端输入

gdb ./a.out core
复制代码

即可得到下面的结果

...
[New LWP 6950]

warning: Can not read pathname for load map: Input/output error.

warning: no loadable sections found in added symbol-file system-supplied DSO at 0x7ffe32533000
Core was generated by ./a.out.
Program terminated with signal 11, Segmentation fault.
#0  0x00000000004005bb in main () at segmentation_error.c:14
14	    *p = 17;
(gdb) 

复制代码

非常厉害的是,你可以重新运行并调试该可执行文件,设置断点,查看变量,非常方便。

详细的关于core文件设置的,可参考coredump设置方法

参考:

  1. 《The C Programming Language中文版(第2版.新版)》
  2. 《C专家编程》
  3. ubuntu core dump设置方法

转载于:https://juejin.im/post/5afd8d00f265da0b745261e0

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值