非对齐内存导致Bus Error

橘子 发布于 2025-01-12 555 次阅读


这是C++中一个不容易碰到的问题。从程序员debug的视角来看,它甚至可以归类为“玄学问题”——就和缓存导致的编译错误一样,这类问题和程序员所编写的代码关系较低,所以难以察觉问题所在。

关于系统架构的概念,可以参考这篇文章:

对齐内存

什么是对齐内存?回顾一下自己的电脑位数,你会想起有32位操作系统和64位操作系统。32位操作系统的操作单元是4字节,64位操作系统则是8字节;操作系统的位数实际上和CPU强绑定。例如目前最常见的64位CPU的寄存器就是8字节的,每次操作也以8字节为单位:如果需要计算两个int32相加,实际上操作时可能是两个8字节整数相加、得到一个8字节的结果、然后取出4字节的部分作为最终结果(更加具体的操作和使用的指令集是相关的)。

换句话说,CPU希望自己操作的数据是尽可能向位数对齐的。64位操作系统会兼容32位操作,因此一般来说内存至少会倾向于向4位字节对齐。

请看如下例子,你可能会发现他们干的是相同的事情——真的如此吗?

struct ExampleA{
    char var1;
    int var2;
};

在这个例子中,你觉得ExampleA占几个字节?你可能会下意识回答:5字节。一个char占据1字节,一个int占据4字节,一切看起来都非常自然,并且在日常编程中类似的结构数不胜数。然而这就是问题所在:它的内存不对齐!假设var1在内存占据的地址是0x0,则var2占据的地址则是0x1~4,不对齐!这意味一个32位操作系统可能会先取出0x0~3的四字节,处理低位的0x1~3,然后取出0x4~7的四字节,处理高位的0x4. 然而,像这种指向非对齐地址的指针可能在嵌入式系统(例如一些arm架构的机器)上压根不能正常运行,所以它在处理低位的0x1~3时就直接崩溃了,而x86系统则可能可以顺利处理这种问题。

struct ExampleB{
    int var2;
    char var1;
};

在ExampleB的例子中,把var1var2的顺序调换了,内存就对齐了!现在,处理var2时,会直接取出0x1~4,是对齐的四字节。

struct __attribute__((aligned(4))) ExampleC{
    char var1;
    int var2;
};

在ExampleC的例子中,var1var2的顺序与例A相比没变,但是显式的指定了向四字节对齐。这会强制 ExampleC对齐到4字节边界。请注意,在不同的编译器中,指定的方法不同

  • GCC/Clang 支持 __attribute__((aligned(x)))
  • MSVC 支持 __declspec(align(x))

实际上,在大多数 32 位或 64 位系统中,int 类型需要对齐到4字节地址边界。在这种情况下,编译器通常会在ExampleA的 char var1int var2 之间添加3字节的填充(padding),以确保 var2 对齐。但是就像本文提及的,在一些涉及到嵌入式系统的项目需要交叉编译,如果没有正确处理内存对齐,在某些硬件(如某些嵌入式系统或 RISC 架构)上直接报错,比如出现Bus Error。

解决对齐问题

手动调整顺序

正如在ExampleB中看到的,合理的调整结构体或类的成员变量顺序,能够一定程度上避免内存对齐问题。

alignas

使用对齐修饰符 (alignasaligned). C++ 提供了 alignas 关键字,可以显式设置对齐方式。就像在ExampleC中展示过的,它能显式指定对齐到4字节。当然,它可能会导致更多的内存占用,因为var1var2之间填充了3字节。

pragma pack

使用 # pragma pack 来改变结构体的对齐方式。例如:

#pragma pack(1)  // 设置为1字节对齐
struct ExampleD {
    char var1;
    int var2;
};
#pragma pack()   // 恢复默认对齐

