SIGBUS和SIGSEGV的区别

本文通过两个示例程序对比分析了SIGBUS和SIGSEGV两种内存错误信号产生的原因。通过对程序的汇编代码及计算机底层执行流程的剖析,揭示了两者之间的根本区别:SIGBUS通常因地址未对齐引发,而SIGSEGV则是由于访问非法内存地址导致。

SIGBUS和SIGSEGV也许是我们在平时遇到的次数最多的两个内存错误信号。内存问题一直是最令我们头疼的事情,弄清楚两个信号的发生缘由对我们很好的理解程序的运行是大有裨益的。

我们来看两段程序:
//testsigsegv.c
int main() {
        char *pc = (char*)0x00001111;
        *pc = 17;
}

//testsigbus.c
int main() {
        int *pi = (int*)0x00001111;
        *pi = 17;
}

上面的代码那么的相似,我们也同样用gcc编译(加上-g选项,便于gdb调试;平台Solaris Sparc),执行结果也都是dump core。但通过GDB对core进行观察,你会发现细微的不同。第一个例子出的core原因是:Program terminated with signal 11, Segmentation fault. 而第二个例子的core则提示:Program terminated with signal 10, Bus error. 两者有什么不同呢?这两段代码的共同点都是将一个非法地址赋值给指针变量,然后试图写数据到这个地址。

如果要说清楚这个问题,我们就要结合汇编码和一些计算机的体系结构的知识来共同分析了。

先来看testsigsegv.c的汇编码:
... ...
main:
        !#PROLOGUE# 0
        save    %sp, -120, %sp
        !#PROLOGUE# 1
        sethi   %hi(4096), %i0
        or      %i0, 273, %i0
        st      %i0, [%fp-20]
        ld      [%fp-20], %i1
        mov     17, %i0
        stb     %i0, [%i1]
        nop
        ret
        restore
... ...

我们关注的是这句:stb     %i0, [%i1]
从计算机底层的执行角度来说,过程是如何的呢?%i0寄存器里存储的是立即数17,我们要将之存储到寄存器%i1的值指向的内存地址。这一过程对于CPU来说其指挥执行的正常过程是:将寄存器%i0中的值送上数据总线,将寄存器%i1的值送到地址总线,然后使能控制总线上的写信号完成这一向内存写1 byte数据的过程。

我们再看testsigbus.c的汇编码:
... ...
main:
        !#PROLOGUE# 0
        save    %sp, -120, %sp
        !#PROLOGUE# 1
        sethi   %hi(4096), %i0
        or      %i0, 273, %i0
        st      %i0, [%fp-20]
        ld      [%fp-20], %i1
        mov     17, %i0
        st      %i0, [%i1]
        nop
        ret
        restore
... ...

同样最后一句:st      %i0, [%i1],CPU执行的过程与testsigsegv.c中的一致(只是要存储数据长度是4字节),那为什么产生错误的原因不同呢?一个是SIGSEGV,而另一个是SIGBUS。这里涉及到的就是对内存地址的校验的问题了,包括对内存地址是否对齐的校验以及该内存地址是否合法的校验。

我们假设如果首先进行的内存地址是否合法的校验(是否归属于用户进程的地址空间),那么我们回顾一下,这两个程序中的地址0x00001111显然都不合法,按照这种流程,两个程序都应该是SIGSEGV导致的core才对,但是事实并非如此。那难道是先校验内存地址的对齐?我们再看这种思路是否合理?

testsigsegv.c中,0x00001111这个地址值被赋给了char *pc;也就是告诉CPU通过这个地址我们要存取一个字节的值,对于一个字节长度的数据,无所谓对齐,所以该地址通过对齐校验;并被放到地址总线上了。而在testsigbus.c里,0x00001111这个地址值被赋给了int *pi;也就是告诉CPU通过这个地址我们要存取一个起码4个字节的值,那么对于长度4个字节的对象,其存放地址起码要被4整除才可以,而0x00001111这个值显然不能满足要求,也就不能通过内存对齐的校验。也就是说SIGBUS这个信号在地址被放到地址总线之后被检查出来的不符合对齐的错误;而SIGSEGV则是在地址已经放到地址总线上后,由后续流程中的某个设施检查出来的内存违法访问错误。

一般我们平时遇到SIGBUS时总是因为地址未对齐导致的,而SIGSEGV则是由于内存地址不合法造成的。

