对堆栈8字节对齐问题的讨论
1. 结构(struct)或联合(union) 中的第一个数据成员放在 offset 为 0 的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组…
1. 字节对齐原则
-
结构(
struct)或联合(union) 中的第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组、结构体等)的整数倍开始(比如int型变量在32位编译环境下为4字节,则要从4的整数倍地址开始存储。 -
如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。如
struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储。 -
结构体的总大小,也就是
sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。
typedef struct Demo_A
{
double length; // 0 - 7;
int id; // 8 - 11;
char op; // 12 - 13;
float weight; // 16 - 19;
}AA;
typedef struct Demo_B
{
char name[5]; // 0 - 4 ;
int id; // 8 - 11 ;
double score; // 16 - 23 ;
float ui; // 24 - 27 ;
short grade; // 28 - 31 ;
char weight; // 32 ;
}BB;
总大小为最大成员变量大小的整数倍,
sizeof(AA) = 24;sizeof(BB) = 32;
#pragma pack()
在代码前加一句 #pragma pack(1),会发现 sizeof(AA) = 17; sizeof(BB) = 24;
AA 是 8+4+1+4=17;
BB 是 5+4+8+4+2+1=24;
这就是理想中的没有内存对齐的情况,所以 #pragma pack(1) 是告诉编译器,所有的对齐都按照 1 的整数倍对齐,换句话说就是没有对齐规则。
即 #pragma pack(n) 就是所有的对齐都按照 n 的整数倍对齐。
注:
Vc,Vs等编译器默认是#pragma pack(8),所以测试我们的规则会正常;
gcc默认是#pragma pack(4),并且gcc只支持1,2,4对齐。套用三原则里计算的对齐值是不能大于#pragma pack指定的n值。
参考:https://www.cnblogs.com/liangxiaxu/articles/2617484.html
2. 为什么要保证堆栈8字节对齐
AAPCS 规则要求堆栈保持 8 字节对齐。如果不对齐,调用一般的函数也是没问题的。但是当调用需要严格遵守 AAPCS 规则的函数时可能会出错。
例如调用 sprintf 输出一个浮点数时,栈必须是 8 字节对齐的,否则结果可能会出错。
- 实验验证
#include "stdio.h"
#include "string.h"
float fff=1.234;
char buf[128];
int main(void)
{
sprintf(buf,"%.3f\n\r",fff);//A
while(1);
}
- 在
A处设置断点,让程序全速运行至A - 在
MDK中修改MSP的值使MSP满足8字节对齐 - 全速运行程序,观察
buf中的字符为1.234结果正确 - 回到第
2步,修改MSP使之只满足4字节对齐而不满足8字节对齐 - 全速运行程序,观察
buf中的字符为-2.000结果错误
该实验证明了调用
sprintf输出一个浮点数必须要保证栈8字节对齐。
3. 编译器为我们做了什么
- 先看一个实验
#include "stdio.h"
#include "string.h"
float fff=1.234;
char buf[128];
void fun(int a,int b,int c,int d)
{
int v;
v=v;
}
void test(void)
{
}
int main(void)
{
fun(1,2,3,4);
test(); //A
// sprintf(buf,"%.3f\n\r",fff);
while(1);
}
保证初始的时候堆栈是 8 字节对齐的
- 在
A处设置断点; - 全速运行至
A,观察MSP=0x2000025c,没有8字节对齐; - 略微修改一下
main函数代码如下,其他部分代码不变;
int main(void)
{
fun(1,2,3,4);
// test();
sprintf(buf,"%.3f\n\r",fff); //A
while(1);
}
- 同样在
A处设置断点; - 全速运行至
A,观察MSP=0x200002d8,这次8字节对齐了;
这个实验说明了如果编译器发现了某个函数需要调用浮点库时会自动调整编译生成的汇编代码,从而保证调用这些浮点库函数时堆栈是
8字节对齐的。换句话说如果我们保证了栈初始的时候是8字节对齐的,那么编译器可以保证以后调用浮点库时堆栈仍是8字节对齐的。
4. os下应该怎样设置任务堆栈
由上面的讨论可知给任务分配栈时需要保证栈是 8 字节对齐的,不然在该任务中凡是调用 sprintf 的函数均会出错,因为栈一开始就是不对齐的。
是否保证了栈初始是8字节对齐了就万事大吉了呢。no!大家请看一种特殊的情况:
#include "stdio.h"
#include "string.h"
float fff=1.234;
char buf[128];
void fun(int a,int b,int c,int d)
{
int v;
v=v;
}
int main(void)
{
fun(1,2,3,4);
while(1);
}
void SVC_Handler(void)
{
sprintf(buf,"%.3f\n\r",fff);//B
}
mian 函数的反汇编如下:
0x080001DC B500 PUSH {lr}
0x080001DE 2304 MOVS r3,#0x04 ;A
0x080001E0 2203 MOVS r2,#0x03
0x080001E2 2102 MOVS r1,#0x02
0x080001E4 2001 MOVS r0,#0x01
0x080001E6 F7FFFFF5 BL.W fun (0x080001D4)
0x080001EA BF00 NOP
0x080001EC E7FE B 0x080001EC
保证初始的时候堆栈是 8 字节对齐的;
- 在
A处设置断点; - 全速运行至
A,观察此时MSP=0x200002e4未对齐; - 在
MDK中将SVC的挂起位置1; - 在
B处设置断点; - 全速运行至
B,观察此时MSP=0x200002b4未对齐; - 继续全速执行,观察
buf中的字符为:-2.000出错了;
这个实验说明了即使保证栈初始是
8字节对齐的,编译器也只能保证在调用sprintf那个时刻栈是8字节对齐的,但不能保证任意时刻栈都是8字节对齐的,如果恰巧在MSP没有8字节对齐的时刻发生了中断,而中断中又调用了sprintf,这种情况下仍会出错。
5. Cortex-M3 内核为我们做了什么
Cortex-M3 内核提供了一种硬件机制来解决上述这种中断中栈不对齐问题。
CM3 中可以把 NVIC 配置控制寄存器的 STKALIGN 置位,来保证中断中的栈 8 字节对齐。
具体实现过程如下:当发生中断时由硬件自动检测 MSP 是否 8 字节对齐,如果对齐了,则不进行任何操作,如果没有对齐,则自动将 MSP 减 4 这样便对齐了,同时将 xPSR 的第 9 位置位来记录这个 MSP 的非正常的变化,在中断返回若发现 xPSR 的第 9 位是置位的则自动将 MSP 加 4 调整回原来的值。
- 实验验证
#include "stdio.h"
#include "string.h"
float fff=1.234;
char buf[128];
void fun(int a,int b,int c,int d)
{
int v;
v=v;
}
int main(void)
{
fun(1,2,3,4);
while(1);
}
void SVC_Handler(void)
{
sprintf(buf,"%.3f\n\r",fff);//B
}
mian 函数的反汇编如下
0x080001DC B500 PUSH {lr}
0x080001DE 2304 MOVS r3,#0x04 ;A
0x080001E0 2203 MOVS r2,#0x03
0x080001E2 2102 MOVS r1,#0x02
0x080001E4 2001 MOVS r0,#0x01
0x080001E6 F7FFFFF5 BL.W fun (0x080001D4)
0x080001EA BF00 NOP
0x080001EC E7FE B 0x080001EC
- 在
A处设置断点; - 全速运行至
A,观察此时MSP=0x200002e4未对齐; - 在
MDK中将SVC的挂起位置1,同时将0xE000ED14处的值由0x00000000改为0x00000200(即将NVIC配置控制寄存器的STKALIGN置位) - 在
B处设置断点; - 全速运行至
B,观察此时MSP=0x200002b0对齐了; - 观察中断返回时的
MSP=0x200002e4调整回来了; - 继续全速执行,观察
buf中的字符为1.234正确;
这个实验说明了将
NVIC配置控制寄存器的STKALIGN置位可以保护中断时栈仍是8字节对齐
6. 总结
综上所述,为了能够安全的使用严格遵守 AAPCS 规则的函数(比如 sprintf )需要做到以下几点:
- 保证
MSP在初始的时候是8字节对齐的 - 如果用到
OS的话需要保证给每个任务分配的栈是保持8字节对齐的 - 如果用的是基于
CM3内核的处理器需将NVIC配置控制寄存器的STKALIGN置位
7. 使用系统时的操作(uCOS-Ⅲ)
OS_TCB FloatTaskTCB; //任务控制块;
__align(8) CPU_STK FLOAT_TASK_STK[FLOAT_STK_SIZE]; //任务堆栈;
其中 __align(8) 是保证开辟堆栈保持 8 字节对齐;
本文可自由转载与修改,但需保留作者署名与出处链接,不得用于商业用途,且衍生作品需以相同方式共享。 转载请注明:MacLodge's Blog · 对堆栈8字节对齐问题的讨论