总线错误与分段错误

41

总线错误和段错误有什么区别?一个程序第一次出现段错误导致停止,第二次出现总线错误并退出,这种情况可能发生吗?


可能是什么是总线错误?的重复。 - Ciro Santilli OurBigBook.com
7个回答

51

在我使用的大多数架构上,二者的区别在于:

  • SEGV 是由于访问非法内存(例如地址超出进程虚拟地址空间)引起的。
  • SIGBUS 则是由于 CPU 对齐问题引起的(例如尝试从地址读取 long 类型数据,而该地址不是 4 的倍数)。

20
内存映射文件也可能会生成SIGBUS错误。 - bk1e
在ARM上,如果您从未对齐的地址读取浮点数,则可能会发生SIGBUS。 - shoosh
5
好的,我会尽力做到最好的翻译。这句话的意思是:“嘘,我很确定那已经被我的第二条要点覆盖了。” - paxdiablo

26
如果你使用mmap()映射一个文件,并尝试访问超出文件末尾的映射缓冲区的部分,或者出现空间不足等错误条件,SIGBUS信号也会被触发。如果你使用sigaction()注册了一个信号处理程序,并设置了SA_SIGINFO,那么你的程序可能可以检查需要处理的内存地址,只处理与内存映射文件有关的错误。

7
例如,当您的程序尝试执行硬件总线不支持的操作时,可能会导致总线错误。例如,在 SPARC 上,尝试从奇地址读取多字节值(例如 int,32 位)会生成总线错误。
例如,当您进行违反分段规则的访问,即尝试读取或写入您不拥有的内存时,就会发生分段错误。

当你说“读取或写入你不拥有的内存”时,你是什么意思? 当你执行malloc时,你已经分配了比如说5个字节的内存。如果你读取/写入你不拥有的内存,在C语言中它不会给你一个段错误。 - Thunderboltz
相反地,在同一地址空间中覆盖某个其他对象拥有的内存会导致段错误。 - Geek
1
在操作系统级别上,“你所拥有的”通常比运行时(例如通过malloc)向你展示的要多得多。因此,有很多内存空间可以访问,虽然你拥有它们,但仍然不应该使用,还有很多地址空间可以读取,但不能写入(大多数映射库),以及特定的函数来写保护内存区域(mprotect)。 - David Schmitt
3
@Geek:操作系统无法知道在同一地址空间内进行写入的是“谁”。因此,它无法保护您免受同一程序内存覆盖的影响。这就是大多数安全漏洞能够生效的原因。 - David Schmitt
显然跟Pax和Bastien相比,我很菜。 :) 但是是的,@Thunderboltz,就像其他评论者(还有P&B)解释的那样,当您尝试访问不属于您的内存时,会发生段错误。 - unwind

6
解释一下你的问题(可能不正确),意思是“我时而会收到SIGSEGV或者SIGBUS,为什么不一致?”值得注意的是,在C或C++标准中,使用指针进行不良操作不能保证会导致segfault(段错误);这只是“未定义行为”,正如我曾经的一位教授所说,这意味着它可能会导致鳄鱼从地板上出现并吃掉你。

因此,你的情况可能是有两个错误,第一个错误有时会导致SIGSEGV,第二个错误(如果程序没有崩溃且仍在运行)会导致SIGBUS。

我建议你使用调试器逐步调试,并注意观察鳄鱼。


4
一个程序第一次会因为段错误(SIGSEGV)而停止运行,第二次则可能因为总线错误(SIGBUS)而退出,即使是同一个bug也可能出现这种情况。以下是一个来自macOS的严重但简单的示例,它可以通过超出数组边界的索引以确定的方式产生段错误和总线错误。在macOS中,上述未对齐访问并不是问题。(如果在调试器(例如lldb)中运行,则此示例不会导致任何SIGBUS!) bus_segv.c:
#include <stdlib.h>

char array[10];

int main(int argc, char *argv[]) {
    return array[atol(argv[1])];
}

