使用#pragma pack(n)的注意事项与问题案例分享
0. 背景
本文记录很久之前在一个项目中遇到的 ” 幽灵问题 ” , 结构体读写异常,虽然最终结论很简单,遇到过类似问题或者了解对应知识点的可能一眼就知道了,但是没遇到过的可能会花费很多时间去定位甚至无从下手。这就是经验的重要性,所以特分享出这篇文章。结论本身没有很大的技术含量,但是中间涉及的思想,态度,解决问题的思路,过程,如何形成标准,避免类似问题等等确是我们嵌入式开发中的共性问题。
1.
问题回顾
1.1
历史问题
1
在不同地方
,
结构体访问按照不同对齐方式访问
,
开始怀疑
keil
编译器的问题。之前还换了
keil
的不同版本去试都是一样。
之前
can
驱动在改了某版本代码后突然收不到数据,调试记录如下
:
写和读时结构体对齐方式不一样。
mdk
未显式指定结构体对齐方式时
,
通过
.
访问成员变量
,
可能不同地方对齐方式不一样。
Mdk
版本
v5.xx ARM CC
编译器
V5.06
写结构体成员
CAN1->sFIFOMailBox[1].RIR
查看对应的汇编代码是
STR r0 [SP,#0x0C]
即
RIR
成员变量偏移地址是
0xC
,此时采用自然对齐非压缩方式。写进去的值是
0x00 22 E2 F0
。
读结构体成员
CAN1->sFIFOMailBox[1].RIR
查看对应的汇编代码是
LDR r0 [SP,#0x0A]
即
RIR
成员变量偏移地址是
0xA
。与写时偏移地址不一样
,
此时采用了压缩方式
,
读出来的值是
E2 EF A5 A5
,偏移了
2
字节。查看内存,实际内存的值是对的
,
只是结构体访问时对应汇编代码成员变量的的偏移地址不对
,
导致解析错误
,
如果按写入时的偏移
0x0C
解析读到的值是
0x0022E2EF
就是正确的。
解决办法
:
暂时不确定是编译器问题还是配置问题
,
手动显式设置结构体对齐方式
,
可解决该问题。
1.2
历史问题
2
某些结构体增加
#pragma pack(1)
导致后导致系统异常。
当时修改代码后未复现
,
没有记录现场。
2 问题分析过程
查找问题起始点
前面花了差不多一天时间去对比代码逐渐删除
,
最终定位到
driver_can.h
增加以下代码
就有问题不加就没问题
typedef struct
{
uint16_t rx_in_u16; /**< 接收缓冲区写入指针 */
uint16_t rx_out_u16; /**< 接收缓冲区读出指针 */
uint16_t rx_len_u16; /**< 接收缓冲区有效数据大小 */
uint16_t tx_in_u16; /**< 发送缓冲区写入指针 */
uint16_t tx_out_u16; /**< 发送缓冲区读出指针 */
uint16_t tx_len_u16; /**< 发送缓冲区有效数据大小 */
uint16_t rxbuf_len_u16; /**< 接收缓冲区大小 */
uint16_t txbuf_len_u16; /**< 发送缓冲区大小 */
driver_can_data_t *rx_buf_pt; /**< 接收缓冲区 */
driver_can_data_t *tx_buf_pt; /**< 发送缓冲区 */
}driver_can_t;
进一步验证
找到出现问题的代码后就一步步跟踪
在头文件中
driver_can.h
中定义了
在
osapi.c
中
include
“
driver_can.h
”
导致以下代码 绿色语句执行后出错。
在 osapi.c 中 不包含 driver_can.h
上述现象无
调试分析
用仿真器跟踪调试对比有问题和无问题的代码执行时的环境
(
变量地址 变量值等
)
先包含
driver.h
有问题时情况如下
:
Osapi.c 中如下代码执行
可以看出进入函数
uxTaskGetSystemState
执行前
pxTaskStatusArray
的
eCurrentState
和
uxCurrentPriority
只相差
1,
说明是
pack(1)
模式
进入函数 uxTaskGetSystemStat 后 ( 在 task.c 中 ) 看到红色部分变了 ,pxTaskStatusArray 的 eCurrentState 和 uxCurrentPriority 相差 4, 说明是非 pack(1) 模式
在后面继续给
uxCurrentPriority
等成员赋值时实际上溢出了
,
因为传入
pxTaskStatusArray
的是复函数
malloc
出来的
,
所以这里溢出将导致
malloc
的链表关系破坏导致整个堆环境破坏
,
后面问题会蔓延最终导致灾难性错误。
如果上面的
pxTaskStatusArray
不是
malloc
出来的而是栈中的临时变量则会导致栈破坏,最终问题也可能蔓延导灾难性的错误。
而不包含
driver.h
时
进入函数 uxTaskGetSystemState 前
进入函数后 没有变
最终原因
从上可以看出
,
因为
pxTaskStatusArray
对应结构体是没有
TaskStatus_t
显示指定对齐模式的
,
Osapi
包含了
driver.h
的
#pragma pack(1)
所以
osapi
整个文件中没有显示指定的对齐模式的结构体都按照
pack(1)
对齐
,
而
task.c
中按照默认对齐方式
(4
字节
),
所以导致错误。
实际上这里是
#pragma pack(1)
的用法错误 正确用法见《总结》
上述分析过程在 keil 中也是一样的 , 所以之前怀疑的 keil 编译有问题是错误的 , 跟编译器没有关系 , 是 pack(1) 指令使用错误导致。
3.
问题回顾
回顾问题一
为什么不同地方结构体访问不同
?
是因为当时有些地方的头文件中增加了
#pragma pack(1),
有些
c
文件包含了该头文件
,
有写
c
文件没有包含该文件。在包含了该头文件的
c
文件中所有没有显示指定对齐模式的结构将会按照
pack(1)
模式
,
没有包含该头文件的
c
文件中则会按照编译器默认的对齐模式。所以导致不同
c
文件对齐模式不一样
,
关键是看有包含的头文件中有
#pragma pack(1)
为什么对结构体显示的指定对齐模式后就没问题
?
#pragma pack(1)
的含义是
: c
文件
#pragma pack(1)
指令后所有没有显示指定对齐模式的结构体都会按照
pack(1)
对齐。
对于显示指定对齐模式的结构体按照指定对齐模式
,
所以显示指定后不受
#pragma pack(1)
影响
回顾问题二
为什么不知何故加了些代码就好了
?
因为有问题的 c 代码中没有包含有 #pragma pack(1) 的头文件 , 或者结构体显示的增加了对齐模式。
总结
结构体对齐方式的指定有两种
,
推荐使用第一种
l
第一种
:
直接对结构体显式定义对齐模式 这种方式一般使用于头文件申明时
对于支持
gcc
属性扩展的编译器
(IAR KEIL
新版本都支持
)
使用
例如
typedef struct __attribute__ ((__packed__)) loop_to_channel
{
uint8_t loop;
gpio_ch_e ch;
} loop_to_channel_t;
对于IAR还可以使用__packed
/**
* \struct driver_can_status_t
* CAN状态结构体.
*/
typedef __packed struct
{
uint8_t send_err; /**< 发送错误帧计数 */
uint8_t rcv_err; /**< 接收错误帧计数 */
uint32_t send_frames; /**< 发送帧数 */
uint32_t rcv_frames; /**< 接受帧数 */
uint32_t esr; /**< 状态寄存器 */
}driver_can_status_t;
l
第二种
: #pragma pack(1)
这种方式一般使用于
c
文件中对本文件设置后所有地方生效
这种方式一定要注意恢复设置
正确示例
typedef struct
{
uint16_t rx_in_u16; /**< 接收缓冲区写入指针 */
uint16_t rx_out_u16; /**< 接收缓冲区读出指针 */
uint16_t rx_len_u16; /**< 接收缓冲区有效数据大小 */
uint16_t tx_in_u16; /**< 发送缓冲区写入指针 */
uint16_t tx_out_u16; /**< 发送缓冲区读出指针 */
uint16_t tx_len_u16; /**< 发送缓冲区有效数据大小 */
uint16_t rxbuf_len_u16; /**< 接收缓冲区大小 */
uint16_t txbuf_len_u16; /**< 发送缓冲区大小 */
driver_can_data_t *rx_buf_pt; /**< 接收缓冲区 */
driver_can_data_t *tx_buf_pt; /**< 发送缓冲区 */
}driver_can_t;
错误示例
typedef struct
{
uint16_t rx_in_u16; /**< 接收缓冲区写入指针 */
uint16_t rx_out_u16; /**< 接收缓冲区读出指针 */
uint16_t rx_len_u16; /**< 接收缓冲区有效数据大小 */
uint16_t tx_in_u16; /**< 发送缓冲区写入指针 */
uint16_t tx_out_u16; /**< 发送缓冲区读出指针 */
uint16_t tx_len_u16; /**< 发送缓冲区有效数据大小 */
uint16_t rxbuf_len_u16; /**< 接收缓冲区大小 */
uint16_t txbuf_len_u16; /**< 发送缓冲区大小 */
driver_can_data_t *rx_buf_pt; /**< 接收缓冲区 */
driver_can_data_t *tx_buf_pt; /**< 发送缓冲区 */
}driver_can_t;