嵌入式内存栈 (汇编随记)
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语言原型
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; }
汇编代码:
<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语言程序原型
int f(int ag, int ag2, int ag3, int ag4, int ag5, int ag6)
{
int s = 0;
S = agl + ag2 + ag3 + ag4 + ag5 + ag6;
return s;
}
int main(void)
{
int sum =0;
f(1, 2, 3, 4, 5, 6);
printf(" sum:%d\n", sum);
return 9;
}
汇编语言
(f的地址)<f>
push {fp} ; 将 main()的基址FP压入堆栈 (即str fp ,[sp,#-4]!)
add fp,sp,#0
sub sp,sp,#28
str r0,[fp,#-16] ;将main函数通过寄存器r0~r3传递的实参1,2,3,4保存到自己的函数栈帧中
str r1,[fp,#-20] ; r1
... ; r2
... ; r3
ldr r2,[fp,#-16] ;准备进行累计计算, 将栈帧内的数加载到寄存器
ldr r3,[fp,#-20] ;加载到寄存器
add r2,r2,r3 ;r2 = r2 + r3
... ;同理,累计 s = 1+2+3+4;
ldr r3,[fp,#4] ;fp寄存器向后偏移,到上一级函数的栈帧中获取要传递的实参(5)
add r2,r2,r3 ;r2 = r2 + r3
ldr r3,[fp,#8] ;读取main栈帧内的实参(6)
add r3,r2,r3 ;r3 = r2 + r3
str r3,[fp,#-8] ;将运算结果存入堆栈
ldr r3, [fp, #-8]
mov r0,r3 ;传递返回值
sub sp,fp,#0 ;sp指针指向上级函数栈顶
pop {fp} ;fp指向上级函数基址
bxlr ;跳转回函数调用地址
----------------------------------
<main>
push {fp,lr} ;同上,保存main上级基址和上级函数调用地址lr
add fp,sp,#4 ;将fp指向sp的前一地址位置
sub sp,sp,#16 ;开辟栈帧空间 , 将SP指向栈顶
mov r3,#0 ;保存局部变量sum = 0
str r3,[fp,#-8]
mov r3,#6 ;将传递的参数列表从右向左保存,先将实参6压入mian函数的栈帧
str r3,[sp,#4] ;
mov r3,#5 ;将实参5压入栈
str r3,[sp] ;
mov r3,#4 ;其他实参通过寄存器传递
mov r3,#3 ;3
... ;2
... ;1
bl(f()的地址)<f> ;调用f(),同时将pc的值保存在LR中
... ;同上,将sp指针指向栈顶,以及将跳转