SIGBUS (7) 总线错误详解:C++ 开发者的崩溃调试指南

一、信号基础认知(开篇 5 分钟入门)

信号核心信息

  • 信号编号:7
  • 信号名称:SIGBUS (Bus Error)
  • POSIX 标准:是(POSIX.1-2001 定义)
  • 可捕获:是
  • 默认行为:终止进程并生成 coredump

核心定位

SIGBUS 的本质作用是内存访问权限不足告警。与 SIGSEGV(地址无效)不同,SIGBUS 表示访问的地址是有效的,但由于对齐错误、访问只读内存、或访问了无效的内存区域等原因,无法完成访问。

默认行为

Linux 内核的默认处理逻辑:

  • 终止进程:立即终止当前进程
  • 生成 coredump:如果系统配置允许,会生成 core 文件
  • 可捕获:可以捕获,但通常应该让程序终止

与 C++ 的关联性

SIGBUS 在 C++ 开发中的高发场景:

  1. 内存对齐错误:在某些架构(如 SPARC、某些 ARM 配置)上访问未对齐的内存
  2. 只读内存写入:尝试写入只读内存区域(如代码段、常量数据)
  3. 内存映射问题:访问了无效的内存映射区域
  4. 多线程同步:数据竞争导致的内存损坏
  5. 硬件相关:某些硬件限制导致的内存访问问题

二、信号触发场景(结合 C++ 代码实例)

核心触发原因

1. 编程失误类

场景 1.1:内存对齐错误(SPARC/某些 ARM)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 错误代码:在某些架构上访问未对齐的内存
#include <iostream>
#include <cstdlib>

int main() {
// 分配内存
char* buffer = (char*)malloc(100);

// 创建未对齐的 int 指针(地址不是 4 的倍数)
int* unaligned_ptr = (int*)(buffer + 1); // 未对齐

// 在某些架构上(如 SPARC),这会触发 SIGBUS
*unaligned_ptr = 42; // 可能触发 SIGBUS

free(buffer);
return 0;
}

Coredump 信息示例

1
2
3
4
5
6
7
8
Program received signal SIGBUS, Bus error.
0x0000000000401123 in main () at main.cpp:12
12 *unaligned_ptr = 42;
(gdb) bt
#0 0x0000000000401123 in main () at main.cpp:12
#1 0x00007ffff7e3d0b7 in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6
(gdb) print/x unaligned_ptr
$1 = 0x5555555592a1 # 地址不是 4 的倍数(最后一位是 1)

注意:在 x86/x86_64 架构上,CPU 通常会自动处理对齐问题(性能可能下降),所以可能不会触发 SIGBUS。

场景 1.2:写入只读内存
1
2
3
4
5
6
7
8
9
10
11
12
// 错误代码:尝试写入只读内存
#include <iostream>

int main() {
// 字符串字面量存储在只读内存
char* str = "Hello, World!"; // 指向只读内存

// 尝试修改只读内存,可能触发 SIGBUS
str[0] = 'h'; // 可能触发 SIGBUS 或 SIGSEGV

return 0;
}

Coredump 信息示例

1
2
3
4
5
Program received signal SIGBUS, Bus error.
0x0000000000401123 in main () at main.cpp:8
8 str[0] = 'h';
(gdb) info proc mappings
# 可以看到字符串所在的内存区域是只读的
场景 1.3:访问无效的内存映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 错误代码:访问了无效的内存映射区域
#include <iostream>
#include <sys/mman.h>
#include <unistd.h>

