本文转自公众号,欢迎关注
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;
审核编辑:汤梓红
-
嵌入式
+关注
关注
5067文章
19008浏览量
302890 -
keil
+关注
关注
68文章
1211浏览量
166658 -
编译器
+关注
关注
1文章
1618浏览量
49043 -
结构体
+关注
关注
1文章
129浏览量
10832
发布评论请先 登录
相关推荐
评论