提示:本文内容参考慕课课程:《ARM微控制器与嵌入式系统》
一、堆栈Stack
1.堆栈的概念
堆栈是一段连续的存储器空间,堆栈按照后入先出的方式工作(Last In First Out),只能向/从堆栈的顶部加入或取出数据,即堆栈能够保持数据的顺序。
• 堆:是一个进程开启以后,系统分配给它的间,一般这个空间是全局的,系统中所有动态分配的对象(比如指针)都是在这个空间上分配。堆里的数据是有数据结构的,其空间占用是不连续的。在没有OS的嵌入式系统中,通常不使用堆。
• 栈:是一个线程维护的一个先进后出的数据构,主要用于静态变量的分配及维护程序各个函数调用时使用。栈里的内容是没有数据结构的,其空间占用是连续的。栈是微处理器的必要特性,是支持高级语言编程的前提。
堆栈有两种基本的操作方式:
推入PUSH: 将内容加入到堆栈顶端
取出PULL : 将堆栈顶端的内容取出
对于大多数CPU而言,“顶端”是指
低位的地址空间
2.堆栈的作用
• 汇编程序可以使用堆栈来保存局部变量,寄存器值。
• C语言编译器使用堆栈来完成参数传递和返回值传递 →C语言的函数调
• CPU硬件 使用堆栈来保存返回地址和寄存器上下文(register context) →中断
3.堆栈指针寄存器Stack Pointer
1.堆栈顶端位置通过CPU内的堆栈指针寄存器确定(SP,Stack Pointer)。
2.堆栈指针的初始位置由程序代码确定,指向预先划定的堆
栈空间的底部
3.如果要自己操作堆栈,记住:
· Once push must pull
· Last In First Out
在使用C语言时,我们对于堆栈的绝大多数使用是感觉不到的,因为C语言的函数调用,函数的返回值,参数传递,局部变量的开销都是由编译器帮我们隐性的使用了堆栈。
开发工具会写出代码初始化堆栈,C语言编译器会隐性使用堆栈,用汇编编写中断时,需要注意堆栈。
4.举例
先了解一些汇编的基本指令:
- NOP: no operation,浪费一个时钟周期什么都不做
- LDS : load stack pointer with a value,给SP指针赋值
- PSHx: push the contains of register ‘x’ into stack,把内部的x寄存器的值入栈
- PULLx: pull from the stack and put in register ‘x’,从堆栈弹出一个数到寄存器x
- JSR : jump to subroutine,跳转到一个子函数
- RTS : return from subroutine,从子函数返回
下面我们将分析一段代码在16bitCPU的执行过程中堆栈的变化
0x3005: NOP
0x3006: LDS $2000
0x3008: PSHA
0x3009: PSHB
0x300A: JSR SubFunc
0x300C: PULLA
0x300D: PULLB
………..…
SubFunc: 0x4050: NOP
0x4052: RTS
解析过程如下:
1初始状态:
2执行以下代码,完成堆栈初始化,指定闲置内存空间给堆栈用
0x3005: NOP
0x3006: LDS $2000
SP指针初始化为0x2000,即0x2000以上都可以用作堆栈,PC指向下一条待执行的指令地址0x3008(因为下一条指令的地址为0x3008)
3执行以下代码,把A的值入栈
0x3008: PSHA
A寄存器的值被放到了堆栈里,0x1FFF里存储了数据,SP指针寄存器变为0x1FFF,同样PC指针寄存器指向下一条待执行指令的地址
4执行以下代码,原理同上,不再赘述
0x3009: PSHB
5执行以下代码,跳转到子程序SubFunc
0x300A: JSR SubFunc
堆栈里多了0x30和0x0C,两个字节合起来是一个16bit的完整地址0x300C
即在函数调用的一瞬间,
PC指针指向子函数的入口地址0x4050;CPU自动向对堆栈里压了两个字节(0x300C是调用子程序下一条指令的地址)因而SP指针向上移两位为0x1FFC.
6执行以下代码,执行子程序
SubFunc: 0x4050: NOP
PC指针指向子函数的下一条指令地址0x4052
7执行以下代码,从子函数返回
0x4052: RTS
堆栈指针寄存器释放了两个单元,把30和0C的值赋给了PC指针寄存器(0x300C是调用子程序下一条指令的地址),相应的SP指针向下变成0x1FFE
8执行以下代码
0x300C: PULLA
从堆栈弹出到寄存器A,即A=0x56
堆栈释放一个单元,SP下移变为0x1FFF
PC指针指向下一条指令的地址
9执行以下代码,原理同上,不再赘述
0x300D: PULLB
这个完整的程序实现了到子函数的跳转以及AB寄存器内值的交换
二、ARM Cortex M0+ CPU(32bits)
1. 基本指令
1. NOP: no operation,浪费一个时钟周期什么都不做
2. MOVS : move data, update APSR register,给寄存器赋值
3. PUSH: push the contains of register into stack,把寄存器的放入堆栈
4. POP: pop from the stack and put in register,从堆栈弹出给寄存器
5. BL : jump to subroutine, update LR,跳转到子程序
6. BX : return from subroutine, with LR value ,根据Link Register里的值进行跳转函数的返回
2. 举例
下面我们将分析以下代码在ARM Cortex M0+ CPU(32bits)的执行过程中堆栈的变化
00000802: nop
00000804: movs r4,#18
00000806: movs r5,#52
00000808: push {r4}
0000080a: push {r5}
0000080c: bl SubFunc
00000810: pop {r4}
00000812: pop {r5}
SubFunc:00000910: nop
00000912: bx lr
解析过程如下:
1执行以下代码:
00000802: nop
内存空间里每个单元里可以存一个32bit的数,所以地址以4来累加的。
2执行以下代码,把10进制的18存入r4寄存器内,换成16进制就是0x12
00000804: movs r4,#18
3执行以下代码,原理同上
00000806: movs r5,#52
4执行以下代码,把寄存器r4,r5的值压入堆栈
00000808: push {r4}
0000080a: push {r5}
5执行以下代码,跳转到SubFunc函数
0000080c: bl SubFunc
函数跳转瞬间,SP指针指向子函数第一条指令的地址即00000910
返回地址保存在了Link Register里,保存为0x0811,这是由于ARM体系的向下兼容性,当保存的返回地址的最低位是1时,函数调用
和任何跳转返回的时候,CPU仍然工作在Thumb指令集的状态下,最低为变为0就表示CPU应处于ARM指令集模式。所以存的是0x0811,实际信息是回到Thumb模式,回到0x0810地址。
6执行以下代码,执行子函数,返回主程序
SubFunc:00000910: nop
00000912: bx lr
所以PC指针寄存器指向了跳转函数下一条指令地址即0x0810
7执行以下代码
00000810: pop {r4}
00000812: pop {r5}
虽然值还在堆栈里,但是每执行一条pop指令,SP指针寄存器向下移动一个。下一次那些值将会被覆盖掉。
程序的功能为交换r4,r5寄存器的值。因为堆栈要求先入后出。