Linux内核啥时候出发SIGBUS

在 Linux 系统中,SIGBUS 信号(总线错误)通常由内核在以下场景触发:


1. 内存映射文件被截断(Truncated Memory-Mapped File)

  • 当程序通过 mmap 将文件映射到内存后,如果文件被其他进程截断(例如 truncateftruncate),程序访问被截断部分对应的内存区域时,会触发 SIGBUS
  • 示例
    1
    2
    3
    4
    int fd = open("file.txt", O_RDWR);
    void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    // 另一个进程执行:truncate("file.txt", 2048); // 截断文件
    // 访问 addr + 2048 之后的地址会触发 SIGBUS

2. 未对齐的内存访问(Unaligned Memory Access)

  • 在某些架构(如 ARM、SPARC、MIPS 等 RISC CPU)中,访问未对齐的内存地址(例如尝试从非 4 字节对齐的地址读取 int 类型数据)会触发 SIGBUS
  • 注意:x86/x86_64 架构对未对齐访问更宽容(通常不会触发 SIGBUS,但可能有性能损失)。

3. 访问物理无效的内存区域

  • 尝试访问物理上不存在的地址(例如通过 /dev/mem 直接访问不存在的硬件寄存器地址)。
  • 内存映射的硬件设备被移除(如热插拔设备)。

4. 内存页错误(Page Fault)的特殊情况

  • 当内核无法处理某个内存页错误时(例如访问的页在交换空间但已被释放),可能触发 SIGBUS 而非 SIGSEGV

5. 用户态访问内核保留内存

  • 尝试通过 mmap 或其他方式访问内核保留的内存区域(如 vmalloc 区域),可能触发 SIGBUS

6. 硬件错误(罕见)

  • 内存硬件故障(如 ECC 校验错误)可能通过 SIGBUS 通知用户态进程。

如何调试 SIGBUS?

  1. **使用 gdb**:捕获信号并检查程序崩溃时的上下文。
  2. 检查内存操作:确保所有指针操作、内存对齐符合架构要求。
  3. 验证文件映射:确保 mmap 映射的文件未被意外修改。

SIGSEGV 的区别

  • SIGSEGV(段错误):访问逻辑上无效的地址(如野指针、已释放内存)。
  • SIGBUS:访问物理上无效的地址或违反硬件对齐规则。

理解具体场景需要结合代码、架构和系统行为分析。

Linux ACPI支持多段地址的配置示例

在 Linux 系统中,ACPI(Advanced Configuration and Power Interface)通过 DSDT(Differentiated System Description Table)或 SSDT(Secondary System Description Table)描述硬件资源。当设备需要配置多个地址段(如多个内存区域或 I/O 端口范围)时,可以通过 ACPI 表中的 ResourceTemplate 定义多段地址。以下是一个示例和详细说明:


示例:ACPI 表中定义多段地址

假设某个硬件设备需要配置 两段内存地址一段 I/O 端口范围,在 ACPI 表中可以按如下方式描述:

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
Device (DEV0) {
Name (_HID, "XYZ12345") // 设备硬件 ID
Method (_CRS, 0x0, NotSerialized) { // 返回当前资源设置
Name (RBUF, ResourceTemplate() {
// 第一段内存:32 位固定地址(不可缓存)
Memory32Fixed (ReadWrite, 0xFE000000, 0x1000) // 地址 0xFE000000,长度 0x1000
// 第二段内存:64 位动态地址(可预取)
QWordMemory (
ResourceConsumer, // 资源用途
ReadWrite, // 读写权限
MinNotFixed, // 最小地址不固定
MaxNotFixed, // 最大地址不固定
Cacheable, // 可缓存
Prefetchable, // 可预取
0x0000000000000000, // 最小地址
0xFFFFFFFFFFFFFFFF, // 最大地址
0x00000000A0000000, // 地址对齐(4KB)
0x0000000000002000, // 地址长度(8KB)
,, , AddressRangeMemory,
)
// I/O 端口范围
IO (
Decode16, // 16 位解码
0x1000, // 起始端口
0x1000, // 结束端口
0x01, // 端口对齐(1字节)
0x10 // 端口长度(16字节)
)
})
Return (RBUF)
}
}

关键资源描述符说明

  1. **Memory32Fixed**:

    • 描述 32 位固定地址 的内存区域。
    • 参数:(权限, 基地址, 长度)
    • 示例:Memory32Fixed (ReadWrite, 0xFE000000, 0x1000)
  2. **QWordMemory**:

    • 描述 64 位动态地址 的内存区域,支持可缓存、可预取等属性。
    • 参数:(资源用途, 权限, 地址范围限制, 对齐, 长度, 类型)
    • 示例中指定了长度为 0x2000(8KB),地址对齐为 0xA0000000(4KB)。
  3. **IO**:

    • 描述 I/O 端口范围
    • 参数:(解码方式, 起始端口, 结束端口, 对齐, 长度)
    • 示例:16 位端口范围 0x1000-0x100F

Linux 驱动中解析多段地址

在 Linux 设备驱动中,可以通过 ACPI API 解析这些资源:

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
37
#include <linux/acpi.h>

static int my_driver_probe(struct platform_device *pdev) {
struct resource *res;
int i;

// 遍历所有资源
for (i = 0; i < pdev->num_resources; i++) {
res = &pdev->resource[i];

if (resource_type(res) == IORESOURCE_MEM) {
// 处理内存资源
dev_info(&pdev->dev, "Memory region: start=0x%llx, len=0x%llx\n",
res->start, resource_size(res));
} else if (resource_type(res) == IORESOURCE_IO) {
// 处理 I/O 资源
dev_info(&pdev->dev, "I/O ports: start=0x%llx, len=0x%llx\n",
res->start, resource_size(res));
}
}
return 0;
}

static const struct acpi_device_id my_driver_acpi_match[] = {
{ "XYZ12345", 0 }, // 匹配 ACPI 设备 ID
{ }
};
MODULE_DEVICE_TABLE(acpi, my_driver_acpi_match);

static struct platform_driver my_driver = {
.driver = {
.name = "my_driver",
.acpi_match_table = my_driver_acpi_match,
},
.probe = my_driver_probe,
};
module_platform_driver(my_driver);

注意事项

  1. 地址对齐

    • 内存地址需要按硬件要求对齐(如 4KB 对齐)。
    • ACPI 中的 Memory32FixedQWordMemory 需指定正确的 Alignment
  2. 资源冲突

    • 确保地址段不与其他设备冲突,可通过 cat /proc/iomemcat /proc/ioports 检查。
  3. 64 位支持

    • 使用 QWordMemory 描述 64 位地址时,需确保内核和硬件支持。
  4. ACPI 表编译

    • 修改 DSDT/SSDT 后需用 iasl 工具编译为 AML 文件,并通过引导加载程序(如 GRUB)加载。

通过上述示例,可以在 ACPI 中定义多段地址并在 Linux 驱动中正确解析它们。实际应用中需结合硬件手册调整地址范围和属性。