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

描述

本文转自公众号,欢迎关注
https://mp.weixin.qq.com/s/uzaGLFTDBAn8wyR84yaiIw

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;

审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分