你们好呀,我是杂烩君。
相信做嵌入式开发的同学都懂,有些问题看似常见,排查上去却愈发头痛,近来遇见个总线错误的问题。明天我把此次的问题简单总结一下,分享给你们,俺们一起把这个磨人的问题彻底搞明白!
1.段错误vs总线错误
平常开发中,造成程序崩溃的”两大杀手”就是段错误和总线错误。它们似乎就会让程序死掉,但本质完全不同:
1.1段错误
段错误(SegmentationFault)触发的缘由是访问了不该访问的显存。如:
1.2总线错误
总线错误(BusError)触发的缘由是访问方法违背了硬件规则。如:
简单来说:
2.预备知识:显存对齐与#pragmapack
在深入案例之前,先快速了解两个关键概念。
显存对齐:CPU访问显存时,遵照”自然对齐”原则——N字节的数据类型要置于N的倍数地址上。例如4字节的int/float必须置于0x00、0x04、0x08…这样的地址,置于0x01就是”非对齐”。这个规则与CPU是32位还是64位无关,取决于数据类型本身的大小。
#pragmapack:编译器默认会手动填充字节来保证对齐,但我们可以用#pragmapack(1)强制取消填充linux查看操作系统,让结构体”紧凑排列”。以下边这个结构体为例:
struct struct_x {
char a; // 1 字节
float b; // 4 字节
char c; // 1 字节
};
默认对齐(共12字节):
紧凑布局(共6字节):

紧凑布局下,b的地址弄成了0x01(不是4的倍数)。
3.问题案例剖析3.1触发总线错误的代码
#include
#include
#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);
return 0;
}
3.2不同平台的表现

x86PC运行结果:

ARM开发板运行结果:

3.3问题症结剖析
紧凑布局下的显存分布:假定a在0x00,这么b在0x01~0x04linux解压命令,c在0x05。
我们姑且觉得问题就在于floatb的起始地址是0x01,不是4的倍数,触发了总线错误,由于确实可以通过自动填充来修补。
3.4修补方案:自动填充对齐
在a和b之间加入3字节填充linux 栈溢出攻击原理,让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字节linux 栈溢出攻击原理,如果我们把里面反例的结构体更改为如下代码:
#pragma pack(1)
struct struct_x
{
char a;
int b;
char c;
};
#pragma pack()
运行结果会怎样?
如此一问,想必你们也猜到了结果,确实能正常运行!

这是一个十分有意思的问题!把floatb改成intb,同样的非对齐地址,却不会报错!为何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方式三:限制#pragmapack的作用范围
只对必要的结构体使用:
#pragma pack(push, 1) // 保存当前对齐设置,设置为 1
struct network_packet {
// ... 网络协议要求的紧凑布局
};
#pragma pack(pop) // 恢复之前的对齐设置
// 其他结构体不受影响
struct normal_struct {
// ... 正常对齐
};
6.总结
总线错误:非对齐地址+ARM严格检测+浮点指令更敏感。x86能跑不代表ARM能跑,#pragmapack要慎重使用。