http://bigwhite.blogbus.com/logs/12296535.html

### Linux 中 SIGSEGV 信号的含义及处理 #### SIGSEGV 的定义与触发原因 SIGSEGV 是 Linux 系统中的一个信号,表示发生了段错误(Segmentation Fault),通常由于程序尝试访问未分配给它的内存区域而引发。这种错误可能由多种情况引起,包括但不限于以下几种: - 访问空指针 `nullptr` 或者未初始化的指针[^1]。 - 越界访问数组或其他数据结构[^2]。 - 尝试修改只读内存区域的内容,例如字符串常量或代码段[^3]。 当这些操作被执行时,操作系统检测到非法内存访问并发送 SIGSEGV 信号通知应用程序存在潜在问题。 #### 处理 SIGSEGV 方法概述 为了应对 SIGSEGV 错误,默认情况下该信号会导致程序终止运行,并生成 core dump 文件用于调试分析。然而,在某些特殊场景下可以自定义其行为通过设置信号处理器来改变这一过程。下面详细介绍两种常见方式——利用标准库函数 `signal()` 更灵活强大的 POSIX 接口 `sigaction()` 来注册信号处理器。 ##### 使用 signal 函数捕获 SIGSEGV 最简单的办法就是采用 C 标准库里的 `signal()` 函数指定当接收到特定类型的信号时应该调用哪个回调函数作为响应措施。如下所示的例子展示了如何创建一个基本的信号处理器以拦截 SIGSEGV 并打印消息然后退出应用: ```c #include <signal.h> #include <stdio.h> #include <stdlib.h> static void signal_handler(int sig) { printf("Caught a segmentation fault (SIGSEGV)\n"); exit(EXIT_FAILURE); } int main() { if (signal(SIGSEGV, signal_handler) == SIG_ERR){ fprintf(stderr,"Error setting signal handler\n"); return EXIT_FAILURE; } int* p = NULL; // Deliberate null pointer dereference. *p = 1; return EXIT_SUCCESS; } ``` 尽管这种方法易于理解实施,但它有一些局限性不确定性因素需要注意,比如不能保证每次都能成功安装新的信号处理器实例等问题。 ##### 利用 sigaction 设置复杂信号动作 对于更加精细控制的需求来说,则推荐使用功能更为全面丰富的 `sigaction()` API 替代原始版本。它可以提供额外选项参数允许开发者进一步定制化自己的解决方案策略。例如可以通过标志位组合形式启用自动重置机制或者防止递归进入同一个信号上下文中等情况的发生。 这里给出一段示范代码片段说明怎样运用此技术构建健壮可靠的异常恢复框架: ```c #define _GNU_SOURCE #include <execinfo.h> #include <signal.h> #include <stdio.h> #include <stdlib.h> #include <ucontext.h> void segfault_sigaction(int signal, siginfo_t *si, void *arg) { void *trace[16]; char **messages = (char **)NULL; int i, trace_size = 0; printf("Signal %d (%s), address is %p from %p\n", si->si_signo, strsignal(si->si_signo), si->si_addr, __builtin_return_address(0)); trace_size = backtrace(trace, 16); messages = backtrace_symbols(trace, trace_size); for(i=0;i<trace_size;++i) printf("%s\n", messages[i]); free(messages); abort(); } struct sigaction sa; sa.sa_flags = SA_SIGINFO|SA_ONSTACK; sa.sa_sigaction = segfault_sigaction; if(sigemptyset(&sa.sa_mask)==-1 || sigaddset(&sa.sa_mask,SIGBUS)==-1 || sigaction(SIGSEGV,&sa,NULL)!=0 ){ perror("Failed to set up SIGSEGV handler."); exit(EXIT_FAILURE); } // Continue normal execution... ``` 上述示例不仅记录了堆栈跟踪信息以便后续排查定位具体位置外还启用了备用栈支持以防万一主线程栈空间耗尽造成二次崩溃风险。 #### 注意事项 虽然理论上能够捕捉住大部分常规情形下的分页违例事件但是实际开发过程中仍需谨慎对待以下几个方面: - 如果在信号处理器内部再次触碰同样的条件则可能导致无限循环直至资源耗竭最终还是失败告终; - 不同平台架构之间可能存在细微差异影响兼容性表现; ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值