---
title: "函数调用 压栈的工作原理"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/cpp-function-call-stack-mechanism/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/02-编程语言与运行时/C++/函数调用 压栈的工作原理.md"
content_hash: 7b07bb593ae9b44845d96db7c6837e78176d3453989ecafecd46775eb8ed937c
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# 函数调用 压栈的工作原理

﻿# 函数调用 压栈的工作原理

> 创建时间：2020/3/15 17:16

参考：https://blog.csdn.net/u011555996/article/details/70211315
https://blog.csdn.net/qq_34771252/article/details/90547751

> **PE文件** (Portable Executable),可移植的可执行的文件,是微软Windows操作系统上的程序文件（可能是间接被执行，如DLL）
>  eg. EXE、DLL、OCX、SYS、COM

## 内存的使用简介

在Windows平台下，高级语言写出的程序经过编译链接，最终会变成PE文件。当PE文件被装载运行后，就成了所谓的进程。
PE文件代码段中包含的二进制级别的机器代码会被装入内存的 **代码区(.text)** ，处理器将到内存的这个区域一条一条地取出指令和操作数，并送入运算逻辑单元进行运算；如果代码中请求开辟 **动态内存** ，则会在内存的 **堆区** 分配一块大小合适的区域返回给代码区的代码使用；当函数调用发生时， **函数的调用关系等信息** 会动态地保存在内存的 **栈区** ，以供处理器在执行完被调用函数的代码时，返回母函数。

> **关于出栈问题：**
>  pop操作栈顶元素，SS:SP指向1000EH新的栈顶，原栈顶1000CH的数据依然存在，但是它在栈中不存在了，当再次push等入栈指令后，SS：SP移1000CH，并在里面写入新的数据，将其覆盖。

内存的栈区实际上指的就是 **系统栈** 。系统栈由系统自动维护，它用于实现高级语言中函数的调用。对于类似C语言这样的高级语言，系统栈的PUSH、POP等堆栈平衡细节是透明的。一般说来，只有在使用汇编语言开发程序的时候，才需要和它直接打交道。

### 寄存器与函数栈帧

每一个函数独占自己的栈帧空间。当前 **正在运行的函数** 的 **栈帧** 总是在 **栈顶** 。
Win32系统提供两个特殊的 **寄存器** 用于 **标识** 位于 **系统栈顶端的栈帧** 。

  1. **ESP** ： **栈指针寄存器** (extended stack pointer)，其内存放着一个指针，该指针永远指向系统栈 **最上面一个栈帧的栈顶** 。
  2. **EBP** ： **基址指针寄存器** (extended base pointer)，其内存放着一个指针，该指针永远指向系统栈 **最上面一个栈帧的底部** 。

**函数栈帧** ：ESP和EBP之间的内存空间为当前栈帧,EBP标识了当前栈帧的底部，ESP标识了当前栈帧的顶部。
在函数栈帧中，一般 **包含** 以下几类重要信息：
(1) **局部变量** ：为函数局部变量开辟的内存空间。
(2) **栈帧状态值** ：保存 **前栈帧的顶部和底部** (实际上只保存前栈帧的底部，前栈帧的顶部可以通过栈帧平衡计算得到)，用于在 **本栈被弹出** 后恢 **复出上一个栈帧** 。
(3) **函数返回地址** ：保存当前函数调用前的“断点”信息，也就是函数调用前的指令位置，以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。

> 函数栈帧的大小并不固定，一般与其对应函数的局部变量多少有关。函数 **运行过程中** ，其 **栈帧大小** 也是 **在不停变化** 的。

  3. **EIP** ： **指令寄存器** (extended instruction pointer)，其内存放着一个指针，该指针永远指向下一条等待执行的指令地址。

> 其他 ：
>  EAX：累加(Accumulator)寄存器，常用于函数返回值
>  EBX：基址(Base)寄存器，以它为基址访问内存
>  ECX：计数器(Counter)寄存器，常用作字符串和循环操作中的计数器
>  EDX：数据(Data)寄存器，常用于乘除法和I/O指针
>  ESI：源变址寄存器
>  DSI：目的变址寄存器
>  ESP：栈(Stack)指针寄存器，指向栈顶部
>  EBP：基址指针寄存器，指向当前栈底部
>  EIP：指令寄存器，指向下一条指令的地址

### 函数调用与返回步骤

更具体的过程见：https://www.cnblogs.com/UnknowCodeMaker/p/11002225.html

#### 调用：

  1. 参数入栈：将参数从右向左依次压入系统栈中。
  2. 返回地址入栈：将当前代码区调用指令的下一条指令地址压入栈中，供函数返回时继续执行。
  3. 代码区跳转：处理器从当前代码区跳转到被调用函数的入口处。
  4. 栈帧调整：具体包括：
    1. 保存当前栈帧状态值，已备后面恢复本栈帧时使用(EBP入栈)。
    2. 将当前栈帧切换到新栈帧(将ESP值装入EBP，更新栈帧底部)。
    3. 给新栈帧分配空间(把ESP减去所需空间的大小，抬高栈顶)。
    4. 对于_stdcall调用约定，函数调用时用到的指令序列大致如下：

