嵌入式Lee

文章数:196 被阅读:578672

账号入驻

使用#pragma pack(n)的注意事项与问题案例分享

最新更新时间:2023-09-19
    阅读数:

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 增加以下代码

就有问题不加就没问题

#pragma pack(1)
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 文件中对本文件设置后所有地方生效

这种方式一定要注意恢复设置

正确示例

#pragma pack(push)
#pragma pack(1)
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;
#pragma pack(pop)

错误示例

#pragma pack(1)
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;



最新有关嵌入式Lee的文章

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: TI培训

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京ICP证060456号 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved