嵌入式 C:packed 与对齐
更新时间:2026-09-21。嵌入式/裸机视角。主机视角的布局规则见 结构体内存对齐。
一句话回答
编译器为了让多字节成员「自然对齐」会在结构体里插入填充字节,导致 sizeof 比成员之和大;当你要让结构体逐字节对应通信报文、Flash 内容或外设数据时,用 __attribute__((packed)) 去掉填充,但 packed 会带来未对齐访问,在很多 ARM 核上直接取其多字节成员地址可能触发硬 fault。
一、填充从哪来
c
struct A {
uint8_t a; // 偏移 0,占 1
// 编译器插入 3 字节填充
uint32_t b; // 偏移 4,对齐到 4
}; // sizeof = 8,而不是 5规则:成员通常对齐到「其自身大小」的地址偏移,结构体整体大小还要补齐到最大对齐的整数倍。
二、packed 去掉填充
c
struct B {
uint8_t a;
uint32_t b;
} __attribute__((packed)); // GCC/Clang/arm-none-eabi
// sizeof(struct B) == 5,成员紧密排列这在映射固定格式的字节流(通信帧、传感器块、EEPROM 布局)时必需,否则字节全错位。
三、未对齐访问的坑
packed 后 b 落在偏移 1(非 4 字节对齐):
c
struct B x;
uint32_t v = x.b; // 很多 ARM 核不支持非对齐取数 -> HardFault
uint32_t *p = &x.b; // 这个指针本身就未对齐,解引用危险安全做法是按字节拷贝再拼装,或用 memcpy(编译器会生成安全的取数序列):
c
uint32_t v;
__builtin_memcpy(&v, &x.b, sizeof(v)); // 不产生未对齐访问需要手动对齐时用 _Alignas(4) uint8_t buf[...];(C11)或 __attribute__((aligned(4)))。
常见坑
- 以为
sizeof等于成员之和:忽略填充,报文解析整体错位。 - packed 后直接用成员指针:在 Cortex-M0/M3 等核上未对齐访问硬 fault(M4/M7 部分指令支持但仍有边界)。
- 跨编译器不可移植:GCC 用
__attribute__((packed)),另有#pragma pack(1);裸机多为 GCC,但要心里有数。 - 只解决对齐、忽略端序:跨设备结构体传输同时有字节序问题,见 端序转换。
- 为省事把所有结构体都 packed:性能下降且易触发未对齐异常,只在确实要逐字节映射时使用。
总结
自然对齐使编译器插入填充、sizeof 变大;映射报文/Flash/外设时用 packed 去掉填充,但 packed 成员可能未对齐、在 ARM 上直接取址会硬 fault,应改用 memcpy 按字节访问,并同时处理端序问题——packed 是映射工具,不是默认选项。