一个sizeof笔误引发的flash操作异常

上个月我合入升级逻辑代码后,测试发现升级后的固件版本还是旧的。反复验证了几次,结果都一样。

最初怀疑 Flash 写入失败、校验逻辑有缺陷、固件打包出错。排查了一圈,最后发现罪魁祸首是结构体定义里一个不起眼的星号(*)。

还原现场:

typedef struct SConfig {
    uint8_t data[4092];
    uint32_t checksum;
} S_Config, *P_Config;        // 注意这一行

S_Config g_sysConfig = {0};

flash_erase(address, sizeof(P_Config));
flash_write(address, &g_sysConfig, sizeof(S_Config));

同事说:“当时想获取配置信息大小,顺手写了 sizeof(P_Config)。”

编译零警告,逻辑看起来也没问题,灾难就此埋下。

问题出在哪里

排查问题时盯着那段代码看了半小时,愣是没看出毛病,直到我灵机一动,打印出了 sizeof 的结果,

先看 sizeof 的结果:

printf("sizeof(S_Config) = %zu\n", sizeof(S_Config));  // 4096
printf("sizeof(P_Config) = %zu\n", sizeof(P_Config));  // 4

同样的结构体,两种"大小"。P_Config 难道不是 S_Config 的别名吗?

回头看那行 typedef:

typedef struct SConfig { ... } S_Config, *P_Config;

拆开来看,这一行等价于两个定义:

typedef struct SConfig { ... } S_Config;      // S_Config 是结构体类型
typedef struct SConfig *            P_Config;  // P_Config 是指针类型

一行代码里定义了两个完全不同的类型:一个是结构体,一个是指向结构体的指针。

P_Config 的类型是 struct SConfig *,在 32 位 MCU 上指针固定占 4 字节。所以 sizeof(P_Config) 返回的是指针大小,不是结构体大小。

深入理解:为什么指针是 4 字节

32 位 MCU 的地址总线有 32 根,可寻址范围 2^32 = 4GB。要表示 4GB 空间中的任意一个地址,需要 32 个二进制位,也就是 4 字节。

sizeof(char*)  = 4
sizeof(int*)   = 4
sizeof(void*)  = 4

无论指向 intchar 还是巨大的结构体,指针的大小,只取决于地址总线的宽度,与它指向的对象有多大无关。

sizeof 是编译时运算符

sizeof 不是函数,是编译时运算符。编译完成后,sizeof 的结果已经作为立即数嵌入到指令中:

int get_ptr_size()   { return sizeof(P_Config); }
int get_struct_size() { return sizeof(S_Config); }

反汇编后(arm-gcc):

get_ptr_size:
    mov    r0, #4
    bx     lr

get_struct_size:
    mov    r0, #4096
    bx     lr

两者都是 mov r0, #imm,只是立即数不同。从编译器视角看,你确实是在获取指针类型的大小,它不会(也无法)揣测你的意图。

sizeof 是编译时的运算符,它只认类型,不认你的意图。

Flash 为何因此数据错乱

NOR Flash 的擦除和写入有各自的最小单位。擦除的最小单位是扇区,常见大小有 2KB、4KB、64KB 等,具体取决于芯片型号。写入则以字节或字为单位。

Flash 的位操作有一个硬件约束:1 可以编程为 0,但 0 不能直接变回 1。要从 0 回到 1,必须擦除整个扇区(扇区所有位恢复为 1)。

回到 Bug 现场,这里写入是正确的(传入了 sizeof(S_Config) = 4096),但擦除时出了问题:

flash_erase(addr, 4)        -> 传了 4(指针大小)
                            -> 只擦除了第一个扇区
                            -> 配置横跨多个扇区,后面的扇区没擦除
flash_write(addr, &g_sysConfig, 4096)
                            -> 写入完整新数据
校验读出                   -> 未擦除的扇区残留旧数据,校验不通过

如果 flash_erasesizeof(S_Config) = 4096,所有相关扇区完整擦除再写入,就不会有问题。只擦除了部分区域却以为全部擦干净了,这就是差异。

根因总结

typedef 中的 * 创建了一个指针类型,不是结构体别名。sizeof 作用于指针类型时返回的是指针本身的大小,而不是指向对象的大小。两者相差了 1024 倍(4096 / 4)。

这不是编译器的错。C 语言信任程序员——你写 sizeof(P_Config),它就给你指针大小,不会反问"你确定要这个?"

如何避免

第一,不要在一行 typedef 中同时定义类型和指针:

// 不推荐
typedef struct SConfig { ... } S_Config, *P_Config;

// 推荐
typedef struct SConfig { ... } S_Config;

S_Config g_sysConfig;
S_Config *pConfig;        // 手动写 *,一眼看出是指针

第二,尽量避免用 typedef 隐藏指针。Linux 内核编码规范也建议不要 typedef 指针类型,因为这会掩盖指针的语义,让调用者误以为在传值。

第三,可以用 _Static_assert 做编译期检查:

_Static_assert(sizeof(P_Config) == sizeof(S_Config),
               "P_Config is a pointer type, not the struct!");

上面这行会编译失败(4 != 4096),在编译期就能发现类型不匹配。

第四,可采用一些静态编译工具Cppcheck、Parasoft等帮助发现问题。

思考

  1. 如果在 64 位系统上编译同样的代码,sizeof(P_Config) 是多少?为什么?
  2. 如果项目中已经大量使用了 typedef ... *Handle 这种模式,有什么方法可以批量排查潜在风险?
  3. 下面代码的输出是什么?
typedef int Array[10];
typedef Array *ArrayPtr;

printf("%zu\n", sizeof(Array));
printf("%zu\n", sizeof(ArrayPtr));

(提示:如果你猜不到 sizeof(ArrayPtr) 是多少,不妨先想想 Array 到底是个什么类型?)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

顾鉴行思

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值