1. 栈的概念
栈,本身是一段内存,程序运行时用于保存一些临时数据,如局部变量、参数、返回地址等等。
学习了数据结构,对栈的概念相信大家都不陌生,后进先出的数据结构,即最后进栈的元素最先出栈。但是在C语言中,栈是由编译器自动管理的。这一点与堆相反,堆是由程序员手动分配和释放的,例如malloc和free。而栈是由编译器自动管理的。
当程序调用一个函数时,编译器会为该函数的局部变量、参数和返回地址等信息在栈上分配一段空间,并将这些信息压入栈中。当函数执行完毕后,这些信息会被弹出栈,并回到调用该函数的位置继续执行代码。
在C语言中我们不需要了解栈的工作方式,但是学习了ARM底层架构,就要了解栈在程序中的工作过程。
2. 栈的分类
在学习数据结构,没有听说过栈还有什么分类,通常是将栈和队列一起讲的,而在底层技术中,栈其实有很多种分类,这些分类决定了栈的特点和使用方式,而C语言中不需要关心,所以我们没有了解。那么栈有哪几种分类呢?他们之间的区别是什么?
首先我们要了解,CPU要使用栈,那么就要知道栈的地址在哪?栈是由栈指针来管理的,栈指针指向栈的顶部。栈指针的地址通常由编译器或操作系统来分配,对于ARM来讲,在汇编语言中,可以使用寄存器SP来表示栈指针,所以当使用栈时,找到SP就能知道栈指针指向的地址了。
2.1 增栈与减栈
增栈:压栈操作将数据放入栈顶,栈顶指针向下移动;减栈:压栈操作将栈顶数据弹出,栈顶指针向上移动。
- 对于增栈,存数据时,SP指向的地址越来越大;取数据时,SP指向的地址越来越小
- 对于减栈,存数据时,SP指向的地址越来越小;取数据时,SP指向的地址越来越大
可以理解为SP最开始指向中间某个地址,增栈意味着向地址大的压入,而减栈意味着向地址小的压入。
2.2 满栈与空栈
满栈:栈指针指向最后一次压入到栈中的数据,压栈时需要先移动栈指针到相邻的位置再压栈
空栈:栈指针指向最后一次压入到栈中的数据的相邻位置,压栈时可直接压栈,之后才需要把栈指针移动到相邻位置
简单来说,
- 对于满栈,刚开始的时候栈指针指向的地址有元素,指向的是最后一个压入栈中的数据,所以先移动栈指针,才能继续压栈。
- 对于空栈,刚开始的时候栈指针指向的地址无元素,指向的是最后一个压入栈的数据的后一个地址,所以可以先进行压栈,后再移动栈指针。
2.3 四种栈的分类
根据2.1和2.2,我们可以知道栈的两种地址类型和两种数据方式,将这两种大类进行合并,我们可以知道,栈有四种类型:空增(EA)、空减(ED)、满增(FA)、满减(FD)对于ARM处理器一般使用满减栈
2.4 多寄存器内存访问指令的寻址方式
在寄存器中,用于将多个寄存器的数据存储到连续的一块内存中
STMIA和STMIB
这两种都是从低地址向高地址存放数据
- STMIA:指针指向哪,就从那开始以由低到高地址存数据
- STMIB:指针先指向的下一个地方开始,再由低到高地址存数据
STMDA和STMDB
这两种都是从高地址向低地址存放数据
- STMDA:指针指向哪,就从那开始以由高到低地址存数据
- STMDB:指针先指向的下一个地方开始,再由高到低地址存数据
思考:如上我们知道,ARM存储器一般使用满减栈,那么我们如何选择上述寻址方式?
回忆一下满减(FD),对于满减是先移动指针,再进行存入数据,且存入数据时,地址越来越小,可以很容易得到,满减对应的是STMDB
2.5 如何读取栈里的数据?
对于满减,后进先出,那么我们从地址低的读到地址高的,且从指针的指向开始,那么满减对应的应该是LADIA
示例代码:
MOV R1, #1
MOV R2, #2
MOV R3, #3
MOV R4, #4
MOV R11, #0x40000020
STMDB R11!, {R1-R4}
LDMIA R11!, {R6-R9}
@ STMFD R11!, {R1-R4}
@ LDMFD R11!, {R6-R9}
因为已知是满减,所以可以直接使用FD作为结尾也是没有问题的。但是要记住,虽然可以直接使用满减后缀,但是编译器也是会自动转换成第一种方法的。
得到:
3. 栈的应用举例
3.1 叶子函数的调用
@ 初始化栈指针
MOV SP, #0x40000020
MAIN:
MOV R1, #3
MOV R2, #5
BL FUNC
ADD R3, R1, R2
B stop
FUNC:
@ 压栈保护现场
STMFD SP!, {R1, R2} @ 满减栈
MOV R1, #10
MOV R2, #20
SUB R3, R2, R1
@ 出栈恢复现场
LDMFD SP!, {R1, R2}
MOV PC, LR
结果:
这段代码作用是:先初始化栈指针,0x40000020初始化地址,在FUNC函数中,首先使用STMFD指令将R1和R2的值压入栈中,以保护现场。以免等下重新赋值时覆盖了原有的值。然后计算R2-R1的值,并将结果存储在R3中。最后使用LDMFD指令将R1和R2的值从栈中弹出,以恢复现场。
3.2 非叶子函数的调用
@ 初始化栈指针
MOV SP, #0x40000020
MAIN:
MOV R1, #3
MOV R2, #5
BL FUNC1
ADD R3, R1, R2
B stop
@ FUNC1:
STMFD SP!, {R1, R2, LR} @ 满减栈
MOV R1, #10
MOV R2, #20
BL FUNC2
SUB R3, R2, R1
LDMFD SP!, {R1, R2, LR}
MOV PC, LR
FUNC2:
STMFD SP!, {R1, R2}
MOV R1, #7
MOV R2, #8
MUL R3, R1, R2
LDMFD SP!, {R1, R2}
MOV PC, LR
非叶子函数,可能会再次覆盖之前入栈的数据,那么就要在入栈的程序后,在下一个程序中紧跟着入栈。
这段ARM汇编代码演示了如何在函数调用期间使用栈来保护现场和恢复现场,并且演示了如何嵌套调用多个函数。该代码使用STMFD和LDMFD指令将当前函数的寄存器值保存到栈中,并在函数返回时将这些值从栈中弹出,以恢复现场,具体的动态结果可以自行演示!