这会节省内存空间,结构占据5字节。这也是分析ExampleA时遇到的情况。但可能导致性能下降,因为 var2 的读取需要多次内存访问:先取出0x0~3的四字节,处理低位的0x1~3,然后取出0x4~7的四字节,处理高位的0x4. 在某些硬件或平台上,非对齐访问可能导致程序崩溃(如 Bus Error)。

#pragma pack(4)  // 设置为4字节对齐
struct ExampleE {
    char var1;
    int var2;
};
#pragma pack()   // 恢复默认对齐

ExampleE强制指定了对齐到4字节,和使用了aligned(4)指定的效果类似。

此外,还可以全局指定参数:

#if defined(__ARM_ARCH)
    #pragma pack(4)
#endif

在这段代码中,如果是ARM架构,则会指定使用4字节对齐。

编译器优化

根据GPT所说,大多数现代编译器(如 GCC、Clang)会自动调整结构体布局以实现最佳对齐。确保未手动关闭此功能。例如,在 GCC 中,使用 -fpack-struct 来控制对齐。

实验

写一个简单的代码来输出相关结构的大小,证明理论。

这个是在MacOS上的运行结果,可以看到是编译器是默认使用四字节对齐的。

# OS uname -a
Darwin MacBook-Air.local 22.6.0 Darwin Kernel Version 22.6.0: Wed Jul  5 22:17:35 PDT 2023; root:xnu-8796.141.3~6/RELEASE_ARM64_T8112 arm64
# g++ --version
Apple clang version 15.0.0 (clang-1500.0.40.1)
Target: arm64-apple-darwin22.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

Size of ExampleA: 8, Offset of var1 (char): 0, Offset of var2 (int): 4
Size of ExampleB: 8, Offset of var1 (char): 4, Offset of var2 (int): 0
Size of ExampleC: 8, Offset of var1 (char): 0, Offset of var2 (int): 4
Size of ExampleD: 5, Offset of var1 (char): 0, Offset of var2 (int): 1
Size of ExampleE: 8, Offset of var1 (char): 0, Offset of var2 (int): 4

这个是在一台Ubuntu 22.04运行的结果,可以看到编译器也是默认使用四字节对齐的。

# OS uname -a
Linux VM-8-16-ubuntu 5.15.0-106-generic #116-Ubuntu SMP Wed Apr 17 09:17:56 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
# g++ --version
g++ (Ubuntu 11.2.0-19ubuntu1) 11.2.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Size of ExampleA: 8, Offset of var1 (char): 0, Offset of var2 (int): 4
Size of ExampleB: 8, Offset of var1 (char): 4, Offset of var2 (int): 0
Size of ExampleC: 8, Offset of var1 (char): 0, Offset of var2 (int): 4
Size of ExampleD: 5, Offset of var1 (char): 0, Offset of var2 (int): 1
Size of ExampleE: 8, Offset of var1 (char): 0, Offset of var2 (int): 4

这是在一台嵌入式设备上。

# OS uname -a
Linux CMI010723050541 4.1.15-rt18+ #29 PREEMPT RT Mon Apr 25 11:37:25 CST 2022 armv7l GNU/Linux
# cross compilation g++ version
arm-none-linux-gnueabihf-g++ (Arm GNU Toolchain 13.3.Rel1 (Build arm-13.24)) 13.3.1 20240614
Copyright © 2023 Free Software Foundation, Inc.
本程序是自由软件;请参看源代码的版权声明。本软件没有任何担保;
包括没有适销性和某一专用目的下的适用性担保。

Size of ExampleA: 8, Offset of var1 (char): 0, Offset of var2 (int): 4
Size of ExampleB: 8, Offset of var1 (char): 4, Offset of var2 (int): 0
Size of ExampleC: 8, Offset of var1 (char): 0, Offset of var2 (int): 4
Size of ExampleD: 5, Offset of var1 (char): 0, Offset of var2 (int): 1
Size of ExampleE: 8, Offset of var1 (char): 0, Offset of var2 (int): 4

可以看到上述样例在我所提到的各个平台输出都是一致的。说明该样例没有区分出这些平台的差异。