Linux错误(2)之SIGBUS错误分析
Author: Once Day Date: 2025年3月12日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…
漫漫长路,有人对你微笑过嘛…
全系列文章可参考专栏: Linux实践记录_Once_day的博客-CSDN博客
参考文章:
- libunwind] Unwind through aarch64/Linux sigreturn frame (llvm.org)
- Aarch64 crash (SIGBUS) due to atomic instructions on under-aligned memory (gnu.org)
- Arm A64 Instruction Set Architecture
- JVM coredump分析系列(4):常见的SIGBUS案例分析 | HeapDump性能社区
- Logging and debugging unaligned accesses on Linux / aarch64 - Stack Overflow
- singal 7 SIGBUS(Bus error)_program received signal sigbus, bus error. memset
-CSDN博客- Bus error的调试解决方法-CSDN博客
- 段错误(SIGSEGV)与总线错误(SIGBUS)-CSDN博客
- SIGSEGV 和 SIGBUS & gdb看汇编 - blcblc - 博客园 (cnblogs.com)
- c - What is a bus error? Is it different from a segmentation fault? - Stack Overflow
- X86 Linux 下 SIGBUS 总结 - twoon - 博客园 (cnblogs.com)
- 关于SIGBUS 信号_received signal sigbus, bus error-CSDN博客
- access_mem in aaarch64 coredump · Issue #260 · libunwind/libunwind · GitHub
- libunwind(3) (nongnu.org)
文章目录
- Linux错误(2)之SIGBUS错误分析
- 1. 问题分析
- 1.1【现象介绍】
- 1.2【分析原因】
- 1.3【解决思路】
- 2. 实例验证
- 2.1 对齐问题触发SIGBUS信号
- 2.2 映射内存大小改变触发SIGBUS信号
- 3. 总结
1. 问题分析
1.1【现象介绍】
SIGBUS是BUS error的缩写,中文称为总线错误信号。它是Unix/Linux系统中的一种异常信号,通常在访问内存时发生某些类型的错误时产生。产生SIGBUS的典型原因包括:
- 地址对齐错误(Alignment Fault):当访问的内存地址不满足硬件的对齐要求时触发。例如在需要4字节对齐的系统上访问一个地址不是4的倍数的int变量。
- 未映射的物理地址访问:试图访问未映射到任何设备的物理地址空间时会触发SIGBUS。这可能是由错误的指针运算或直接访问物理内存导致的。
- 特定于设备的硬件错误:一些硬件相关的错误,如访问未初始化的内存控制器、总线奇偶校验错误等也会触发SIGBUS。
SIGBUS在不同的硬件架构和字长下会有一些差异:
- X86架构:在x86上,SIGBUS主要出现在对齐错误的情况下。x86 CPU允许未对齐的内存访问,但通常会有性能损失。只有使用了SIMD指令(如SSE)时要求更严格的对齐,否则会产生SIGBUS。
- ARM架构:相比x86,ARM CPU通常要求更严格的内存对齐,未对齐访问会直接触发SIGBUS而不只是性能问题。尤其是在ARM64上,对齐要求更高。
- 大/小端序:在一些场景下,访问不同字节序的数据也可能触发SIGBUS。但这个问题通常在编译器层面处理。
SIGBUS和SIGSEGV(Segmentation Fault,段错误)信号有一些相似之处,它们都和非法内存访问有关,有时会被混淆。它们主要区别在于:
-
产生原因:SIGSEGV由访问未映射或者权限不足的虚拟内存地址触发,而SIGBUS则主要由访问映射的物理内存但其他硬件相关错误触发。
-
处理难度:SIGSEGV相对容易判断和修复,通常检查程序的地址映射即可。而SIGBUS可能和具体硬件相关,排查难度大一些。
-
可恢复性:SIGSEGV异常通过修改地址映射表可以在一定程度上恢复执行,而SIGBUS异常通常不可恢复,因为可能对应着物理硬件错误。
需要注意SIGBUS更多的和硬件体系结构相关,不同系统和编译器下可能有细微差异。在编写底层程序时,彻底了解目标平台的内存模型和对齐要求是很有必要的。
现代CPU一般不会因为地址非对齐出现SIGBUS错误,更常见原因是映射的内存地址出现了问题,如SO库被更新、共享内存大小变化、映射的页面属性异常等。
1.2【分析原因】
SIGBUS触发场景常见有如下两种:
(1)文件映射访问异常:进程通过mmap系统调用将文件映射到内存中,建立文件和内存页面之间的映射关系。这种方式可以实现高效的文件IO,避免了繁琐的read/write系统调用。然而,这种映射是基于文件大小的,如果文件大小发生变化,已经映射的内存页面可能会出现问题。
如果进程A对文件进行了mmap,而此时另一个进程B对该文件进行了truncate操作,将文件截断到更小的大小。这时,进程A中已经映射的超出文件实际大小的那部分内存页就处于一种不一致的状态。当进程A试图访问这些无效的内存页时,就会触发SIGBUS信号。
另一个常见场景是,某个进程直接用一个新文件覆盖了旧的动态库或可执行文件。由于系统出于性能考虑,通常采用copy-on-write的策略,并不会立即使已经加载的旧版本失效。只有当进程真正执行到新文件中不存在的部分(例如新版本删减了一些代码)时,才会触发SIGBUS。这种场景在升级更新软件时容易遇到。
(2)访问不对齐的内存:现代处理器对内存的访问通常要求地址按照特定的字节数对齐,例如32位的整数要求地址是4的倍数,64位的整数要求地址是8的倍数。这种要求和硬件的设计有关,可以简化电路并提高访问效率。
在x86平台上,CPU一般允许访问未对齐的内存,但会带来一定的性能损失。而在ARM等RISC架构的处理器上,未对齐访问通常会直接触发异常(ARM64位CPU会自行处理对齐问题,一般不会触发异常)。程序员在编写代码时,尤其是在处理网络数据或磁盘数据时,需要特别注意字节对齐问题。
有趣的是,x86平台也提供了一种机制,可以主动禁止未对齐访问,即通过修改EFLAGS寄存器的AC(Alignment Check)标志位。当设置AC位为1时,CPU会在每次内存访问时检查地址的对齐情况,如果发现未对齐访问就会抛出SIGBUS异常。
这个特性对于调试和测试代码很有帮助。它可以帮助程序员及早发现代码中隐藏的对齐问题,提高软件的可移植性和稳定性。但在生产环境中一般不会启用这个特性,因为它会带来额外的性能开销。
1.3【解决思路】
(1)文件映射访问异常的解决方案:这类问题的根源在于多个进程对同一个文件进行了不同步的操作。因此,解决方案的核心思路是加强进程间的协调和同步。具体可以采取以下措施:
-
文件锁:在对文件进行映射或者修改大小之前,先获取文件锁。这样可以防止其他进程同时修改文件,导致不一致。Linux提供了flock和fcntl两种文件锁机制,可以根据需要选用。
-
避免直接覆盖文件:如果需要更新动态库或可执行文件,不要直接用新文件覆盖旧文件,而是先写入一个新文件,然后用rename系统调用进行原子性的替换。这样可以确保任何时刻文件系统中只存在一个完整的版本。
-
异常处理:在访问mmap的内存时,用try/catch等机制捕获SIGBUS异常,并进行适当的错误处理,如重新加载文件、通知用户等,提高程序的健壮性。
-
定期检查文件大小:如果进程长时间持有一个mmap,可以定期检查文件的实际大小是否发生变化,如果变小了就及时解除不一致的映射,重新进行mmap。
(2)访问不对齐内存的解决方案:对于这类问题,首要原则是尽量避免在代码中产生非对齐访问。具体措施包括:
-
使用对齐的数据结构:在定义结构体时,确保每个字段都按照其大小要求进行对齐。可以使用编译器提供的对齐属性,如GNU C的
__attribute__((aligned(n)))
。 -
内存分配时考虑对齐:使用malloc、mmap等分配内存时,确保返回的内存地址是对齐的。可以使用posix_memalign、aligned_alloc等对齐版本的内存分配函数。
-
网络/磁盘IO时注意对齐:在处理网络数据或磁盘数据时,注意数据的边界可能不对齐。需要使用memcpy等函数进行中间缓冲,不要直接将IO缓冲区强制转换为结构体指针。
-
使用对齐版本的内存访问指令:现代CPU提供了一些对齐版本的内存访问指令,如movaps、lddqu等。在进行大块数据操作时,使用这些指令可以避免对齐异常。
-
必要时进行填充:如果某些数据结构无法完美对齐,可以在末尾添加填充字节,使其对齐。虽然这会浪费一些内存空间,但可以避免更严重的异常。
2. 实例验证
2.1 对齐问题触发SIGBUS信号
测试代码如下:
/*
* SPDX-License-Identifier: BSD-3-Clause
*
* Copyright (c) 2025 Once Day <once_day@qq.com>, All rights reserved.
*
* @FilePath: /tools/sigbus_alignment.c
* @Author: Once Day <once_day@qq.com>.
* @Date: 2025-03-17 21:42
* @info: Encoder=utf-8,Tabsize=4,Eol=\n.
*
* @Description:
* 测试SIGBUS信号
*
*/
#include <stdint.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
unsigned long int original_eflags;
int temp_value;
void sigbus_handler(int sig)
{
printf("Caught SIGBUS: Alignment Fault!\n");
// 恢复原始的EFLAGS值
__asm__ volatile("push %0 \n\t popf" : : "r"(original_eflags));
exit(1);
}
int main()
{
// 安装SIGBUS信号处理器
signal(SIGBUS, sigbus_handler);
// 分配一个字符数组(确保未对齐)
char data[64] = {0};
// 将字符数组的起始地址+3转换为int指针
int *misaligned = (int *)((((uint64_t)(data + 8)) & ~0x7) + 1);
// 手动设置CPU对齐检查标识
__asm__ volatile("pushf \n\t pop %0" : "=r"(original_eflags));
// 打印原始EFLAGS值
printf("Original EFLAGS: 0x%lx\n", original_eflags);
// 打印对齐检查位
printf("Alignment Check Bit: %s\n", (original_eflags & 0x40000) ? "Enabled" : "Disabled");
printf("Misaligned pointer: %p\n", misaligned);
// 设置对齐检查位
__asm__ volatile("pushf \n\t orl $0x40000, (%rsp) \n\t popf");
// 尝试通过misaligned指针写入数据
*misaligned = 0x12345678;
// 如果没有触发SIGBUS,这行代码会执行
printf("No SIGBUS, data written: %d\n", temp_value);
// 恢复原始的EFLAGS值
__asm__ volatile("push %0 \n\t popf" : : "r"(original_eflags));
return temp_value;
}
这段 C 语言代码的主要目的是演示在 x86 架构下处理未对齐内存访问时触发的SIGBUS
信号。具体步骤如下:
- 包含必要的头文件,并定义全局变量用于存储原始的 EFLAGS 寄存器值和一个临时变量。
- 编写信号处理函数,当捕获到
SIGBUS
信号时,输出提示信息,恢复原始 EFLAGS 值,然后终止程序。 - 在
main
函数中,首先安装SIGBUS
信号处理器,接着准备一个未对齐的内存地址。 - 保存并打印原始的 EFLAGS 值和对齐检查位的状态,然后设置对齐检查位。
- 尝试通过未对齐的指针写入数据,若触发
SIGBUS
信号则进入信号处理函数;若未触发,则输出提示信息。 - 最后恢复原始的 EFLAGS 值,并返回临时变量。
编译执行,可以出现SIGBUS信号,解决也很简单,只有将地址对齐即可:
ubuntu->tools:$ gcc sigbus_alignment.c -O0 -o sigbus_alignment.out
ubuntu->tools:$ ./sigbus_alignment.out
Original EFLAGS: 0x206
Alignment Check Bit: Disabled
Misaligned pointer: 0x7ffc32e491f9
Caught SIGBUS: Alignment Fault!
2.2 映射内存大小改变触发SIGBUS信号
测试代码如下:
/*
* SPDX-License-Identifier: BSD-3-Clause
*
* Copyright (c) 2025 Once Day <once_day@qq.com>, All rights reserved.
*
* @FilePath: /tools/sigbus_mmap.c
* @Author: Once Day <once_day@qq.com>.
* @Date: 2025-03-17 22:16
* @info: Encoder=utf-8,Tabsize=4,Eol=\n.
*
* @Description:
* 测试SIGBUS信号
*
*/
#define _GNU_SOURCE
#define __USE_GNU
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <setjmp.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>
static uint64_t mapped_size = 8192;
static uint64_t mapped_size_latest = 4096;
static char *mapped_mem;
static int mapped_fd;
// 使用signal longjmp跳转到sigbus_handler
static sigjmp_buf jmpbuf;
void sigbus_handler(int sig)
{
printf("Caught SIGBUS: Invalid memory access!\n");
// 首先取消原有内存映射
munmap(mapped_mem, mapped_size);
// 重新映射内存
mapped_mem =
mmap(mapped_mem, mapped_size_latest, PROT_READ | PROT_WRITE, MAP_SHARED, mapped_fd, 0);
if (mapped_mem == MAP_FAILED) {
perror("mmap");
exit(1);
}
printf("Remapped address: %p\n", mapped_mem);
// 继续执行
siglongjmp(jmpbuf, 1);
}
int main()
{
// 安装SIGBUS信号处理器
signal(SIGBUS, sigbus_handler);
// 创建一个共享内存文件映射
mapped_fd = shm_open("test-sigbus", O_CREAT | O_RDWR, 0666);
if (mapped_fd == -1) {
perror("shm_open");
exit(1);
}
// 删除共享内存文件(确保下次重新映射)
shm_unlink("test-sigbus");
// 设置文件大小
ftruncate(mapped_fd, mapped_size);
// 映射共享内存
mapped_mem = mmap(NULL, mapped_size, PROT_READ | PROT_WRITE, MAP_SHARED, mapped_fd, 0);
printf("Mapped address: %p\n", mapped_mem);
// 写入数据
for (int i = 0; i < mapped_size; i++) {
mapped_mem[i] = 'A' + i % 26;
}
printf("Data written\n");
// 重新设置文件大小
printf("Truncated file size\n");
ftruncate(mapped_fd, mapped_size_latest);
// 尝试访问超出映射内存的地址
printf("Accessing out-of-bound memory...\n");
// 设置长跳转点
if (sigsetjmp(jmpbuf, 1) == 0) {
// 尝试访问超出映射内存的地址
mapped_mem[mapped_size_latest] = 0;
} else {
// 如果发生SIGBUS,长跳转到这里
printf("Check mapped file length has been changed\n");
printf("Read data under new size: %c\n", mapped_mem[mapped_size_latest - 1]);
}
// 如果没有触发SIGBUS,这行代码会执行
printf("End of program\n");
return 0;
}
这段 C 语言代码的主要目的是测试SIGBUS
信号,模拟内存映射文件大小改变后,访问超出新文件大小的内存区域触发SIGBUS
信号的情况,并对该信号进行处理。具体步骤如下:
- 头文件和全局变量:包含必要的系统头文件,定义全局变量用于记录映射内存的大小、文件描述符和映射内存地址,同时使用
sigjmp_buf
用于长跳转。 - 信号处理函数:定义
sigbus_handler
函数,当捕获到SIGBUS
信号时,先取消原有内存映射,再重新映射内存,若映射失败则输出错误信息并退出程序。最后使用siglongjmp
跳转到之前设置的跳转点继续执行。 main
函数,安装SIGBUS
信号处理器,创建一个共享内存文件映射,删除该文件以确保下次重新映射,设置文件大小并进行内存映射。向映射内存写入数据。重新设置文件大小,尝试访问超出新文件大小的内存地址。使用sigsetjmp
设置长跳转点,若触发SIGBUS
信号,跳转到相应代码块,输出检查信息并读取新大小下的最后一个字符。若未触发SIGBUS
信号,输出程序结束信息并返回。
编译执行,可以出现SIGBUS信号,解决方法可以是重新映射共享内存,并且同步修改访问内存的变量值。
ubuntu->tools:$ gcc sigbus_mmap.c -O0 -g -o sigbus_mmap.out
ubuntu->tools:$ ./sigbus_mmap.out
Mapped address: 0x7f7738469000
Data written
Truncated file size
Accessing out-of-bound memory...
Caught SIGBUS: Invalid memory access!
Remapped address: 0x7f7738469000
Check mapped file length has been changed
Read data under new size: N
End of program
3. 总结
一般而已,对齐问题主要影响性能,所以在面对热点代码时,需要保证核心数据结构对齐,特别是对于原子操作,需要尽可能在一个cache line内。
对于文件映射内存后,文件大小发生改变,这种情况下需要重新映射共享内存,直接重启应用是非常不错的选项,如果无法重启应用,可尝试在信号处理函数里面重新映射共享内存,并且修改相关的变量信息。
对于存在异常捕获的编程语言,如C++和Python,这一步很好操作,但是对于C语言,需要借助setjmp来完成退栈操作。