什么是总线错误?它和分段错误有什么不同?

350

“总线错误”消息是什么意思?它与分段错误有何不同?


14
我想为两者都添加一个简单的解释:段错误意味着您正在尝试访问您不被允许的内存(例如,它不是您程序的一部分)。然而,在总线错误上,通常意味着您正在尝试访问不存在的内存(例如,您尝试访问12G地址,但您只有8G内存),或者如果您超出可用内存的限制。 - xdevs23
你在哪个平台上看到的?PC?Mac?x86?32/64? - Peter Mortensen
当我将一个指向字符串字面值(只读内存)的指针传递给strtok时,我遇到了一个总线错误。这是当发生未定义行为时可能发生的情况的一个例子。 - Ali
17个回答

326

总线错误在x86上现在很少见,当处理器无法尝试所请求的内存访问时发生;通常是由于:

  • 使用具有不符合对齐要求的地址的处理器指令。

段错误发生在访问不属于进程内存的内存时。它们非常普遍,通常是以下情况的结果:

  • 使用已释放的指针。
  • 使用未初始化的虚假指针。
  • 使用空指针。
  • 溢出缓冲区。

附注:更精确地说,并不是操作指针本身会导致问题,而是访问它指向的内存(解引用)。


156
它们并不罕见,我只是在《如何艰难地学习C语言》的第9个练习中就遇到了一个…… - 11684
42
在Linux系统中,导致总线错误的另一个原因是操作系统无法将虚拟页面与物理内存对应(例如低内存条件或在使用大页内存时没有足够的大页)。通常mmap(和malloc)仅保留虚拟地址空间,内核会按需分配物理内存(称为软页故障)。如果进行足够大的malloc,然后写入足够多的内容,就会产生总线错误。 - Eloff
1
对我来说,包含“/var/cache”目录的分区仅仅是满了。 - c33s
2
在我的情况下,一个方法将一个 void * 参数强制转换为存储回调的对象(一个属性指向对象,另一个属性指向该方法)。然后调用回调。但是,作为 void * 传递的东西完全不同,因此方法调用导致总线错误。 - Christopher K.
当我使用内存映射 I/O 时,我的 .NET Core 应用程序在磁盘已满时也会出现这种情况。也许这与 @Eloff 的评论有关。 - John
显示剩余4条评论

111

段错误(segfault)是指尝试访问未授权访问的内存。这是只读的,你没有权限,等等...

总线错误(bus error)是指试图访问不可能存在的内存。您使用了系统无法理解的地址或者该操作的错误类型的地址。


28

mmap 最小化的 POSIX 7 示例

当内核向进程发送 SIGBUS 信号时会出现 "总线错误"。

因为忘记使用 ftruncate,导致产生此问题的最简示例如下:

#include <fcntl.h> /* O_ constants */
#include <unistd.h> /* ftruncate */
#include <sys/mman.h> /* mmap */

int main() {
    int fd;
    int *map;
    int size = sizeof(int);
    char *name = "/a";

    shm_unlink(name);
    fd = shm_open(name, O_RDWR | O_CREAT, (mode_t)0600);
    /* THIS is the cause of the problem. */
    /*ftruncate(fd, size);*/
    map = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    /* This is what generates the SIGBUS. */
    *map = 0;
}

使用以下命令运行:

gcc -std=c99 main.c -lrt
./a.out

在 Ubuntu 14.04 中测试。

POSIX 描述 SIGBUS 为:

访问内存对象的未定义部分。

mmap 规范 表示:

pa 开始并持续 len 字节的地址范围内对整个对象之后的整页的引用将导致发送 SIGBUS 信号。

shm_open 表示 它生成大小为 0 的对象:

共享内存对象大小为零。

因此,在 *map = 0 处,我们正在触及已分配对象的末尾。

ARMv8 aarch64 中非对齐的堆栈内存访问

这在 SPARC 中有提到:什么是总线错误?,但这里我将提供一个更可重现的例子。

你只需要一个 freestanding aarch64 程序:

.global _start
_start:
asm_main_after_prologue:
    /* misalign the stack out of 16-bit boundary */
    add sp, sp, #-4
    /* access the stack */
    ldr w0, [sp]

    /* exit syscall in case SIGBUS does not happen */
    mov x0, 0
    mov x8, 93
    svc 0

该程序在Ubuntu 18.04 aarch64,Linux kernel 4.15.0下,在ThunderX2服务器上引发SIGBUS。