> push 参数3 ;假设该函数有3个参数，将从右向做依次入栈
>  push 参数2
>  push 参数1
>  call 函数地址 ;call指令将同时完成两项工作：a)向栈中压入当前指令地址的下一个指令地址，即保存返回地址。 b)跳转到所调用函数的入口处。
>  push ebp ;保存旧栈帧的底部
>  mov ebp,esp ;设置新栈帧的底部 (栈帧切换)
>  sub esp,xxx ;设置新栈帧的顶部 (抬高栈顶，为新栈帧开辟空间)

#### 返回：

  1. 保存返回值，通常将函数的返回值保存在寄存器EAX中。
  2. 弹出当前帧，恢复上一个栈帧。具体包括：
    1. 在堆栈平衡的基础上，给ESP加上栈帧的大小，降低栈顶，回收当前栈帧的空间。
    2. 将当前栈帧底部保存的前栈帧EBP值弹入EBP寄存器，恢复出上一个栈帧。
    3. 将函数返回地址弹给EIP寄存器。
    4. 跳转：按照函数返回地址跳回母函数中继续执行。

> add esp,xxx ;降低栈顶，回收当前的栈帧
>  pop ebp ;将上一个栈帧底部位置恢复到ebp
>  retn ;a)弹出当前栈顶元素，即弹出栈帧中的返回地址，至此，栈帧恢复到上一个栈帧工作完成。b)让处理器跳转到弹出的返回地址，恢复调用前代码区

### 函数调用约定与相关指令

![assets/0012 - 函数调用 压栈的工作原理__resource-001-ac61d4983c84.png](/media/5e740e82cc3c8ed8352e.png)

详见:https://blog.csdn.net/qq_34771252/article/details/90547751
**函数调用约定** ：不同的操作系统、不同的语言、不同的编译器在实现函数调用时的原理虽然基本相同，但具体的调用约定还是有差别的。这包括 **参数传递方式** ， **参数入栈顺序是从右向左还是从左向右** ， **函数返回时恢复堆栈平衡的操作在子函数中进行还是在母函数中进行** 。

常见的调用约定有：stdcall，cdecl，fastcall，thiscall，naked call

#### __stdcall

> 很多时候称为 **pascal调用约定** ，Microsoft C++系列的C/C++编译器中，常常用PASCAL宏来声明这个调用约定，类似的宏还有WINAPI和CALLBACK。

声明格式：

```cpp
string str1 = &quot;test&quot;;
string str2 = &quot;test&quot;;

```

  * 参数从右向左压入堆栈
  * 函数自身修改堆栈
  * 函数名自动加前导的下划线，后面紧跟一个@符号，其后紧跟着参数的尺寸

push 2 第二个参数入栈
push 1 第一个参数入栈
call function 调用参数，注意此时自动把cs:eip入栈
而对于函数自身，则可以翻译为：
push ebp 保存ebp寄存器，该寄存器将用来保存堆栈的栈顶指针，可以在函数退出时恢复mov ebp,esp 保存堆栈指针mov eax,[ebp + 8H] 堆栈中ebp指向位置之前依次保存有ebp,cs:eip,a,b,ebp +8指向a
add eax,[ebp + 0CH] 堆栈中ebp + 12处保存了b
mov esp,ebp 恢复esp
pop ebp
ret 8
而在编译时，这个函数的名字被翻译成_function@8

#### __cdecl

**C调用约定** ，是 **C语言缺省** 的调用约定

```cpp
string str1 = &quot;test&quot;;
string str2 = &quot;test&quot;;

```

  * 参数首先由右向左压入堆栈
  * 函数本身不清理堆栈，调用者负责清理堆栈

#### __fastcall

函数的第一个和第二个DWORD参数（或者尺寸更小的）通过ecx和edx传递，其他参数通过从右向左的顺序压栈被调用函数清理堆栈函数名修改规则同stdcall

```cpp
string str1 = &quot;test&quot;;
string str2 = &quot;test&quot;;

```

#### thiscall

唯一一个不能明确指明的函数修饰，因为thiscall不是关键字
它是C++类成员函数缺省的调用约定。由于成员函数调用还有一个this指针，因此必须特殊处理

  * 参数从右向左入栈
  * 如果参数个数确定，this指针通过ecx传递给被调用者；
  * 如果参数个数不确定，this指针在所有参数压栈后被压入堆栈。对参数个数不定的，调用者清理堆栈，否则函数自己清理堆栈
