为什么需要数据对齐?
避免数据在内存中跨边界存储,减少读取数据次数,提高效率,本质上是以空间换时间的做法
下图中属于同一水平位置的为同一边界
变量在同一边界里的一次存储周期就可以读取
一旦跨了上下两个边界来存储就需要至少两个存储周期来读取
根据存储器结构,如下图,参考链接:多体并行:高位/低位交叉编址
CPU数据线有32位一次最多可以从内存读取32数据,这里的一次指一次存储周期
LDR r1 , [pc,#4], 是从pc+#4地址处开始连续读取4个字节的数据到r1寄存器
LDRH r1 , [pc,#4], 是从pc+#4地址处开始连续读取2个字节的数据到r1寄存器
LDRB r1 , [pc,#4] 直接取pc+#4当前那个地址的数据
上述指令都是在一个存储周期里完成的
一般地址线只能确定一个字节所在的地址,而上述指令地址都一样,却不止读到仅仅一个字节的数据,还能读到2个字节或者4个字节的数据,可以看到上述指令除了操作码不一样其它都一样,按照上图的存储结构,指令的地址一样那么体内地址肯定是一样的,体内地址一样就横向选中了同一水平位置的存储,操作码的不同说明控制线输出的信号应该不同,就可以控制一个类似体号的控制器,选中连续的好几个字节的存储
地址线加上控制线就可以实现上述的操作
LDM r1 ,{r0-r1} ; 是将r1指向的内存地址开始连续8个字节的数据存放到r0和r1寄存器中
该指令是在2个存储周期里完成的
详解边界对齐
为什么要内存对齐?字节对齐和边界对齐介绍
对于数据在内存中的位置,需要确定两个因素:大小、起始位置
根据存储器结构,就是通过起始位置和数据大小获取内存中的数据
一、全局变量静态变量,地址从小增大
1、原子类型数据
按照开始的存储结构的图,short型变量起始位置放在地址1的位置应该也放得下,也能一次性读取,为什么必须得是0,2,4,6.。。。
那是因为放在不好判断地址1和地址3的不同,放在地址3就会跨界,而为了区分地址1和3做出来的电路更为复杂得不偿失,
8字节的也是同理
2、组合类型数据
详细参考链接:
详解边界对齐
// structure C
typedef struct structc_tag
{
char c;
double d;
int s;
} structc_t;
这个结构的大小应该是 sizeof(char) + 7 + sizeof(double) + sizeof(int) = 1 +7 + 8 + 4 = 20
然而,正确答案是24
所以,所有结构的起始存储位置必须是结构中对边界要求最严格的数据类型所要求的位置。
struct {
int a;
short b;
int c;
char d;
}A;
struct {
double a;
short b;
int c;
char d;
}B;
上述总结还有另一个表达,组合数据结构的大小是组合结构中最大原子数据大小的整数倍:上述结构B就是20不是8的整数倍,24是8的整数倍
总结:
第一,编译器按照成员列表的顺序给每个成员分配内存.
第二,当成员需要满足正确的边界对齐时,成员之间用额外字节填充.
第三,结构体的首地址必须满足结构体中边界要求最为严格的数据类型所要求的地址. (也即首地址为最宽基本类型的整数倍)
第四,结构体的大小为其最宽基本类型的整数倍.
3、#pragma pack (2)的作用
详细参考链接:为什么要内存对齐?字节对齐和边界对齐介绍。
文章中说的编译器默认4字节对齐是错误的,默认是8字节对齐其它分析都对
这里的指定几字节对齐,意思是数据本身的对齐数大于指定的话就按指定的对齐数来,也就是数据起始地址为指定对齐数的倍数
下面的struct中int a的自身对齐数为4大于指定对齐数2,所以按照2来,它存放的起始地址就为0,2,4,6.。。。这些
4、结构体中嵌套结构体
#pragma pack(8)
struct example1 {
short a;
long b;
};
struct example2 {
char c;
struct example1 e;
short s;
};
struct example2数据结构的大小是16个字节,这个值是这样计算出来的:
1(char c) + 3(padding) + 8(struct example1 e) + 2(short s) + 2(padding) = 16。
该类型也是按照组合类型数据的规则来计算大小
数据结构内存边界对齐的三条原则
二、局部变量,栈中的变量
不同的数据(或数据结构)按顺序从地址大处向地址低处,同一数据内按顺序地址从小到大
sp要8字节对齐,sp的地址只会是8的倍数,(现象就是每次压栈出栈都是8的倍数个,参考下面的)
栈中的数据与全局数据相同的是大小的计算方法一样,不同的是栈中的数据起始地址不是自身对齐值的倍数而是4的倍数或8的倍数(如果自身对齐值小于4就是4的倍数大于4就是8的倍数)
1、结构体变量加lr寄存器大小小于8字节
typedef struct
{
char a;
char b;
} Tchar;
void temp()
{
Tchar a2 = {'!','s'};
a2.a = a2.a + a2.b;
printf("the size of struct Tchar is %d\r\n ",sizeof(a2));
printf("the address of struct Tchar is %p\r\n ",&a2);
printf("the address of struct Tchar a is %p\r\n ",&a2.a);
printf("the address of struct Tchar b is %p\r\n ",&a2.b);
}
int main()
{
temp();
return 1;
}
进入temp()前sp为 0x20000420,执行完Tchar a2 = {‘!’,‘s’};sp为 0x20000418,可以看到汇编指令是执行了
PUSH {r3,lr},执行前是栈指针8字节对齐,执行后也是8字节对齐。
结构体变量加lr寄存器大小小于8字节,也是要在栈中分配8字节让栈指针8字节对齐
2、结构体变量加lr寄存器大小大于8字节,小于16字节
struct naturalalign
{
char a;
short b;
char c;
};
void temp()
{
struct naturalalign a1 = {'a',511,'5'};
a1.b = a1.b + 5;
printf("the size of struct naturalalign is %d\r\n ",sizeof(a1));
printf("the address of struct naturalalign is %p\r\n ",&a1);
printf("the address of struct naturalalign char a is %p\r\n ",&a1.a);
printf("the address of struct naturalalign char b is %p\r\n ",&a1.b);
printf("the address of struct naturalalign char c is %p\r\n ",&a1.c);
}
int main()
{
temp();
return 1;
}
进入temp()前sp为 0x20000420,执行完struct naturalalign a1 = {‘a’,511,‘5’}; sp为 0x20000410,可以看到汇编指令是执行了
PUSH {r2-r4,lr},执行前是栈指针8字节对齐,执行后也是8字节对齐。
结构体变量加lr寄存器大小大于8字节小于16字节,也是要在栈中分配16字节让栈指针8字节对齐
3、结构体数组加lr寄存器大小大于8字节,等于16字节
struct naturalalign
{
char a;
short b;
char c;
};
void temp()
{
struct naturalalign a1[2] = {'a',511,'5'};
printf("the size of struct naturalalign is %d\r\n ",sizeof(a1));
printf("the address of struct naturalalign is %p\r\n ",&a1);
printf("the address of struct naturalalign char a is %p\r\n ",&a1[0].a);
printf("the address of struct naturalalign char b is %p\r\n ",&a1[0].b);
printf("the address of struct naturalalign char c is %p\r\n ",&a1[0].c);
printf("the address of struct naturalalign char a is %p\r\n ",&a1[1].a);
printf("the address of struct naturalalign char b is %p\r\n ",&a1[1].b);
printf("the address of struct naturalalign char c is %p\r\n ",&a1[1].c);
}
int main()
{
temp();
return 1;
}
进入temp()前sp为 0x20000420,执行完struct naturalalign a1[2] = {‘a’,511,‘5’}; sp为 0x20000410,可以看到汇编指令是执行了
PUSH {r1-r3,lr},执行前是栈指针8字节对齐,执行后也是8字节对齐。
结构体数组加lr寄存器大小等于16字节,也是要在栈中分配16字节让栈指针8字节对齐
4、两个结构体加lr寄存器大小大于8字节,等于16字节
struct naturalalign
{
char a;
short b;
char c;
};
void temp()
{
struct naturalalign a1 = {'a',511,'5'};
struct naturalalign a2 = {'s',511,'b'};
printf("the size of struct naturalalign is %d\r\n ",sizeof(a1));
printf("the address of struct naturalalign is %p\r\n ",&a1);
printf("the address of struct naturalalign char a is %p\r\n ",&a1.a);
printf("the address of struct naturalalign char b is %p\r\n ",&a1.b);
printf("the address of struct naturalalign char c is %p\r\n ",&a1.c);
printf("the address of struct naturalalign char a is %p\r\n ",&a2.a);
printf("the address of struct naturalalign char b is %p\r\n ",&a2.b);
printf("the address of struct naturalalign char c is %p\r\n ",&a2.c);
}
int main()
{
temp();
return 1;
}
进入temp()前sp为 0x20000420,执行完struct naturalalign a1 = {‘a’,511,‘5’};
struct naturalalign a2 = {‘s’,511,‘b’};
sp为 0x20000408,可以看到汇编指令是执行了
PUSH {r0-r4,lr} ,执行前是栈指针8字节对齐,执行后也是8字节对齐。
结构体数组加lr寄存器大小等于16字节,也是要在栈中分配16字节让栈指针8字节对齐
这里做一下对比,如果该两个结构体存放在全局区,是怎么样
struct naturalalign
{
char a;
short b;
char c;
};
struct naturalalign a1 = {'a',511,'5'};
struct naturalalign a2 = {'s',511,'b'};
void temp()
{
printf("the size of struct naturalalign is %d\r\n ",sizeof(a1));
printf("the address of struct naturalalign is %p\r\n ",&a1);
printf("the address of struct naturalalign char a is %p\r\n ",&a1.a);
printf("the address of struct naturalalign char b is %p\r\n ",&a1.b);
printf("the address of struct naturalalign char c is %p\r\n ",&a1.c);
printf("the address of struct naturalalign char a is %p\r\n ",&a2.a);
printf("the address of struct naturalalign char b is %p\r\n ",&a2.b);
printf("the address of struct naturalalign char c is %p\r\n ",&a2.c);
}
int main()
{
temp();
return 1;
}
三、stm32中涉及到数据对齐的地方
0、硬件结构和指令代码
stm32的sp寄存器和pc寄存器的后两位都是为0的,栈指针起码是保持4字节对齐的
由于Cortex-M3架构采用16位和32位指令集,因此其PC指针地址是4字节对齐的,换言之其最低2位一定全是0。指令代码都是4字节对齐的
CM3硬件为了保持8字节对齐,在中断发生时会自动入栈8字节的寄存器数据 退出中断时会自动出栈8字节的寄存器数据
这也是CM3多任务执行的关键
详情可以查看2、task.c中prvInitialiseNewTask()
1、startup_stm32f10xxx.s启动文件
; Amount of memory (in bytes) allocated for Stack
;为Stack分配的内存量(以字节为单位)
; Tailor this value to your application needs
;根据您的应用需求定制此值
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
Stack_Size EQU 0x800 ;2K
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
ALIGN=3 :表示首地址按照 2 的 3 次方对齐,也就是按照 8 字节对齐(SP mod 8 = 0)。
PRESERVE8
THUMB
PRESERVE8 指定当前文件保持堆栈八字节对齐。
THUMB 表示后面的指令是 THUMB 指令集 ,CM4 采用的是 THUMB -2指令集
这里的PRESERVE8 仅仅是一个声明,声明该文件里的堆栈是8字节对齐的,也即__initial_sp的值是8的倍数的地址,怎么保证__initial_sp的值是8的倍数呢?
首先分配的内存段首地址是8的倍数(ALIGN=3),然后分配的size 2k也是8的倍数,栈顶也就是__initial_sp就是8的倍数
为什么要保持堆栈的8字节对齐?
PRESERVE8的参考资料:
为什么要加 REQUIRE8 and PRESERVE8? 栈的8字节对齐
一般4字节对齐也没什么问题,但是在调用第三方库文件,比如在stm32中调用microlib 里的printf()输出float型变量时就会产生问题
为什么会造成上述原因?
根据我粗浅的理解:第三方库为了保持兼容性,既能在32位上使用也能在64位上使用,该第三方库采用的8字节对齐的方式,stm32还是采用4字节对齐就会造成数据跨界
对堆栈8字节对齐问题的讨论
STM32 终极字节对齐解析
2、task.c中prvInitialiseNewTask()
#define portBYTE_ALIGNMENT_MASK ( 0x0007 )
pxTopOfStack = &( pxNewTCB->pxStack[ ulStackDepth - ( uint32_t ) 1 ] );
pxTopOfStack = ( StackType_t * ) ( ( ( portPOINTER_SIZE_TYPE ) pxTopOfStack ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
TaskFunction_t pxCode,
void * pvParameters )
{
/* Simulate the stack frame as it would be created by a context switch
* interrupt. */
pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; /* PC */
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */
pxTopOfStack -= 5; /* R12, R3, R2 and R1. */
*pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */
pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */
return pxTopOfStack;
}
创建任务栈时:
1、分配任务栈空间
通过将栈顶指针变量的后3位变为0,将该指针指向的地址8字节对齐
2、手动初始化栈空间,初始化后还是8字节对齐。
后续R11, R10, R9, R8, R7, R6, R5 and R4手动出栈,保持8字节对齐
xPSP, PC, LR,R12以及R3~R0寄存器的值后续会根据psp栈针自动出栈,保持8字节对齐
每个任务最开始运行时的栈顶还是保持着最开始分配的栈顶
具体代码如下:
__asm void prvStartFirstTask( void )
{
/*1首先是使用了 PRESERVE8,进行 8 字节对齐,这是因为,
栈在任何时候都是需要 4 字节对齐的,而在调用入口得8字节对齐,
在进行C编程的时候,编译器会自动完成的对齐的操作,而对于汇编,
就需要开发者手动进行对齐。*/
/*8字节对齐*/
PRESERVE8
/* Use the NVIC offset register to locate the stack. */
/*向量表开始寄存器地址*/
ldr r0, =0xE000ED08
/* 获取向量表的值*/
ldr r0, [r0]
/*获取MSP的初始值(栈底指针)*/
ldr r0, [r0]
/* Set the msp back to the start of the stack. */
/*初始化MSP*/
msr msp, r0
/* Globally enable interrupts. */
/*使能全局中断*/
cpsie i
cpsie f
dsb
isb
/* Call SVC to start the first task. */
/*触发svc中断启动第一个任务*/
svc 0
nop
nop
}
__asm void vPortSVCHandler( void )
{
/*在进入异常前 会将 把xPSP, PC, LR,R12以及R3~R0寄存器的值压入栈 ,由硬件完成
因为这个函数是不返回,这个可以不关心。
*/
extern pxCurrentTCB;
PRESERVE8
ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */
ldr r1, [r3] /* 加载pxCurrentTCB到r1 */
ldr r0, [r1] /* 加载pxCurrentTCB指向的值到r0,目前r0的值等于第一个任务堆栈的栈顶
ldmia r0!, {r4-r11} /* 以r0为基地址,将栈里面的内容加载到r4~r11寄存器,同时r0会递增 */
msr psp, r0 /* 将r0的值,即任务的栈指针更新到psp 后面异常退出时,根据PSP进行出栈, 就是前面任务栈初始化值出栈给到寄存器*/
isb
mov r0, #0 /* 设置r0的值为0 */
msr basepri, r0 /* 设置basepri寄存器的值为0,即所有的中断都没有被屏蔽 */
orr r14, #0xd /* 当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D,
使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */
bx r14 /* 异常返回,这个时候栈中的剩下内容将会自动加载到CPU寄存器:
xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
同时PSP的值也将更新,即指向任务栈的栈顶 */
}
当任务切换时,和任务栈初始化入栈到任务启动出栈差不多,也是手动和自动两部分,都能保持8字节对齐
具体入栈出栈的流程如下:
R11, R10, R9, R8, R7, R6, R5 and R4手动出栈,保持8字节对齐
xPSP, PC, LR,R12以及R3~R0寄存器的值后续会根据psp栈针自动出栈,保持8字节对齐
代码如下:
__asm void xPortPendSVHandler( void )
{
extern pxCurrentTCB;
extern vTaskSwitchContext;
PRESERVE8
/* 当进入PendSVC Handler时,上一个任务运行的环境即:
xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
这些CPU寄存器的值会自动保存到任务的栈中,剩下的r4~r11需要手动保存 */
/* 获取任务栈指针到r0 */
mrs r0, psp
isb
ldr r3, =pxCurrentTCB /* 加载pxCurrentTCB的地址到r3 */
ldr r2, [r3] /* 加载pxCurrentTCB到r2 */
stmdb r0!, {r4-r11} /* 将CPU寄存器r4~r11的值存储到r0指向的地址 */
str r0, [r2] /* 将任务栈的新的栈顶指针存储到当前任务TCB的第一个成员,即栈顶指针 */
//以上 上下文保存
stmdb sp!, {r3, r14} /* 将R3和R14临时压入堆栈,因为即将调用函数vTaskSwitchContext,
调用函数时,返回地址自动保存到R14中,所以一旦调用发生,R14的值会被覆盖,因此需要入栈保护;
R3保存的当前激活的任务TCB指针(pxCurrentTCB)地址,函数调用后会用到,因此也要入栈保护 */
mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY /* 进入临界段 */
msr basepri, r0
dsb
isb
bl vTaskSwitchContext /* 调用函数vTaskSwitchContext,寻找新的任务运行,通过使变量pxCurrentTCB指向新的任务来实现任务切换 */
mov r0, #0 /* 退出临界段 */
msr basepri, r0
ldmia sp!, {r3, r14} /* 恢复r3和r14 */
ldr r1, [r3]
ldr r0, [r1] /* 当前激活的任务TCB第一项保存了任务堆栈的栈顶,现在栈顶值存入R0*/
ldmia r0!, {r4-r11} /* 出栈 */
msr psp, r0
isb
bx r14 /* 异常发生时,R14中保存异常返回标志,包括返回后进入线程模式还是处理器模式、
使用PSP堆栈指针还是MSP堆栈指针,当调用 bx r14指令后,硬件会知道要从异常返回,
然后出栈,这个时候堆栈指针PSP已经指向了新任务堆栈的正确位置,
当新任务的运行地址被出栈到PC寄存器后,新的任务也会被执行。*/
nop
}
四、heap堆内存