你们好呀,我是杂烩君。

相信做嵌入式开发的同学都懂,有些问题看似常见,排查上去却愈发头痛,近来遇见个总线错误的问题。明天我把此次的问题简单总结一下,分享给你们,俺们一起把这个磨人的问题彻底搞明白!

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字节):

段错误与总线错误区别_linux 栈溢出攻击原理_嵌入式开发总线错误

紧凑布局下,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;

linux 栈溢出攻击原理_嵌入式开发总线错误_段错误与总线错误区别

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不同平台的表现

嵌入式开发总线错误_linux 栈溢出攻击原理_段错误与总线错误区别

x86PC运行结果:

linux 栈溢出攻击原理_段错误与总线错误区别_嵌入式开发总线错误

ARM开发板运行结果:

嵌入式开发总线错误_linux 栈溢出攻击原理_段错误与总线错误区别

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()

修补后运行结果:

linux 栈溢出攻击原理_段错误与总线错误区别_嵌入式开发总线错误

4.深入探究

嵌入式开发总线错误_段错误与总线错误区别_linux 栈溢出攻击原理

在这个ARM环境下,float类型与int类型都占了4字节linux 栈溢出攻击原理,如果我们把里面反例的结构体更改为如下代码:

#pragma pack(1)
struct struct_x
{
    char a;
    int b;
    char c;
};
#pragma pack()

运行结果会怎样?

如此一问,想必你们也猜到了结果,确实能正常运行!

嵌入式开发总线错误_段错误与总线错误区别_linux 栈溢出攻击原理

这是一个十分有意思的问题!把floatb改成intb,同样的非对齐地址,却不会报错!为何int可以,float不行?

4.1缘由剖析

linux 栈溢出攻击原理_段错误与总线错误区别_嵌入式开发总线错误

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;

嵌入式开发总线错误_linux 栈溢出攻击原理_段错误与总线错误区别

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要慎重使用。

Tagged:
Author

这篇优质的内容由TA贡献而来

刘遄

《Linux就该这么学》书籍作者,RHCA认证架构师,教育学(计算机专业硕士)。

发表回复