上个月我合入升级逻辑代码后,测试发现升级后的固件版本还是旧的。反复验证了几次,结果都一样。
最初怀疑 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
无论指向 int、char 还是巨大的结构体,指针的大小,只取决于地址总线的宽度,与它指向的对象有多大无关。
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_erase 传 sizeof(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等帮助发现问题。
思考
- 如果在 64 位系统上编译同样的代码,
sizeof(P_Config)是多少?为什么? - 如果项目中已经大量使用了
typedef ... *Handle这种模式,有什么方法可以批量排查潜在风险? - 下面代码的输出是什么?
typedef int Array[10];
typedef Array *ArrayPtr;
printf("%zu\n", sizeof(Array));
printf("%zu\n", sizeof(ArrayPtr));
(提示:如果你猜不到 sizeof(ArrayPtr) 是多少,不妨先想想 Array 到底是个什么类型?)
2794

被折叠的 条评论
为什么被折叠?



