# C-language **Repository Path**: david3901093/c-language ## Basic Information - **Project Name**: C-language - **Description**: C语言学习 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-01-06 - **Last Updated**: 2026-03-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # C-language #### 介绍 C语言学习 ## 函数的调用 在程序执行过程中,CPU 使用栈(Stack)来管理函数的调用。这个栈是一个后进先出(LIFO - Last In, First Out)的数据结构。`ESP` (Stack Pointer) 寄存器始终指向栈顶的位置,而 `EBP` (Base Pointer / Frame Pointer) 寄存器则用于指向当前函数栈帧(Stack Frame)的基址,方便访问该函数的局部变量和参数。对于 x86-64 架构,对应的寄存器是 `RSP` 和 `RBP`。 --- ### **1. 函数栈帧** `EBP`/`RBP` 和 `ESP`/`RSP` 这两个寄存器是维护函数调用栈的关键。`ESP`/`RSP` 指向栈顶,随着数据的压入(push)和弹出(pop)不断变化。`EBP`/`RBP` 则像一个“锚点”,在进入函数时被设置为当前栈帧的底部(或顶部,取决于增长方向),这样函数内部就可以通过 `EBP`/`RBP` 加上一个固定的偏移量来安全地访问自己的局部变量、形参以及保存的旧 `EBP`/`RBP` 值和返回地址。 --- ### **2. 局部变量是怎么创建的?** 局部变量的“创建”发生在函数被调用时。 * 当 CPU 执行到函数调用指令(如 `call`)时,会将控制权转移给被调用函数。 * 被调用函数的入口代码(通常由编译器生成)会首先**预留**出一块栈空间,这块空间就是该函数的栈帧。 * 预留空间的操作通常是通过调整 `ESP`/`RSP` 寄存器实现的(例如 `sub esp, 16` 为 16 字节的空间)。这部分空间就用来存放该函数的所有局部变量。 * 因此,局部变量是在函数栈帧中分配的内存空间,它们的生命周期与函数的执行周期一致。 --- ### **3. 为什么局部变量的值是随机值?** 局部变量存储在栈上。栈区的内存块会被反复使用,当一个函数结束,它的栈帧所占用的内存空间就会被释放(通常是通过恢复 `ESP`/`RSP` 的值来逻辑上“释放”,物理内存并未清零)。 * 当一个新的函数被调用并需要创建新的栈帧时,它可能会分配到之前某个函数使用过的同一片物理内存区域。 * 这片内存区域中可能还残留着**之前程序写入的任意数据**(即“垃圾值”)。 * 编译器不会主动将这片新分配的内存空间清零,因为这会带来不必要的性能开销。 * 因此,如果程序员没有显式地初始化局部变量,变量所在的内存单元就会保留这些旧的、无意义的值,从而表现为“随机值”。 --- ### **4. 函数是怎么传参的?传参的顺序是怎样的?** 函数传参的具体方式取决于**调用约定**(Calling Convention),这是编译器和操作系统之间的一种协议。常见的调用约定有 `__cdecl`, `__stdcall`, `__fastcall` 等。 * **主要方式(以 `__cdecl` 为例):** 参数通过**压入栈**的方式传递。在执行 `call` 指令之前,主调函数(caller)会按照从**右到左**的顺序,将实参压入栈中。 * 例如,调用 `func(a, b, c);` * 压栈顺序是:先压 `c`,再压 `b`,最后压 `a`。 * 这样,在栈中 `a` 的地址最低,`c` 的地址最高。 * **其他方式(如 `__fastcall`):** 一些调用约定会优先使用 CPU 寄存器来传递部分参数(通常是前几个),以提高效率,只有剩余的参数才会通过栈传递。 --- ### **5. 形参和实参是什么关系?** * **实参(Actual Parameter):** 是函数调用时,传递给函数的具体值或变量。 * **形参(Formal Parameter):** 是函数定义时,用来接收实参的变量名。 它们的关系可以理解为: * 形参是主调函数在被调函数栈帧中开辟的一个**副本**或**占位符**。 * 当函数被调用时,实参的**值**会被复制一份给对应的形参。 * 因此,在大多数语言(C/C++ 默认按值传递)中,**形参是实参的一份拷贝**。在函数内部修改形参的值,不会影响到外部的实参。 --- ### **6. 函数调用是怎么做的?** 一个典型的函数调用过程(以 `__cdecl` 为例)包括以下步骤: 1. **参数入栈:** 主调函数将实参按从右到左的顺序压入栈。 2. **调用指令:** 主调函数执行 `call` 指令。`call` 指令会自动将下一条指令的地址(即**返回地址**)压入栈,然后跳转到被调函数的入口地址。 3. **保存旧栈帧指针:** 被调函数(callee)开始执行,首先会将主调函数的 `EBP`/`RBP` 值压入栈,以保存旧的栈帧信息。 4. **建立新栈帧:** 被调函数将 `ESP`/`RSP` 的值赋给 `EBP`/`RBP`,此时 `EBP`/`RBP` 就指向了新栈帧的底部(也就是保存的旧 `EBP`/`RBP` 值的位置)。 5. **分配局部变量空间:** 被调函数通过调整 `ESP`/`RSP` 来为自己的局部变量分配空间。 6. **执行函数体:** 开始执行函数内部的代码,使用 `EBP`/`RBP` 加上偏移量来访问形参和局部变量。 7. **返回值准备:** 如果函数有返回值,通常会将其存放在特定的寄存器中(如 `EAX` 或 `RAX`)。 --- ### **7. 函数调用结束后是怎么返回的?** 函数返回的过程是调用过程的逆操作: 1. **恢复栈顶指针:** 函数内部代码会将 `EBP`/`RBP` 的值赋给 `ESP`/`RSP`,这样就丢弃了所有局部变量占用的空间。 2. **恢复旧栈帧指针:** 从栈中弹出(`pop`)之前保存的旧 `EBP`/`RBP` 值,并恢复到 `EBP`/`RBP` 寄存器中。此时栈顶就是返回地址。 3. **执行返回指令:** 执行 `ret` 指令。`ret` 指令会从栈顶弹出返回地址,并跳转到该地址继续执行。 4. **清理参数(由主调方完成,对于 `__cdecl`):** `call` 指令之后,主调函数需要负责将之前压入的参数从栈中清理掉(通过增加 `ESP`/`RSP` 的值)。至此,整个函数调用过程彻底结束。