不幸的是,我无法在QEMU v4.0.0用户模式下重现它,我不确定原因。

该故障似乎是可选的,并由SCTLR_ELx.SASCTLR_EL1.SA0字段控制,我在此处进一步概括了相关文档


11

我认为当应用程序在数据总线上表现出数据未对齐时,内核会引发SIGBUS信号。我认为由于现代大多数处理器的大多数编译器都会为程序员填充/对齐数据,因此昔日的对齐问题已经得到缓解,因此我们今天不太经常看到SIGBUS(据我所知)。

来自:这里


4
取决于您在代码中使用的卑劣技巧。如果您做了一些愚蠢的事情,比如进行指针运算,然后强制类型转换以访问问题模式(即您设置了一个uint8_t数组,将1、2或3加到数组的指针上,然后强制转换为short、int或long并尝试访问有问题的结果),则可能会触发BUS错误/对齐陷阱。X86系统基本上可以让您这样做,尽管会受到真正的性能损失。一些 ARMv7系统也可以让您这样做,但大多数ARM、MIPS、Power等系统会对此抱怨。 - Svartalf

10

我同意以上所有答案。关于BUS错误,以下是我的两分钱:

BUS错误不一定来自程序代码内的指令。当您运行一个二进制文件时,在执行期间,该二进制文件被修改(被构建覆盖或删除等)可能会导致此问题。

验证是否为此情况

检查导致此错误的方法之一是从构建输出目录启动几个相同二进制文件的实例,并在它们开始后运行一个构建。 构建完成并替换了当前正在运行的二进制文件(两个实例都在运行的那个),这两个运行实例都会在短时间内崩溃并显示SIGBUS错误。

潜在原因

这是因为操作系统交换存储页面,并且在某些情况下,该二进制文件可能没有完全加载到内存中。 当操作系统尝试从同一二进制文件获取下一页时,如果自上次读取以来二进制文件已更改,则会发生这些崩溃。


2
同意,这是我经验中导致总线错误最常见的原因。 - itaych

10
POSIX系统上,当某些原因导致无法分页代码页时,您也可以收到SIGBUS信号。请注意,此处保留了HTML标签。

10
更新.so文件并同时运行进程时,经常会出现这种情况。 - poordeveloper
1
另一个发生的原因是,如果您尝试将文件mmap到比/dev/shm的大小更大的空间,则会出现此问题。 - ilija139

5

我最近在OS X上使用C语言编程时遇到了总线错误的具体例子:

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

int main(void)
{
    char buffer[120];
    fgets(buffer, sizeof buffer, stdin);
    strcat("foo", buffer);
    return 0;
}

如果您不记得文档,strcat 将第二个参数附加到第一个参数上,通过改变第一个参数来实现(反转参数也可以正常工作)。在Linux上,这会导致分段错误(如预期的那样),但在OS X上,它会导致总线错误。为什么?我真的不知道。


可能是堆栈溢出保护引发了总线错误。 - Joshua
3
"foo" 存储在只读的内存段中,因此无法对其进行写入。这不是堆栈溢出保护,而是内存写入保护(如果您的程序可以重写自身,则存在安全漏洞)。 - Mark Lakata

5

总线错误的一个典型示例是在某些体系结构上(例如SPARC,至少某些SPARC,可能已经更改),当您进行未对齐的访问时。例如:

unsigned char data[6];
(unsigned int *) (data + 2) = 0xdeadf00d;

该代码片段试图将32位整数值0xdeadf00d写入一个(很可能)未正确对齐的地址,这将在那些在这方面“挑剔”的架构上生成总线错误。顺便说一下,英特尔x86不是这样的架构。它会允许访问(尽管执行速度较慢)。

