嵌入式内存栈 (汇编随记)

嵌入式内存栈 (汇编随记)


version : v1.0 「2022.7.28」 最后补充

author: Y.Z.T.

简介: 随记 , 内存空间分布 与 函数调用过程中的栈帧变化



1️⃣ 内存空间分布

可执行文件(.o)在被加载到内存中时 , 内存空间分布图:

image-20220924001131747


  • 函数 翻译成二进制指令放在 代码段
  • 初始化的全局变量静态局部变量 放在 数据段 中(.data)
  • 未初始化的全局变量静态变量 放在 BSS段 中(.bss)
  • 函数的 局部变量 保存在
  • 使用malloc申请的 动态内存 保存在 堆空间


2️⃣ 栈的使用


栈的基本操作: 入栈(push)和出栈(pop)

空栈与满栈:

  • 根据栈指针SP 指向栈顶元素的不同, 栈可以分成满栈空栈
  • 满栈的栈指针SP 总是指向栈顶元素
  • 空栈的栈指针总是指向栈顶元素上方的可用元素。

递增栈和递减栈:

  • 根据栈的生长方向不同 , 栈可以分成递增栈递减栈
  • 一个元素入栈时, 递增栈的栈指针从低地址向高地址增长
  • 一个元素入栈时, 递减栈的栈指针从高地址向低地址增长

栈的作用 :

  • 是C语言运行的基础
  • C语言函数中的局部变量、传递的实参返回的结果、编译器生成的临时变量都是保存在栈中的
  • 所以在系统一上电会先运行汇编代码 , 先初始化栈空间 , 再跳入第一个C语言函数

ARM处理器使用的是满递减栈


防止栈溢出 :

  • 尽量不要在函数内使用大数组,如果确实需要大块内存,则可以使用malloc申请动态内存。
  • 函数的嵌套层数不宜过深。
  • 递归的层数不宜太深。

栈的分类

image-20221003131823961


满递减栈入栈操作

image-20221003131921334



2.1 函数调用

2.1.1 栈帧

每个函数的栈空间被称为栈帧


说明:

  • 每个栈帧都使用两个寄存器维护 , 其中FP 指向栈帧底部 ; SP 指向栈帧的顶部

image-20221003141511274

  • 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()函数中运行
    image-20221003144546960


2.2 参数传递

ARM处理一般会使用寄存器来传递参数

  • 在函数调用过程, 当要传递的参数个数小于4时 , 会直接使用r0~r3寄存器传递
  • 当要传递的参数个数大于4时, 前4个参数使用寄存器传递 , 剩余的参数则压入堆栈保存
  • C语言默认参数传递是从右到左 , 栈的清理方是函数调用者

示例:

C语言程序原型

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

汇编语言

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
(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指针指向栈顶,以及将跳转


-------------已经到底啦! -------------