作者:GorgonMeducer 傻孩子
首发:裸机思维
- 什么情况下会产生非对齐的操作呢?
- *
在讨论这个问题之前,我们先要记住一个结论:
一般情况下,出于效率和兼容性的考虑,编译器会避免产生非对齐的操作。当且仅当编译器不知道(被蒙蔽)的情况下,才有可能产生隐性的非对齐的操作。
不知所云?让我们举一个简单的例子:
extern void word_access ( uint32_t *pwTarget );
// 如果你这么用,显然是没有任何问题的
extern uint32_t wDemo;
...
word_access (&wDemo);
// 如果你这么做呢……
extern uint8_t chBuffer[16];
...
word_access ((uint32_t *)&chBuffer[1]);
不管你是否已经明白问题所在了,我们来简单分析下这段代码:
- 函数 word\_access() 需要一个 uint32\_t 型的指针作为形式参数
- 参考 上一篇文章 的内容,我们知道,对于 (uint32\_t * ) 指针的操作,编译器会生成对齐到word的操作指令, 比如LDR和STR。我们可以下结论说,函数 word\_access() 存在隐含要求,就是传入的指针必须是word对齐的。
- 最后一个例子中,数组chBuffer[] 很有可能被分配在一个对齐到 word 或者 halfword 的地址上,那么 &chBuffer[1] 几乎可以肯定是一个非对齐的地址
- 把一个非对齐的地址传给一个默认需要对齐的函数,结果不言自明。
可能有人会问:既然代码已经写的清清楚楚——“我们使用的是一个非对齐的地址”——为什么编译器仍然会假装不知道呢?其实编译器并非不知道,如果我们直接这么写:
word\_access (&chBuffer[1]);
编译器立马就会报告Error:“指针的类型不符”。为了头疼医头,脚疼医脚的“屏蔽”这个Error,很多人会加入强制类型转换 (uint32\_t *) 。实际上,从ANSI-C的标准来看,这个代码并没有任何问题,语法和逻辑上都讲得通。但是对齐是一个“潜规则”,你不遵守它,就会吃亏。这里,强制类型转换相当于直接给编译器蒙住了眼睛:“甭管之前看到了什么,反正现在这个指针,我说是对齐的就是对齐的!!!”
- 谁会写这么傻的代码呢?
- *
也许你不会直接写出这么傻的代码,但是下面的“高级”用法确更加稀松平常:
// 这是一个消息地图中常见的消息处理函数
void xxx_msg_handler( uint8_t *pchStream, uint16_t hwSize )
{
// offset 0x00: 1 BYTE Command / Message
uint8_t chCMD = pchStream[0];
// offset 0x01: 4 BYTE Serial Number of the frame
uint32_t wSN = *(uint32_t *)&pchStream[1];
...
}
对于通信数据帧解析来说,上述用法在常见不过了,怎么样踩地雷了吧?这只是举一个例子,只要用到指针强制类型转换的地方,都是在“蒙蔽”编译器,都有可能受到对齐潜规则的惩罚。
- 为什么我这么写了,代码执行的好好的?
- *
因为 ARMv7-M 支持非对齐操作,具体请看 对齐(1)的内容,所以你幸免于难。但是,对如下的情况,你就绝无可能幸免:
- 如果 ARMv7-M 中关闭了对非对齐操作的支持(感觉是废话)
- 用的是 ARMv6-M,本身就不支持任何非对齐操作
- 如果编译器用的是STM、LDM,POP和PUSH这种完全不支持非对齐操作的指令
- 如果编译器用的是LDRD,STRD这类双字(DWORD)操作的指令,地址没有对齐到WORD或者DWORD
- 如果你操作的地址比 0xE0000000 大,简单说就是你在访问Processor的系统外设,比如NVIC,SysTick,SCB,MPU等等(不知道我在说啥也没关系,通常这类操作都是用CMSIS库实现的,不太可能出现非对齐操作)
- 非对齐操作有什么危害?
- *
非对齐操作的危害主要有以下几点:
- 影响代码的可移植性。(比如:好好的工程,加入某个模块就立即异常……还没有源代码,只有.a,呵呵)
- 直接导致性能下降。尽管LDR/STR这样的指令支持非对齐操作,但其实我们的流水线是通过1)将这一非对齐的操作拆分成两个对齐的操作,最后2)再组装起来 实现的。
- 如果操作的目标地址上是一个“易失性”的寄存器,那么非对齐的操作被拆分了以后,会导致原本的一次操作变成了连续的两次。从而对操作的内容产生破坏性的后果。
- *
注意:这里“易失性”意思就是,每次操作的时候:
- 要么操作本身会导致寄存器内容改变
比如,GPIO的Toggle寄存器,每次写操作都会导致对应的引脚翻转
比如,外设的中断状态寄存器,读取状态寄存器的操作本身就会清除标志
……
- 要么每次读取的内容都会不同
比如, Timer计数器,每次读取的时候计数器的内容都不同
比如,ADC的采样结果寄存器,读取顺序不同,很可能每次读取时候的值都会变化
……
- *
- 如果操作的目标地址由多个Processor共享,甚至是与DMA共享,那么非对齐操作导致的连续两次操作,其原子性是没有保证的——其它Processor(总线Master)很有可能在你的两次操作中间插入进来——破坏了数据内容的完整性。这个很难调试,很难发现的哦!
是不是越听腿越哆嗦?啥?不哆嗦?莫装13,反正以后坑的是自己。珍爱生命,远离非对齐操作。
- 针对本文的例子,如何避免非对齐操作?
- *
1、对第一个例子来说,要么避免给函数提供非对齐的地址,要么直接告诉编译器对应的函数处理的地址可能是非对齐的,直接修改函数原形即可:
// 假设我们有一个函数,它要执行一个可能非对齐的 32bit 的整数操作
**extern void word\_access ( uint32\_t \_\_packed \*pwTarget );**
2、对第二个例子来说,由于数据帧的格式已经确定,因此,我们需要直接告诉编译器对目标数据的访问是非对齐的,对应的代码如下:
// 这是一个消息地图中常见的消息处理函数
void xxx_msg_handler( uint8_t *pchStream, uint16_t hwSize )
{
// offset 0x00: 1 BYTE Command / Message
uint8_t chCMD = pchStream[0];
// offset 0x01: 4 BYTE Serial Number of the frame
uint32_t wSN = *(uint32_t __packed*)&pchStream[1];
...
}
专栏推荐文章
如果你喜欢我的思维,欢迎订阅裸机思维
版权归裸机思维(傻孩子图书工作室旗下公众号)所有,
所有内容原创,严禁任何形式的转载。