作者:GorgonMeducer 傻孩子
首发:裸机思维前面的两篇文章,我们分别介绍了“为什么变量要对齐到它的尺寸大小”,“编译器会怎么处理内存的对齐问题”以及“非对齐是如何产生的和非对齐的后果”,感觉自己错过了重要内容的朋友可以发送关键字“对齐”来复习一下。下面我们来介绍几个于对齐相关的问题:
1. 结构体的对齐
在ARM Compiler里面,结构体内的成员并不是简单的对齐到字(Word)或者半字(Half Word),更别提字节了(Byte),结构体的对齐使用以下规则:
- 整个结构体,根据结构体内最大的那个元素来对齐。比如,整个结构体内部最大的元素是WORD,那么整个结构体就默认对齐到4字节。
- 结构体内部,成员变量的排列顺序严格按照定义的顺序进行
- 结构体内部,成员变量自动对齐到自己的大小——这就会导致空隙的产生。
比如:
struct {uint8\_t a;
uint16\_t b;
uint8\_t c;
uint32\_t d;
} Example;
- 结构体内部,成员变量可以单独指定对齐方式为byte,例如
struct {uint8\_t a;
uint16\_t b \_\_attribute\_\_ ((packed));
uint8\_t c;
uint32\_t d;
} Example;
效果就会变成:
2. Cortex-M 中断向量表的对齐
Cortex-M中断向量表保存的都是32位的地址,每一个地址指向一个中断处理程序,因此中断向量表的大小必然是4的整倍数。理论上,你有n个中断,就因该有(n+1)*4 个字节大小的中断向量表。然而事情并非这么简单。为了硬件实现的方便:
- 中断向量表的大小必须是2^n (6<n<12) ,也就是128B,256B,512B, 1024B,2048B之一
- 中断向量表的地址必须要对齐到它的大小,比如512Byte大小的中断向量表,其首地址必须要对齐到 0x0200(是0x200的整数倍)
为什么会存在这样的限制呢,原因很简单,假设向量号为x的中断被触发了,Cortex-M内核就会用这个x作为下标去访问这个uint32\_t的数组,那么这个中断向量具体在内存里面的地址如何计算的呢?
我们认为是这样的:
中断向量地址 = 向量表基地址 + (x * 4)
然而,我们天真了,为了省事,这里的“+”运算被替换成了简单的"或"运算,也就是说,实际的硬件实现是这样的:
中断向量地址 = 向量表基地址 OR (x *4)
这意味着什么呢?加法运算是会进位的!或运算不会。举例来说:
0x01 OR 0x01 = 0x01
而
0x01 + 0x01 = 0x02
当硬件认为系统中向量表应该是512个字节大小时,如果向量表的基地址(通过SCB->VTOR寄存器设置)对齐到了0x0200,那么或运算的结果就与加法是等效的——原因很简单,(x * 4) 的部分不会超过0x0200,因而与加法等效。反之就有问题了。
又由于系统强制要求中断向量表必须最少对齐到128个字节,那么对一个512字节大小的向量表来说,如果仅对齐到128个字节会发生什么呢?——如果前31个中断(包括系统自己的异常)触发了,系统可以正常处理,从第32个中断开始,任何一个触发,系统一定会出错——中断向量的所在的位置算错啦!(注意不是中断处理程序的地址算错了,是保存中断处理程序地址的那个向量所在的内存地址被算错了)
3. Cortex-M MPU 受保护内存区块的对齐
MPU也许你听说过,但你多半没有用过,因为“太!难!用!拉!”,为了硬件实现的方便,MPU每一个Region的设置被加入了一个人为的限制:
- Region的大小必须是 2^n (4<n<33),也就是32,64...2G, 4G
- Region的基地址必须对齐到它的大小
又来!是的,就是这么坑,所以如果你想用MPU保护一个任意位置任意大小的Memory,比如stack,不好意思,你要用很多个Region一起来拼接……具体怎么拼,说起来都麻烦,何况用……算了不说了。
好消息是,最新的ARMv8-M终于改进了这个反人类的设计,允许用户通过起始地址+终止地址的方法设定任意大小任意位置的Region(当然Region大小必须是32的倍数,这个地址也必须是32的倍数)。可以好好松口气了。
专栏推荐文章
如果你喜欢我的思维,欢迎订阅裸机思维
版权归裸机思维(傻孩子图书工作室旗下公众号)所有,
所有内容原创,严禁任何形式的转载。