该示例从命令行中获取一个整数,该整数作为数组的索引。某些索引值(即使在数组之外)也不会导致任何信号。(所有给定值都取决于标准片段/部分大小。我使用clang-902.0.39.1在High Sierra macOS 10.13.5上生成二进制文件,i5-4288U CPU @ 2.60GHz。)
索引大于77791并且小于-4128会导致分段错误(SIGSEGV)。24544会导致总线错误(SIGBUS)。下面是完整的映射表:
$ ./bus_segv -4129
Segmentation fault: 11
$ ./bus_segv -4128
...
$ ./bus_segv 24543
$ ./bus_segv 24544
Bus error: 10
...
$ ./bus_segv 28639
Bus error: 10
$ ./bus_segv 28640
...
$ ./bus_segv 45023
$ ./bus_segv 45024
Bus error: 10
...
$ ./bus_segv 53215
Bus error: 10
$ ./bus_segv 53216
...
$ ./bus_segv 69599
$ ./bus_segv 69600
Bus error: 10
...
$ ./bus_segv 73695
Bus error: 10
$ ./bus_segv 73696
...
$ ./bus_segv 77791
$ ./bus_segv 77792
Segmentation fault: 11

如果你查看反汇编代码,你会发现总线错误范围的边界并不像索引出现的那么奇怪:

$ otool -tv bus_segv

bus_segv:
(__TEXT,__text) section
_main:
0000000100000f60    pushq   %rbp
0000000100000f61    movq    %rsp, %rbp
0000000100000f64    subq    $0x10, %rsp
0000000100000f68    movl    $0x0, -0x4(%rbp)
0000000100000f6f    movl    %edi, -0x8(%rbp)
0000000100000f72    movq    %rsi, -0x10(%rbp)
0000000100000f76    movq    -0x10(%rbp), %rsi
0000000100000f7a    movq    0x8(%rsi), %rdi
0000000100000f7e    callq   0x100000f94 ## symbol stub for: _atol
0000000100000f83    leaq    0x96(%rip), %rsi
0000000100000f8a    movsbl  (%rsi,%rax), %eax
0000000100000f8e    addq    $0x10, %rsp
0000000100000f92    popq    %rbp    
0000000100000f93    retq    

通过 leaq 0x96(%rip), %rsi,rsi成为数组起始地址的(相对于PC确定的)地址:

rsi = 0x100000f8a + 0x96 = 0x100001020
rsi - 4128 = 0x100000000 (below segmentation fault)
rsi + 24544 = 0x100007000 (here and above bus error)
rsi + 28640 = 0x100008000 (below bus error)
rsi + 45024 = 0x10000c000 (here and above bus error)
rsi + 53216 = 0x10000e000 (below bus error)
rsi + 69600 = 0x100012000 (here and above bus error)
rsi + 73696 = 0x100013000 (below bus error)
rsi + 77792 = 0x100014000 (here and above segmentation fault)

lldb 可能会针对进程设置不同的页面限制。在调试会话中,我无法重现任何总线错误。因此,调试器可能是避免二进制文件出现总线错误的一种解决方法。

安德烈亚斯


4

我假设你所说的是Posix定义的SIGSEGVSIGBUS信号。

SIGSEGV发生在程序引用无效地址时。SIGBUS是一种实现定义的硬件故障。这两个信号的默认操作是终止程序。

程序可以捕获这些信号,甚至忽略它们。


1
如果不考虑问题中的这段代码:

Can it happen that a program gives a seg fault and stops for the first time and for the second time it may give a bus error and exit ?

那么这与什么是总线错误?是重复的。
你应该能够通过在此处找到的信息自己回答这个问题。
疯狂就是一遍又一遍地做同样的事情,却期待不同的结果。
-- 阿尔伯特·爱因斯坦

当然,如果按照字面意思理解这个问题的话...

#include <signal.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
int main() {
    srand(time(NULL));
    if (rand() % 2)
        kill(getpid(), SIGBUS);
    else
        kill(getpid(), SIGSEGV);
    return 0;
}

Tada,一个程序可以在一次运行中因分段错误而退出,在另一次运行中因总线错误而退出。


最佳答案 - Daniel Bandeira

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接