C专家编程 第7章 对内存的思考 7.7 总线错误

    总线错误 
    常见的运行时错误:
    bus error(core dumped)总线错误(信息已转储)
    和
    segmentation fault(core dumped)段错误(信息已转储)
 
    大多数的问题都是出于这样一个事实:错误就是操作系统所检测到的异常,而这个异常是尽可能以操作系统方便的原则来报告的。总线错误和段错误的准确原因在不同的操作系统版本上各不相同。
    这里,我所描述的是运行于SPARC架构的SunOS出现的这两类错误以及产生错误的原因:

    当硬件告诉操作系统一个有问题的内存引用时,就会出现这两种错误。操作系统通过向出错的进程发送一个信号与之交流。信号就是一种事件通知或一个软件中断,在UNIX系统编程中应用很广,但在应用程序编程中几乎不使用。在缺省情况下,进程收到“总线错误”或“段错误”信号后将信息转储并终止。不过可以为这些信号设置一个信号处理程序(signal handler),用于修改进程的缺省反应。
    信号是由于硬件中断而产生的。对中断的编程是非常困难的,因为它们是异步发生的(即发生时间是不可预测的)。因此,信号编程和调试也是很困难的。可以通过阅读信号的主文档和头文件usr/include/sys/signal.h了解更多相关的信息。

    信号处理函数是ANSI C的一部分,与UNIX一样,它也同样适用于PC。
    /*
    **编程挑战 
    */ 
    /*请在PC上编写一个捕捉INT 1B(Ctrl-Break)信号的信号处理程序,让它打印一条友好的用户信息但并不退出程序。*/
    //捕捉INT 1B(Ctrl-Break)信号的信号处理程序
    #include<stdio.h>
    #include<signal.h>
    #include<setjmp.h>
    #include<stdlib.h>
    jmp_buf buf;
    void handler(int s)
    {
        if (s == SIGBREAK) printf( "now got a break signal\n" );
        if (s == SIGSEGV) printf( "now got a segmentation violation signal\n" );
        if (s == SIGILL) printf ( "now got an illegal instruction signal\n" );
        if (s == SIGINT) printf ( "now got an interupt instruction signal\n" );
        longjmp(buf,1);
    }
    int main(void)
    {
        signal(SIGBREAK,handler);
        signal(SIGSEGV,handler);
        signal(SIGILL,handler);
        signal(SIGINT,handler);
        if(setjmp(buf))
        {
            printf("back in main\n");
            getchar();
            return EXIT_FAILURE;
        }
        else printf("first time through\n");
        loop:
        //在这里循环,等待ctrl+c
        goto loop;
        
        return EXIT_SUCCESS;
    }
    如果你使用UNIX,请编写一个信号处理程序,这样在收到Ctrl-C(传递给一个UNIX进程的Ctrl-C用作一个SIGINT信号)信号后程序将重新启动而不是简单退出。可以使用typedef来帮助你定义信号处理块,详见第3章有关声明的描述。

    使用setjmp/longjmp从信号中恢复
    #include <setjmp.h>
    使用setjmp/longjmp和信号处理。这样,程序在收到Ctrl-C(作为SIGINT信号传递给UNIX程序)
时重新启动,而不是退出。
    注意,系统并不支持在信号处理程序内部调用库函数(除非严格符合标准所限制的条件)
    观察情况的最好方法莫过于交互式I/O。你在现实的代码中不能使用这种伎俩,记住了吗? 

    #include <signal.h>
    #include <stdio.h>
    #include <stdlib.h>
    jmp_buf buf;
    void handler(int s) {
        if (s == SIGINT) {
            printf("now got a SIGINT signal");
        }
        longjmp(buf, 1);
        /*没有到达*/ 
    }

    int main() {
        signal(SIGINT, handler);
        if (setjmp(buf)) {
            printf("back in main\n");
            return EXIT_FAILURE;
        } else {
            printf("first time through\n");
        }    
        loop:
        /*在这里循环,等待ctr-c*/
        goto loop; 
    }

输出:

/*并没有打印back in main就直接结束了?哪位大神知道原因是什么吗?*/

    在任何使用信号的源文件中,都必须在文件前面包含一行#include<signal.h>。

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

    我们表示“数据项不能跨越页面或cache边界”规则的方法多少有些间接,因为我们用地址对齐这个术语来陈诉这个问题,而不是直接了当说是禁止内存跨页访问,但它们说的是同一回事。 
    “走在裂缝上,会折断你祖母的背”有点类似“提取未对齐地址数据然后低声诅咒,会引起一个总线错误。” 
    #include <stdio.h>

    int main() {
        union {
            char a[10];
            int i;
        } u;
        int *p = (int *)&(u.a[1]);
        *p = 17; /*p中未对齐的地址会引起一个总线错误*/ 
        printf( "%d\n", u.a[1] );
        return 0;
    }
    这将导致一个总线错误,因为数据和int的联合确保数据a是按照int的4字节对齐的,所以“a+1”的地址未按int对齐。然后我们试图往这个地址存储4字节的数据,但这个访问只是按照单字节的char对齐,这就违反了规则。一个好的编译器在发现不对齐的情况下会发出警告。

    编译器通过自动分配和填充数据(在内存中)来进行对齐。当然,在磁盘和磁带上并没有这样的对齐要求,所以程序员在其上可以很愉快地不必关心数据对齐。但是,当他们把一个char指针转换为int指针时,就会出现神秘的总线错误。总线错误也可能由于引用一块物理上不存在的内存引起。

    段错误
    段错误或段违规(segmentation violation)。在Sun的硬件中,段错误由内存管理单元(负责支持虚拟内存的硬件)的异常所致,而该异常则通常是由解除引用一个未初始化或非法值的指针引起的。如果指针引用一个并不位于你的地址空间中的地址,操作系统便会对此进行干涉。
