ARM 栈帧
本系列均以 corter-A7(armv7-a) 为例
在 ARM 中,通常为满减栈(Full Descending FD), 也就是说,堆栈指针指向堆栈内存中最后一个填充的位置,并且随着每个新数据项被压入堆栈而递减。
栈的本质
要理解栈的本质,首先得明白,程序执行是怎么执行的?
程序执行其实就是对应一个又一个状态机,在 A 状态有对应的内核寄存器,在 B 状态也有对应的内核寄存器值。
A 状态切换到 B 状态,就需要把 A 状态的内核寄存器保存下来,不然无法从 B 状态再回到 A 状态。此时就需要将 A 状态下内核寄存器的值放到内存中,即入栈
B 状态切换到 A 状态,需要恢复 A 状态的现场,即内核寄存器的值,所以需要将存到内存中的数据写回到内核寄存器,即出栈
其实 linux/rtos 的任务也是一个又一个状态机,状态切换的时候,先将当前状态(A)的内核寄存器保存到内存中,再把下一个状态(B)的内核寄存器从内存中读回来,程序跳转到 B 状态执行,其实这就是上下文切换的过程。
入栈/出栈的本质其实就是内存中的一块区域进行操作:
- 需要入栈的时候,就把内核寄存器的值写入到内存中
- 需要出栈的时候,就把相应内存中的值写回到内核寄存器中
入栈
在调用入栈指令 PUSH/STMFD 之前,堆栈指针(SP)指向堆栈中最后一个被占用的字节,入栈指令完成后,堆栈指针被递减8(两个字),并且这两个字的内容被写入内存。
编号低的寄存器写入内存低地址处
。
出栈
假设要将:R0-R3 和 LR 寄存器入栈,当前的 SP 指针指向 0x80000000,由于 ARM 处理器的栈是向下增长的,使用的汇编代码如下:PUSH {R0-R3,LR}
入栈之后,内存分布如下
假设这个时候又对 LR 进行入栈操作,汇编代码如下:PUSH LR
入栈之后,内存分布如下
上面两个图就是分步对 R0-R3,R12,及 LR 进行压栈以后的编程模型,如果要出栈的话,就使用如下代码:
POP LR
POP {R0-R3,R12}
出栈的时候从栈顶开始,也就是 SP 当前执行的位置,地址依次增大,提取堆栈中的数据到要恢复的寄存器列表中。
PUSH 和 POP 的另一种写法是 STMFD SP! 和 LDMFD SP!
因此上面的代码可以写为
入栈
STMFD SP!,{R0~R3, R12}
STMFD SP! ,{LR}
出栈
LDMFD SP! ,{LR}
LDMFD SP! ,{R0~R3, R12}
示例测试
gdb 小技巧:
- 窗口缩小:winheight src -5
- 窗口放大:winheight src +5
入栈
入栈之前先查看当前寄存器
-
Info reg:从内核寄存器中可以看到,当前 r0 r1 r2 r3 lr 寄存器的值依次为:1 2 3 4 5
sp 指针当前为0x801fffec -
x/20 0x801fffec 查看 SP 栈指针地址数据
可以看到 SP 地址数据从高到低依次为:5 4 3 2 1
从这里可以看到,编号低的寄存器写入内存低地址处
寄存器清零
出栈
源码
.global _start
_start:
/* 进入 SVC 模式(其实复位之后,系统就处于 SVC 模式,这里也可以不切换,设置 SP 的值) */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将 r0 寄存器中的低 5 位清零,也就是 cpsr 的 M0~M4 */
orr r0, r0, #0x13 /* r0 或上 0x13,表示使用 SVC 模式 */
msr cpsr, r0 /* 将 r0 的数据写入到 cpsr 中 */
ldr sp, =0X80200000 /* 设置栈指针 */
ldr r0, =0x01
ldr r1, =0x02
ldr r2, =0x03
ldr r3, =0x04
ldr lr, =0x05
push {r0-r3, lr}
/* nop for debug */
nop
nop
nop
mov r0, #0
mov r1, #0
mov r2, #0
mov r3, #0
mov lr, #0
/* nop for debug */
nop
nop
nop
pop {r0-r3, lr}
/* nop for debug */
nop
nop
nop
stmfd sp!, {r0-r3, lr}
/* nop for debug */
nop
nop
nop
mov r0, #0
mov r1, #0
mov r2, #0
mov r3, #0
mov lr, #0
/* nop for debug */
nop
nop
nop
ldmfd sp!, {r0-r3, lr}
/* nop for debug */
nop
nop
nop
loop:
b loop