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 LRPOP {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 模式 */mrs r0, cpsrbic 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, =0x01ldr r1, =0x02ldr r2, =0x03ldr r3, =0x04ldr lr, =0x05push {r0-r3, lr}/* nop for debug */nopnopnopmov r0, #0mov r1, #0mov r2, #0mov r3, #0mov lr, #0/* nop for debug */nopnopnoppop {r0-r3, lr}/* nop for debug */nopnopnopstmfd sp!, {r0-r3, lr}/* nop for debug */nopnopnopmov r0, #0mov r1, #0mov r2, #0mov r3, #0mov lr, #0/* nop for debug */nopnopnopldmfd sp!, {r0-r3, lr}/* nop for debug */nopnopnoploop:b loop