1
如果我有一个数据数组data[8],在32位架构中,这个数字现在是4的倍数,所以已经对齐了。我现在还会遇到错误吗?此外,请解释一下,将指针进行数据类型转换是否是一个不好的想法。它会在易碎结构上导致对齐错误吗?请详细说明,这将对我很有帮助。 - dexterous
2
“脆弱”并不是我用来形容这一切的词。 X86机器和代码已经让人们做了一些相当愚蠢的事情,这就是其中之一。 如果您遇到此类问题,请重新思考您的代码-从一开始在X86上它就不是非常高效的。 - Svartalf
3
@Supercat: 问题在于,在X86上你可以这样做而不会出问题。但如果你在ARM、MIPS、Power等上尝试,就会遇到可怕的问题。在ARM架构低于版本V7的情况下,您的代码将出现对齐错误;而在V7上,如果您的运行时设置为此,则可能会以严重的性能损失处理它。你根本不想这样做,这是一种糟糕的实践,说白了就是不好。 :D - Svartalf
@Svartalf:我很清楚不同平台对齐访问的处理方式是不同的。我希望C标准能够定义一种方式,通过这种方式源代码可以指定“我希望这种类型的行为类似于一个具有__对齐方式的指针,指向使用类型__的每个位置的__低位存储的无符号整数,在__-first格式下”,并让编译器生成必要的机器码来实现这一点。编译器可能最终需要生成糟糕的低效代码,但这也不会比程序员为了可移植性而编写的代码更糟糕。这就是区别... - supercat
在满足自然处理器行为的平台上,编译器可以轻松利用所述要求。例如,在x86上,如果程序员请求MSB优先对齐,则处理器可能需要在加载和存储之前添加字节交换指令,但这仍然比四个单独的8位存储更便宜。 - supercat
显示剩余2条评论

3
首先,SIGBUS和SIGSEGV不是特定类型的错误,而是错误的组或家族。这就是为什么通常会看到一个信号编号(si_no)和一个信号代码(si_code)。
它们还取决于操作系统和架构,因此可能会导致它们的确切原因也不同。
一般来说,我们可以说: SIGSEGV与内存映射(权限、无映射等)有关,即mmu错误。
当内存映射成功并且遇到底层内存系统的问题(内存不足、该位置没有内存、对齐、smmu阻止访问等)时,就会发生SIGBUS,即总线错误。
如果文件从系统中消失,例如将文件映射到可移动介质上并将其拔出,则SIGBUS也可能出现在mmap文件中。
在平台上查看信号子类型的好地方是siginfo.h头文件,以了解信号子类型的概述。 例如,对于Linux,此页面提供了一个概述。 https://elixir.bootlin.com/linux/latest/source/include/uapi/asm-generic/siginfo.h#L245
/*
 * SIGSEGV si_codes
 */
#define SEGV_MAPERR 1   /* address not mapped to object */
#define SEGV_ACCERR 2   /* invalid permissions for mapped object */
#define SEGV_BNDERR 3   /* failed address bound checks */
#ifdef __ia64__
# define __SEGV_PSTKOVF 4   /* paragraph stack overflow */
#else
# define SEGV_PKUERR    4   /* failed protection key checks */
#endif
#define SEGV_ACCADI 5   /* ADI not enabled for mapped object */
#define SEGV_ADIDERR    6   /* Disrupting MCD error */
#define SEGV_ADIPERR    7   /* Precise MCD exception */
#define SEGV_MTEAERR    8   /* Asynchronous ARM MTE error */
#define SEGV_MTESERR    9   /* Synchronous ARM MTE exception */
#define NSIGSEGV    9

/*
 * SIGBUS si_codes
 */
#define BUS_ADRALN  1   /* invalid address alignment */
#define BUS_ADRERR  2   /* non-existent physical address */
#define BUS_OBJERR  3   /* object specific hardware error */
/* hardware memory error consumed on a machine check: action required */
#define BUS_MCEERR_AR   4
/* hardware memory error detected in process but not consumed: action optional*/
#define BUS_MCEERR_AO   5
#define NSIGBUS     5

最后需要注意的是,所有信号也可以由用户生成,例如kill命令。如果是由用户生成,则si_code为SI_USER。因此,特殊来源会得到负的si_code。
/*
 * si_code values
 * Digital reserves positive values for kernel-generated signals.
 */
#define SI_USER     0       /* sent by kill, sigsend, raise */
#define SI_KERNEL   0x80        /* sent by the kernel from somewhere */
#define SI_QUEUE    -1      /* sent by sigqueue */
#define SI_TIMER    -2      /* sent by timer expiration */
#define SI_MESGQ    -3      /* sent by real time mesq state change */
#define SI_ASYNCIO  -4      /* sent by AIO completion */
#define SI_SIGIO    -5      /* sent by queued SIGIO */
#define SI_TKILL    -6      /* sent by tkill system call */
#define SI_DETHREAD -7      /* sent by execve() killing subsidiary threads */
#define SI_ASYNCNL  -60     /* sent by glibc async name lookup completion */

#define SI_FROMUSER(siptr)  ((siptr)->si_code <= 0)
#define SI_FROMKERNEL(siptr)    ((siptr)->si_code > 0)

3

当根目录占满100%时,我遇到了总线错误。


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