一个引起段错误的小型程序如下: 
    int main() {
        int *p = 0;
        *p = 17; /*引起一个段错误*/ 
    } 
    一个微妙之处是,不同的编程错误通常会导致指针具有非法的值。与总线错误不同,段错误更像是一个简洁的症状而不是引起错误的原因。
    一个更糟糕的微妙之处是,如果未初始化的指针恰好具有未对齐的值(对于指针所要访问的数据而言),它将产生总线错误,而不是段错误。对于绝大多数架构的计算机而言确实如此,因为CPU先看到地址,然后再把它发送给MMU。 
    对非法指针值的解除引用既可能会像上面这样显式地出现,也可能在库函数中出现(传递给它一个非法值)。 

    SunOS中的一个段违规Bug
    一个段违规问题,错误发生于ncheck实用程序运行于一个受损的文件系统之时。因为在绝大多数情况下,使用ncheck的目的就是为了检查怀疑有所损坏的文件系统。
    问题的症状是ncheck无法运行printf,直接原因是解除引用一个空指针而引起段违规。 
    int main() {
        int *p = 0;
        (void)printf("%s", p->name);
    }
    //啰嗦的方法 
    if (p->name != NULL) {
        (void)printf("%s", p->name);
    } else {
        (void)printf("null");
    }
    //条件运算符,既可以简化代码,又可以保持引用的局部性 
    (void)printf("%s", p->name ? p->name : "null");

    //条件运算符更合理一些 
    if (表达式) 表达式非零时的语句 else 表达式为零时的语句
    表达式 ?  表达式非零时的语句 else 表达式为零时的语句
    千万不要在一个条件运算符内嵌套另一个条件操作符。如果这样做了,你很快机会发现想明白代码的确切意思可不是件容易的事情。 

    通常导致段错误的几个直接原因如下所示:
    *解除引用一个包含非法值的指针
    *解除引用一个空指针(常常用于从系统程序中返回空指针,并未经检查就使用)
    *在未得到正确的权限时进行访问。例如,试图往一个只读的文本段存储值就会引起段错误
    *用完了堆栈或堆空间(虚拟内存虽然巨大但绝非无限)
    下面这个说法可能过于简单,但在绝大多数架构的绝大多数情况下,总线错误意味着CPU对进程引用内存的一些做法不满,而段错误则是MMU对进程引用内存的一些情况发出抱怨。
    以发生频率为序,最终可能导致段错误的常见编程错误如下所示。 

    坏指针值错误:在指针赋值之前就用它来引用内存,或者向库函数传送一个坏指针(不要上当!如果调试器显示系统程序中出现了段错误,并不是因为系统程序引起了段错误,问题很可能还在存在于自己的代码中)。第三种可能导致坏指针的原因是对指针进行释放之后再访问它的内容。可以修改free语句,在指针释放之后再将它置为空值。 
    free(p); p = NULL;
    这样,如果在指针释放之后继续使用该指针,至少程序能在终止之前进行信息转储。  
    改写(overwrite)错误:越过数组边界写入数据,在动态分配的内存两端之外写入数据,或改写一些堆管理数据结构(在动态分配的内存之前的区域写入数据就很容易发生这种情况)。 
    p = malloc(256); p[-1] = 0; p[256] = 0; 
    指针释放引起的错误:释放同一个内存块两次,或释放一块未曾使用malloc分配的内存,或释放仍在使用中的内存,或释放一个无效的指针。一个极为常见的释放内存有关的错误就是在 
    for (p = start; p; p = p->next) {
        free(p);
    } 
    p = p->next相当于(*p).next; 
    这样在下一次循环迭代时,程序就会对已经释放的指针进行解除引用操作,从而导致不可预料的结果。
    如何在链表中释放元素
    在遍历链表时正确释放元素的方法是使用临时变量存储下一个元素的地址。这样就可以安全地在任何时候释放当前元素,而不必担心在取下一个元素的地址时还要引用它。 
    struct node *p, *start, *tmp;
    for (p = start; p; p = tmp) {
        tmp = p->next;
        free(p)
    }

    如果你的程序所需的内存超过了操作系统所能提供给它的数量,程序就会发出一条“段错误”信息并终止。可以用一种简单的方法把这种段错误与基于Bug的段错误区分开来。
    要弄清是否用完了堆栈,可以在dbx命令下运行该程序:
    %dbx a.out
    (dbx) catch SIGSEGV
    (dbx) run
    ...
    signal SEGV (segmentation violation) in <some_routine> at 0xeff57708
    (dbx) where
    如果可以看到调用链,那说明堆栈空间还没有用完。
    但是,如果看到像下面这样的提示:
    fetch at 0xeffe7a60 failed -- I/O error
    (dbx)
    那么,堆栈很可能已经用完。上面这个十六进制数就是可以提取或映射的堆栈地址。你也可以尝试在C-shell中调整堆栈段的大小限制
    limit stacksize 10
    你可以在C-shell中调整堆栈段和数据段的最大值。上面语句的意思就是把堆栈段的上限调整为10KB。进程的总地址空间仍然受到交换区大小的限制,可以用swap-s命令查看交换区的大小。

    当程序出现坏指针时,什么样的结果都有可能发生。一种广为接受的说法是,如果“你走运”,指针将指向你的地址空间之外,这样第一次使用该指针时就会使程序进行信息转储后终止。如果你“不走运”,指针将指向你的地址空间之内,并损坏(改写)它所指向的内存的任何信息。这将引起隐晦的Bug,非常难以捕捉。近年来,市场上出现了一些优秀的工具软件,可以帮助你解决这方面的问题。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_40186813

你的能量无可限量。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值