# 栈、栈帧与函数调用

## 栈

栈是一个先入后出队列。关于算法栈，请自行搜索或参考：[https://zh.wikipedia.org/wiki/堆栈](https://zh.wikipedia.org/wiki/%E5%A0%86%E6%A0%88) 。\
在操作系统中，一般用栈来保存函数的状态和局部变量。

Linux的栈位于程序内存空间的末端，从**高地址向低地址**增长。\
在x86架构中，使用esp寄存器指向栈顶的内存地址；在x64架构中，使用rsp寄存器指向栈顶。一般可以简称为sp。

## 栈帧

栈帧是指函数在被调用时，所拥有的一块独立的用于存放函数所使用的状态和变量的栈空间。

每个函数都对应有一个栈帧。同一个函数多次进入，每次可能会分配到不同的栈帧。整个栈的内容在同一个时刻可以看作是由许多栈帧依序“堆叠”组成的。

对于一个运行中的函数，其使用的栈帧区域被sp和bp寄存器限定（对于x86，sp等价esp，bp等价rsp；对于x64，sp等价rsp，bp等价rbp）。bp指向栈帧的底部，sp指向栈帧的顶部。

![](https://1080702507-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LjOgFtBxUgZGl51CO9N%2F-LjOgm7HmmTWEbBKRKHH%2F-LjOgn4Pl-EzpIs0RCEc%2Fstack-20170713.png?generation=1562725529515765\&alt=media)\
（图片来源：<http://witmax.cn/c-function-heap-stack.html>）

在函数中使用的所有变量（本地变量、实参），一般使用bp定位。设N为整型字节数，bp+2N是第一个实参的地址，bp-N是第一个本地变量的地址。

## 函数调用

一个函数调用，一般需要以下步骤

1. 保存函数的实参
2. 保存子函数结束后，需要返回的地址（返回到哪里）
3. 保存父函数的栈帧信息
4. 在栈上开辟空间供局部变量使用
5. 执行函数实现的功能
6. 释放局部变量使用的空间
7. 根据保存的父函数栈帧信息，恢复父函数栈帧
8. 根据保存的返回地址，恢复父函数执行流，一般是函数调用指令后的下一条指令

共有多种函数调用方式，这里介绍一种：`stdcall`。stdcall是标准调用约定，还有其他调用约定：cdecl、PASCAL、fastcall、thiscall，可以自行搜索。

在stdcall中，调用一个函数func(a,b,c)会有一些比较固定的代码（x86架构）。

父函数调用时，会把参数从右至左入栈，实现保存函数实参的功能：

```
push c  
push b  
push a
```

然后执行call指令：

```
call func
```

这里call指令内部实际上做了两个工作，一个是将这个call指令的下一条语句入栈，实现返回地址的保存。然后把执行流跳转到函数里。所以一个call指令从功能上可以拆分为以下两个指令：

```
push 本call指令下一条指令的地址  
jmp func
```

执行流到了func函数内部，会先进行父函数栈帧信息的保存。此时esp和ebp依然维持父函数的栈帧。\
当前子函数所有的栈中变量被释放后，esp会回到函数调用前的状态，因此无需保存esp，只要保存ebp的信息即可。

```
push ebp
```

此时，子函数的栈帧底部变到esp处：

```
mov ebp, esp
```

栈帧底部设置完毕后，可以为局部变量开辟空间，这里开辟了一个32（0x20）字节的栈空间：

```
sub esp, 20h
```

注意栈是从高地址到低地址增长的，故这里做减法。

然后就可以开始进行函数的操作，通过ebp定位函数的参数和局部变量空间，并将函数返回值放在eax寄存器中。

在函数的功能代码全部执行完毕后，释放之前开辟的栈空间：

```
add esp, 20h
```

此时esp恢复到压入ebp之后的状态，这时栈顶为父函数的ebp值，可以依据这个信息恢复父函数的ebp，进而恢复栈帧：

```
pop ebp
```

当前栈顶为返回地址，这时父函数的栈信息已经恢复，只要根据这个返回地址更改执行流，回到父函数call func指令的下一条指令即可。

```
retn
```

至此，一个函数的调用流程结束，栈的状态和调用前完全一致，子函数的返回值被存在eax寄存器中。
