• 正文
  • 相关推荐
申请入驻 产业图谱

嵌入式总线错误深度剖析!

8小时前
186
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好呀,我是杂烩君。

相信做嵌入式开发的朋友都懂,有些问题看似常见,排查起来却格外头疼,最近遇到个总线错误的问题。今天我把这次的问题简单总结一下,分享给大家,咱们一起把这个磨人的问题彻底搞明白!

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

以上是本次的分享,如果觉得文章有帮助,麻烦帮忙转发,谢谢大家!

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

本公众号专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,公众号内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!