int main() {
// 创建内存映射
size_t page_size = getpagesize();
void* addr = mmap(nullptr, page_size, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

if (addr == MAP_FAILED) {
std::cerr << "mmap failed" << std::endl;
return 1;
}

// 尝试写入只读映射的内存,可能触发 SIGBUS
int* ptr = (int*)addr;
*ptr = 42; // 映射是 PROT_READ,写入可能触发 SIGBUS

munmap(addr, page_size);
return 0;
}

2. 系统限制类

场景 2.1:访问内核空间(某些系统)
1
2
3
4
5
6
7
8
9
10
// 错误代码:尝试访问内核空间
#include <iostream>

int main() {
// 尝试访问内核空间地址(通常会被阻止)
volatile int* kernel_space = (volatile int*)0xFFFFFFFF;
*kernel_space = 42; // 可能触发 SIGBUS 或 SIGSEGV

return 0;
}

3. 运行时异常类

场景 3.1:内存映射文件被截断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 错误代码:内存映射的文件被截断
#include <iostream>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
// 创建文件并映射
int fd = open("test.dat", O_CREAT | O_RDWR, 0644);
ftruncate(fd, 4096);

void* addr = mmap(nullptr, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

// 在另一个进程或线程中截断文件
// ftruncate(fd, 0); // 在其他地方执行

// 访问被截断的映射区域,可能触发 SIGBUS
int* ptr = (int*)addr;
*ptr = 42; // 如果文件被截断,可能触发 SIGBUS

munmap(addr, 4096);
close(fd);
return 0;
}

易混淆场景辨析

SIGBUS vs SIGSEGV

SIGBUS:地址有效但访问权限不足(对齐错误、只读内存写入等)

1
2
char* str = "readonly";
str[0] = 'R'; // SIGBUS:尝试写入只读内存

SIGSEGV:地址无效(访问了不属于进程的内存空间)

1
2
int* ptr = nullptr;
*ptr = 42; // SIGSEGV:地址 0x0 无效

SIGBUS vs SIGILL

  • SIGBUS:指令有效,但内存访问有问题(对齐、权限等)
  • SIGILL:指令本身非法(CPU 不认识这条指令)

三、崩溃调试与定位(实操步骤)

基础定位:core 文件 + gdb 调试

步骤 1:开启 core 文件

1
ulimit -c unlimited

步骤 2:gdb 加载 core 文件

1
2
3
g++ -g -O0 -o program main.cpp
./program # 会崩溃并生成 core
gdb ./program core

步骤 3:关键调试命令

1
2
3
4
5
(gdb) bt                    # 查看调用栈
(gdb) print/x variable # 以 16 进制打印变量(查看地址对齐)
(gdb) info proc mappings # 查看内存映射(确认内存权限)
(gdb) x/10x address # 查看内存内容
(gdb) info registers # 查看寄存器

实际调试示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ gdb ./sigbus_example core
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
...
Core was generated by `./sigbus_example'.
Program terminated with signal SIGBUS, Bus error.
#0 0x0000000000401123 in main () at main.cpp:12
12 *unaligned_ptr = 42;

(gdb) bt
#0 0x0000000000401123 in main () at main.cpp:12
#1 0x00007ffff7e3d0b7 in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6

(gdb) print/x unaligned_ptr
$1 = 0x5555555592a1 # 地址不是 4 的倍数

(gdb) print/x (unsigned long)unaligned_ptr % 4
$2 = 0x1 # 地址对 4 取模为 1,未对齐

(gdb) info proc mappings
# 查看内存映射,确认内存权限

进阶工具

工具 1:使用 Address Sanitizer

1
2
3
# 编译时启用 ASan
g++ -g -fsanitize=address -fno-omit-frame-pointer -o program main.cpp
./program

ASan 输出示例

1
2
3
4
==12345==ERROR: AddressSanitizer: BUS on unknown address
==12345==The signal is caused by a WRITE memory access.
#0 0x401123 in main main.cpp:12
#1 0x7f8b8c5d0b97 in __libc_start_main

工具 2:使用 valgrind

1
valgrind --tool=memcheck ./program

工具 3:查看内存映射

1
2
3
4
5
# 查看进程的内存映射
cat /proc/$(pidof program)/maps

# 或使用 pmap
pmap $(pidof program)

定位关键点

SIGBUS 崩溃的核心排查方向:

  1. 检查内存对齐:确认访问的地址是否对齐(特别是在 SPARC/ARM 上)
  2. 检查内存权限:确认尝试写入的内存是否可写
  3. 检查内存映射:确认内存映射是否有效
  4. 检查文件映射:确认映射的文件是否被截断
  5. 检查架构差异:某些问题只在特定架构上出现

四、崩溃修复方案(针对性解决)

分场景修复代码

场景 1:内存对齐错误

快速修复:确保对齐
1
2
3
4
5
6
7
8
9
10
// 修复前
char* buffer = (char*)malloc(100);
int* unaligned_ptr = (int*)(buffer + 1); // 未对齐
*unaligned_ptr = 42; // 可能触发 SIGBUS

// 快速修复
char* buffer = (char*)malloc(100);
// 确保地址对齐到 4 字节边界
int* aligned_ptr = (int*)((char*)buffer + (4 - ((uintptr_t)buffer % 4)) % 4);
*aligned_ptr = 42;
优雅修复:使用对齐的内存分配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 优雅修复方案 1:使用对齐的内存分配(C++11)
#include <cstdlib>
#include <cstdint>

void* aligned_malloc(size_t size, size_t alignment) {
void* ptr = nullptr;
posix_memalign(&ptr, alignment, size);
return ptr;
}

int main() {
// 分配对齐到 16 字节的内存
int* aligned_ptr = (int*)aligned_malloc(100, 16);
*aligned_ptr = 42;
free(aligned_ptr);
return 0;
}

// 优雅修复方案 2:使用 C++17 的 std::aligned_storage
#include <type_traits>

alignas(16) std::aligned_storage<sizeof(int), alignof(int)>::type storage;
int* ptr = reinterpret_cast<int*>(&storage);
*ptr = 42;

场景 2:写入只读内存

快速修复:使用可写内存
1
2
3
4
5
6
7
// 修复前
char* str = "Hello, World!"; // 只读内存
str[0] = 'h'; // 触发 SIGBUS

// 快速修复
char str[] = "Hello, World!"; // 栈上的可写数组
str[0] = 'h'; // 可以修改
优雅修复:使用 std::string
1
2
3
4
5
// 优雅修复:使用 std::string
#include <string>

std::string str = "Hello, World!"; // 可修改
str[0] = 'h'; // 可以修改

场景 3:内存映射权限错误

快速修复:使用正确的权限
1
2
3
4
5
6
7
8
9
// 修复前
void* addr = mmap(nullptr, page_size, PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
int* ptr = (int*)addr;
*ptr = 42; // 只读映射,写入触发 SIGBUS

// 快速修复
void* addr = mmap(nullptr, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
int* ptr = (int*)addr;
*ptr = 42; // 可写映射
优雅修复:使用 RAII 管理内存映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 优雅修复:使用 RAII 类管理内存映射
#include <sys/mman.h>
#include <stdexcept>

class MemoryMapping {
private:
void* addr_;
size_t size_;

public:
MemoryMapping(size_t size, int prot, int flags) : size_(size) {
addr_ = mmap(nullptr, size, prot, flags, -1, 0);
if (addr_ == MAP_FAILED) {
throw std::runtime_error("mmap failed");
}
}

~MemoryMapping() {
if (addr_) {
munmap(addr_, size_);
}
}

void* get() { return addr_; }

// 禁止拷贝
MemoryMapping(const MemoryMapping&) = delete;
MemoryMapping& operator=(const MemoryMapping&) = delete;
};

int main() {
MemoryMapping mapping(4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS);
int* ptr = (int*)mapping.get();
*ptr = 42; // 安全访问
return 0;
}

修复验证

单元测试

1
2
3
4
5
6
7
8
9
10
11
12
#include <gtest/gtest.h>

TEST(AlignmentTest, AlignedAccess) {
alignas(16) char buffer[100];
int* ptr = (int*)buffer;
EXPECT_NO_THROW(*ptr = 42);
}

TEST(MemoryTest, WritableMemory) {
char str[] = "test";
EXPECT_NO_THROW(str[0] = 'T');
}

避坑提醒

  1. 注意架构差异:对齐问题在 x86/x86_64 上可能不会触发 SIGBUS,但在 SPARC/ARM 上会
  2. 字符串字面量是只读的:使用 char[] 而非 char* 指向字符串字面量
  3. 检查内存映射权限:确保映射的内存有正确的权限
  4. 处理文件截断:如果使用文件映射,处理文件被截断的情况

五、长期预防策略(从编码到部署全链路)

编码规范

C++ 开发中规避 SIGBUS 的编码习惯:

  1. 使用对齐的内存分配

    1
    alignas(16) int data[100];  // 对齐到 16 字节
  2. 使用 std::string 而非字符串字面量

    1
    std::string str = "Hello";  // 而非 char* str = "Hello";
  3. 检查内存映射权限

    1
    void* addr = mmap(..., PROT_READ | PROT_WRITE, ...);  // 确保有写权限
  4. 使用标准库容器

    1
    std::vector<int> vec;  // 自动处理对齐

编译阶段

开启防御性编译选项:

1
2
3
4
5
# 启用警告和检查
g++ -g -O2 -Wall -Wextra \
-Wcast-align \
-fstack-protector-strong \
-o program main.cpp
  • -Wcast-align:警告未对齐的指针转换

测试策略

1
2
3
4
5
6
7
// 测试对齐访问
TEST(AlignmentTest, VariousAlignments) {
for (int align = 1; align <= 16; align *= 2) {
alignas(align) char buffer[100];
// 测试访问
}
}

线上监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <signal.h>
#include <execinfo.h>
#include <iostream>

void busHandler(int sig) {
void* array[10];
size_t size = backtrace(array, 10);

std::cerr << "SIGBUS caught! Stack trace:" << std::endl;
backtrace_symbols_fd(array, size, STDERR_FILENO);

// 记录详细信息
// logToFile("SIGBUS occurred", array, size);

exit(1);
}

int main() {
signal(SIGBUS, busHandler);
// 程序代码
return 0;
}

六、拓展延伸(加深理解)

相关信号对比

SIGBUS vs SIGSEGV vs SIGILL

特性 SIGBUS SIGSEGV SIGILL
触发原因 地址有效但访问权限不足 地址无效 非法指令
常见场景 对齐错误、只读内存写入 空指针、越界 非法指令
架构依赖 是(SPARC/ARM 更严格)

进阶技巧:检测内存对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <cstdint>
#include <iostream>

template<typename T>
bool isAligned(const void* ptr) {
return (reinterpret_cast<uintptr_t>(ptr) % alignof(T)) == 0;
}

int main() {
char buffer[100];
int* ptr = (int*)(buffer + 1);

if (!isAligned<int>(ptr)) {
std::cerr << "Pointer is not aligned!" << std::endl;
// 修复对齐
}

return 0;
}

实际案例分享

案例:字符串字面量修改导致的崩溃

问题描述:程序在修改字符串时崩溃,coredump 显示 SIGBUS。

排查过程

  1. GDB 分析显示崩溃在字符串修改
  2. 检查代码,发现使用 char* 指向字符串字面量
  3. 确认字符串字面量存储在只读内存

根本原因

1
2
3
// 问题代码
char* str = "Hello, World!"; // 指向只读内存
str[0] = 'h'; // 尝试写入只读内存,触发 SIGBUS

解决方案

1
2
3
4
5
6
7
// 修复代码
char str[] = "Hello, World!"; // 栈上的可写数组
str[0] = 'h'; // 可以修改

// 或使用 std::string
std::string str = "Hello, World!";
str[0] = 'h';

总结

SIGBUS 是总线错误信号,主要原因是内存对齐错误或访问权限不足。通过:

  1. 确保内存对齐:特别是在严格对齐要求的架构上
  2. 使用可写内存:避免修改只读内存
  3. 检查内存权限:确保内存映射有正确的权限
  4. 调试技巧:使用 GDB 检查地址对齐和内存映射
  5. 预防策略:编码规范、编译选项、测试覆盖

可以有效减少 SIGBUS 崩溃,提高程序稳定性。