嵌入式内存栈 (汇编随记)
嵌入式内存栈 (汇编随记)
version : v1.0 「2022.7.28」 最后补充
author: Y.Z.T.
简介: 随记 , 内存空间分布 与 函数调用过程中的栈帧变化
1️⃣ 内存空间分布
可执行文件(.o
)在被加载到内存中时 , 内存空间分布图:
- 函数 翻译成二进制指令放在 代码段 中
- 初始化的全局变量 和 静态局部变量 放在 数据段 中(.data)
- 未初始化的全局变量 和 静态变量 放在 BSS段 中(.bss)
- 函数的 局部变量 保存在 栈 中
- 使用
malloc
申请的 动态内存 保存在 堆空间 中
2️⃣ 栈的使用
栈的基本操作: 入栈(push)和出栈(pop)
空栈与满栈:
- 根据栈指针
SP
指向栈顶元素的不同, 栈可以分成满栈和空栈- 满栈的栈指针
SP
总是指向栈顶元素- 空栈的栈指针总是指向栈顶元素上方的可用元素。
递增栈和递减栈:
- 根据栈的生长方向不同 , 栈可以分成递增栈和递减栈
- 一个元素入栈时, 递增栈的栈指针从低地址向高地址增长
- 一个元素入栈时, 递减栈的栈指针从高地址向低地址增长
栈的作用 :
- 栈是C语言运行的基础
- C语言函数中的局部变量、传递的实参、返回的结果、编译器生成的临时变量都是保存在栈中的
- 所以在系统一上电会先运行汇编代码 , 先初始化栈空间 , 再跳入第一个C语言函数
ARM处理器使用的是满递减栈
防止栈溢出 :
- 尽量不要在函数内使用大数组,如果确实需要大块内存,则可以使用
malloc
申请动态内存。- 函数的嵌套层数不宜过深。
- 递归的层数不宜太深。
栈的分类
满递减栈入栈操作
2.1 函数调用
2.1.1 栈帧
每个函数的栈空间被称为栈帧
说明:
- 每个栈帧都使用两个寄存器维护 , 其中
FP
指向栈帧底部 ;SP
指向栈帧的顶部
- SP总是指向当前正在运行函数栈帧的栈顶 ; FP总是指向当前运行函数的栈底
- 栈帧用于保存局部变量和实参, 还用于保存函数的上下文
例如
main
函数调用了f()
函数
main()的 基址FP , main()的**返回地址LR都保存在f()函数的栈帧中
上级函数栈帧的起始地址(FP), 即栈底会保存在当前函数的栈帧中
多个栈帧通过FP构成一个链 , 就是某个进程的函数调用栈
当函数运行结束后, 当前函数的栈帧空间就会释放, SP/FP指向上一级函数栈帧
示例:
C语言原型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 int g (void)
{
int x = 100;
int y = 200;
return 300;
}
int f (void)
{
int 1 = 20;
int m = 30;
int n = 40;
g();
return 50;
}
int main (void)
{
int i= 2;
int j = 3;
int k = 4;
f();
return 0;
}汇编代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42 <main>
push {fp,lr} ;将当前fp所指向的地址,即main的上级函数栈帧基址压入堆栈 ; 同时将mian的上级返回地址压入堆栈
add fp,sp #4 ;将fp指向当前sp指针的前一个地址(即保存基址FP的地方)
sub sp,sp,#16 ;开辟栈帧空间 , 将SP指向栈顶(即②位置),
mov r3,#2 ;
str r3,[fp,#-16] ;将局部变量2 压入FP偏移4个地址的位置
... ; 同理压入其他局部变量
...
bl(f的地址)<f> ; 调用f()函数(带链接跳转 , 跳到f()所在的地址)同时将pc值保存在LR寄存器
mov r3 #0 ;返回值
mov r0 r3 ;保存返回值
mov sp,fp,#4 ;将SP指向上级函数栈帧栈顶,即位置①(出栈先移动SP指针,再弹出数据)
pop {fp,pc} ;将FP、LR的值依次弹出到FP、PC寄存器。实现跳转回上级函数
----------------------------------------------
(f的地址)<f> ;函数f的内存地址
push {fp,lr} ;将当前fp所指向的地址,即main的栈帧基址压入堆栈 ; 同时将mian的返回地址LR压入堆栈
add fp,sp #4 ;将fp指向当前sp指针的前一个地址(即保存基址FP的地方)
sub sp,sp,#16 ;开辟栈帧空间 , 将SP指向栈顶(即②位置),
... ;同理将局部变量压入栈中
...
bl(g的地址)<g> ;调用g()函数,同时将pc值保存在LR寄存器
mov r3 #50 ;返回值
mov r0 r3 ;保存返回值
mov sp,fp,#4 ;同理,将SP指向上级函数栈帧栈顶
pop {fp,pc} ;同理,将FP、LR的值依次弹出到FP、PC寄存器。实现跳转回main函数
-------------------------------------------------
(g的地址)<g>
push {fp} ; 同理将 f()的基址FP压入堆栈 (即str fp ,[sp,#-4]!)
; 因为g函数已经不再跳转,所以LR寄存器的值一直是f()的返回地址,所以不需要压入LR
add fp,sp,#0 ;同理
sub sp,sp,#12 ;同理将sp指向栈顶
... ;同理保存局部变量
...
mov r3,#300 ;返回值
mov r0,r3
sub sp,fp,#0 ;sp = fp - 0
pop {fp} ;将FP的值弹到fp寄存器 , 即将fp指向f()函数基址(ldr fp,[sp],#4)
bxlr ;直接将lR寄存器的值赋给PC指针 , 实现返回上一级f()函数中运行
2.2 参数传递
ARM处理一般会使用寄存器来传递参数
- 在函数调用过程, 当要传递的参数个数小于4时 , 会直接使用r0~r3寄存器传递
- 当要传递的参数个数大于4时, 前4个参数使用寄存器传递 , 剩余的参数则压入堆栈保存
- C语言默认参数传递是从右到左 , 栈的清理方是函数调用者
示例:
C语言程序原型
1 | int f(int ag, int ag2, int ag3, int ag4, int ag5, int ag6) |
汇编语言
1 | (f的地址)<f> |
-------------已经到底啦! -------------