大家好呀,我是杂烩君。
相信做嵌入式开发的朋友都懂,有些问题看似常见,排查起来却格外头疼,最近遇到个总线错误的问题。今天我把这次的问题简单总结一下,分享给大家,咱们一起把这个磨人的问题彻底搞明白!
1. 段错误 vs 总线错误
平时开发中,导致程序崩溃的"两大杀手"就是 段错误 和 总线错误。它们虽然都会让程序挂掉,但本质完全不同:
1.1 段错误
段错误(Segmentation Fault)触发的原因是访问了不该访问的内存。如:
- 空指针解引用数组越界访问已释放的内存栈溢出
1.2 总线错误
总线错误(Bus Error)触发的原因是访问方式违反了硬件规则。如:
- 非对齐地址访问访问不存在的物理地址硬件故障
简单来说:
段错误:你去了一个"禁区"(不属于你的内存)
总线错误:你走的"姿势不对"(访问方式违反硬件规则)
2. 预备知识
在深入案例之前,先快速了解两个关键概念。
内存对齐:CPU 访问内存时,遵循 "自然对齐" 原则 —— N 字节的数据类型要放在 N 的倍数地址上。比如 4 字节的 int/float 必须放在 0x00、0x04、0x08... 这样的地址,放在 0x01 就是"非对齐"。这个规则与 CPU 是 32 位还是 64 位无关,取决于数据类型本身的大小。
# pragma pack:编译器默认会自动填充字节来保证对齐,但我们可以用 #pragma pack(1) 强制取消填充,让结构体"紧凑排列"。以下面这个结构体为例:
struct struct_x {
char a; // 1 字节
float b; // 4 字节
char c; // 1 字节
};
默认对齐 (共12字节):
紧凑布局 (共6字节):
紧凑布局下,b 的地址变成了 0x01(不是 4 的倍数)。
3. 问题案例分析
3.1 触发总线错误的代码
#include <stdio.h>
#include <stdlib.h>
#pragma pack(1) // 强制 1 字节对齐
struct struct_x
{
char a; // 1 字节,地址:0x00
float b; // 4 字节,地址:0x01 ← 非对齐!
char c; // 1 字节,地址:0x05
};
#pragma pack()
int main(void)
{
struct struct_x test = {0};
printf("sizeof(struct struct_x) = %ldn", sizeof(test));
test.a = 1;
test.b = 2.0; // 这里可能触发总线错误!
test.c = 3;
char *a = &test.a;
float *b = &test.b;
char *c = &test.c;
printf("*a = %d, addr = %pn", *a, a);
printf("*b = %f, addr = %pn", *b, b); // ARM 上会崩溃
printf("*c = %d, addr = %pn", *c, c);
return0;
}
3.2 不同平台的表现
x86 PC 运行结果:
ARM 开发板运行结果:
3.3 问题根源分析
紧凑布局下的内存分布:假设a 在 0x00,那么b 在 0x01~0x04,c 在 0x05。
我们暂且认为问题就在于 float b 的起始地址是 0x01,不是 4 的倍数,触发了总线错误,因为确实可以通过手动填充来修复。
3.4 修复方案:手动填充对齐
在 a 和 b 之间加入 3 字节填充,让 b 对齐到 4 字节边界:
#pragma pack(1)
struct struct_x
{
char a; // 0x00
char d[3]; // 0x01~0x03 (填充)
float b; // 0x04 ← 对齐了!
char c; // 0x08
};
#pragma pack()
修复后运行结果:
4. 深入探究
在这个ARM环境下,float类型与int类型都占了4字节,假如我们把上面例子的结构体修改为如下代码:
#pragma pack(1)
struct struct_x
{
char a;
int b;
char c;
};
#pragma pack()
运行结果会如何?
这么一问,想必大家也猜到了结果,确实能正常运行!
这是一个非常有意思的问题!把 float b 改成 int b,同样的非对齐地址,却不会报错!为什么 int 可以,float 不行?
4.1 原因分析
int 和 float 虽然都是 4 字节,但 CPU 用的是不同的指令和寄存器来访问它们,而浮点指令对地址对齐的要求更严格。
ARMv6 及以后的处理器对普通 load/store 指令提供了非对齐访问支持,但浮点指令(VFP)始终要求严格对齐。所以上述 int 的代码能正常运行,float的代码会触发总线错误。
5. 预防总线错误的几个技巧
5.1 方法一:调整结构体成员顺序
不好的顺序(会产生填充):
struct bad_order {
char a; // 1 byte
int b; // 4 bytes (需要 3 bytes 填充)
char c; // 1 byte
}; // 总共:12 bytes
好的顺序(紧凑且对齐):
struct good_order {
int b; // 4 bytes
char a; // 1 byte
char c; // 1 byte
}; // 总共:8 bytes
5.2 方法二:使用 memcpy 安全访问
不安全:直接解引用非对齐指针
float value;
float val = *((float*)unaligned_ptr); // 可能崩溃!
安全:使用 memcpy
float value;
memcpy(&value, unaligned_ptr, sizeof(float)); // 总是安全
5.3 方法三:限制 # pragma pack 的作用范围
只对必要的结构体使用:
#pragma pack(push, 1) // 保存当前对齐设置,设置为 1
struct network_packet {
// ... 网络协议要求的紧凑布局
};
#pragma pack(pop) // 恢复之前的对齐设置
// 其他结构体不受影响
struct normal_struct {
// ... 正常对齐
};
6. 总结
总线错误:非对齐地址 + ARM 严格检查 + 浮点指令更敏感。x86 能跑不代表 ARM 能跑,#pragma pack 要谨慎使用。
以上是本次的分享,如果觉得文章有帮助,麻烦帮忙转发,谢谢大家!
186