函数的机制调用约定及栈细节,包括函数的调用过程、栈结构的形成与利用、以及如何通过栈结构实现函数递归调用。讲解中详细分析了函数调用时参数的传递、返回地址的保存、局部变量的申请与释放等关键步骤,并通过走迷宫的例子展示了递归调用在解决复杂问题中的应用。此外,还提及了函数调用约定的几种类型及其在不同编程环境中的兼容性问题,栈结构和调用约定对于掌握函数机制的重要性。

回车和换行

换行(\n)和回车(\r)是两种不同的控制字符,它们在计算机系统中有不同的作用和历史背景。以下是它们的区别和使用场景:

换行(\n

  • 名称:换行字符(Line Feed,LF)

  • ASCII值:10

  • 功能:将光标移到下一行的开头,通常用于表示行结束。

  • 使用场景:在Unix及其衍生系统(如Linux、macOS)中,换行字符用于表示文本文件中的行结束。

回车(\r

  • 名称:回车字符(Carriage Return,CR)

  • ASCII值:13

  • 功能:将光标移到当前行的开头,而不移动到下一行。

  • 使用场景:在旧式打字机和一些早期的计算机系统中,回车字符用于将光标移到行首。在经典的Mac OS系统(版本9及更早版本)中,回车字符用于表示文本文件中的行结束。

不同系统的行结束表示法

不同操作系统有不同的行结束表示方法:

  • Unix/Linux:使用\n(换行)表示行结束。

  • Windows:使用\r\n(回车+换行)表示行结束。

  • 经典Mac OS:使用\r(回车)表示行结束(macOS 9 及更早版本,macOS X及其后继版本已改为使用\n)。

示例代码

以下示例展示了如何在C++中使用换行和回车:

#include <iostream>
using namespace std;

int main() {
    cout << "Hello, World!\n"; // 使用换行
    cout << "This is a new line.\n";
    cout << "Carriage return example:\r"; // 使用回车
    cout << "Replaced line.";
    return 0;
}

在上面的代码中:

  • \n将光标移到下一行的开头,输出后光标位于下一行。

  • \r将光标移到当前行的开头,因此后续的输出会覆盖同一行的内容。

使用示例

  1. 换行(\n):

cout << "Hello,\nWorld!";

输出:

Hello,
World!
  1. 回车(\r):

cout << "Hello, World!\rHi!";

输出:

Hi!o, World!

因为\r将光标移到行首,Hi!覆盖了Hello的前三个字符。

总结

  • 换行(\n)用于将光标移到下一行的开头,通常表示行结束。

  • 回车(\r)用于将光标移到当前行的开头,不移动到下一行。

  • 不同操作系统对行结束的表示方法不同:Unix/Linux使用\n,Windows使用\r\n,经典Mac OS使用\r

栈结构与函数调用过程

在程序执行过程中,栈(stack)是一个重要的数据结构,用于管理函数调用和返回的过程。以下是栈结构与函数调用过程的详细解释。

栈结构

栈是一种后进先出(LIFO,Last In First Out)数据结构,具有以下特性:

  • Push:将数据压入栈顶。

  • Pop:从栈顶弹出数据。

  • Top/Peek:查看栈顶数据而不弹出。

函数调用过程中的栈

在函数调用过程中,栈用于保存函数的状态,包括局部变量、返回地址和调用者的上下文等。这个栈通常称为调用栈运行时栈

函数调用过程

当一个函数被调用时,以下步骤会发生:

  1. 保存当前上下文:保存当前函数的执行状态,包括寄存器值和程序计数器(即当前执行位置)。

  2. 分配栈帧:为被调用函数创建一个新的栈帧,包含以下信息:

  • 返回地址:调用函数的地址,以便函数执行完后返回。

  • 参数:传递给被调用函数的参数。

  • 局部变量:被调用函数中定义的局部变量。

  • 保存的寄存器值:如果被调用函数需要使用一些寄存器,这些寄存器的当前值也会被保存。

  1. 转移控制:跳转到被调用函数的入口地址,开始执行被调用函数的代码。

  2. 执行被调用函数:执行被调用函数的代码,操作其局部变量和参数。

  3. 返回调用者:函数执行完毕后,通过栈顶保存的返回地址,恢复调用者的上下文并继续执行调用者的代码。

示例

以下是一个简单的递归函数示例,用于计算阶乘,并展示函数调用栈的变化:

#include <iostream>
using namespace std;

int factorial(int n) {
    if (n <= 1) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}

int main() {
    int number = 5;
    cout << "Factorial of " << number << " is " << factorial(number) << endl;
    return 0;
}

调用栈变化

假设调用 factorial(3),调用栈的变化如下:

  1. 初始状态

main()
  1. 调用 factorial(3)

factorial(3)
main()
  1. 调用 factorial(2)

factorial(2)
factorial(3)
main()
  1. 调用 factorial(1)

factorial(1)
factorial(2)
factorial(3)
main()
  1. 返回 factorial(1)

factorial(2) [return value: 1]
factorial(3)
main()
  1. 返回 factorial(2)

factorial(3) [return value: 2]
main()
  1. 返回 factorial(3)

main() [return value: 6]

栈帧示例

每个函数调用的栈帧结构大致如下:

+------------------+
| 返回地址          | <-- 高地址
+------------------+
| 参数1            |
+------------------+
| 参数2            |
+------------------+
| ...              |
+------------------+
| 局部变量1        |
+------------------+
| 局部变量2        |
+------------------+
| ...              |
+------------------+
| 保存的寄存器值    |
+------------------+  <-- 低地址

  1. 栈是从高地址到低地址生长

  2. 栈内函数如果有缓冲区,写入的话,是从低地址往高地址写

  3. 缓冲区溢出是一直溢出覆盖到返回地址


函数调用约定与栈细节

函数调用约定(calling convention)定义了在调用函数时,函数参数、返回值、局部变量和寄存器等在栈中的处理方式。不同的编译器和操作系统可能采用不同的调用约定。以下是常见调用约定及其栈细节。

常见调用约定

  1. cdecl(C declaration)

  2. stdcall(Standard call)

  3. fastcall(Fast call)


1. cdecl(C declaration)

特点

  • 参数从右到左压入栈。

  • 调用者负责清理栈上的参数。

  • 返回值通过寄存器 EAXRAX 返回。

栈帧布局

void function(int a, int b, int c) {
    // function body
}

int main() {
    function(1, 2, 3);
    return 0;
}

调用过程

  1. 调用者将参数 c, b, a 依次压入栈。

  2. 调用者将返回地址压入栈。

  3. 被调用者执行函数体。

  4. 被调用者返回结果到 EAXRAX

  5. 调用者从栈中弹出参数。

栈帧示例

高地址
+-------------------+
| 参数 c            |
+-------------------+
| 参数 b            |
+-------------------+
| 参数 a            |
+-------------------+
| 返回地址          | <- 返回地址
+-------------------+
| 局部变量          |
+-------------------+
| 保存的寄存器值    |
+-------------------+
低地址

2. stdcall(Standard call)

特点

  • 参数从右到左压入栈。

  • 被调用者负责清理栈上的参数。

  • 返回值通过寄存器 EAXRAX 返回。

栈帧布局

void __stdcall function(int a, int b, int c) {
    // function body
}

int main() {
    function(1, 2, 3);
    return 0;
}

调用过程

  1. 调用者将参数 c, b, a 依次压入栈。

  2. 调用者将返回地址压入栈。

  3. 被调用者执行函数体。

  4. 被调用者返回结果到 EAXRAX

  5. 被调用者从栈中弹出参数。

栈帧示例

高地址
+-------------------+
| 参数 c            |
+-------------------+
| 参数 b            |
+-------------------+
| 参数 a            |
+-------------------+
| 返回地址          | <- 返回地址
+-------------------+
| 局部变量          |
+-------------------+
| 保存的寄存器值    |
+-------------------+
低地址

3. fastcall(Fast call)

特点

  • 部分参数通过寄存器传递,通常是 ECXEDX

  • 剩余参数从右到左压入栈。

  • 被调用者负责清理栈上的参数。

  • 返回值通过寄存器 EAXRAX 返回。

栈帧布局

void __fastcall function(int a, int b, int c) {
    // function body
}

int main() {
    function(1, 2, 3);
    return 0;
}

调用过程

  1. 调用者将参数 ab 放入寄存器 ECXEDX

  2. 调用者将参数 c 压入栈。

  3. 调用者将返回地址压入栈。

  4. 被调用者执行函数体。

  5. 被调用者返回结果到 EAXRAX

  6. 被调用者从栈中弹出参数。

栈帧示例

高地址
+-------------------+
| 参数 c            |
+-------------------+
| 返回地址          | <- 返回地址
+-------------------+
| 局部变量          |
+-------------------+
| 保存的寄存器值    |
+-------------------+
低地址

栈帧的详细布局

每次函数调用都会在栈上创建一个新的栈帧,包含以下内容:

  1. 返回地址:调用者代码中的位置,函数执行完毕后需要返回到这里。

  2. 参数:传递给被调用函数的参数。

  3. 旧的帧指针(Frame Pointer):指向前一个栈帧的帧指针(通常是 EBPRBP)。

  4. 局部变量:函数内声明的局部变量。

  5. 保存的寄存器:被调用函数可能使用并修改的寄存器值,函数返回时需要恢复。

函数调用过程中的步骤

  1. 调用者准备参数:根据调用约定,将参数按顺序压入栈或放入指定寄存器。

  2. 调用者压入返回地址:将返回地址压入栈。

  3. 跳转到被调用函数:调用者将控制权转移到被调用函数的入口地址。

  4. 被调用者保存上下文:保存调用者的帧指针,设置新的帧指针,分配局部变量空间,保存必要的寄存器。

  5. 被调用者执行函数体:执行实际的函数代码。

  6. 被调用者返回结果:将返回值放入返回值寄存器,恢复寄存器,释放局部变量空间,恢复帧指针。

  7. 被调用者清理栈(根据调用约定,可能由调用者清理):弹出参数,恢复返回地址。

  8. 调用者恢复执行:从返回地址继续执行。

示例代码解释

以下是一个C++示例,展示了调用约定及其栈帧布局:

#include <iostream>
using namespace std;

void __cdecl functionC(int a, int b, int c) {
    int local = a + b + c;
    cout << "functionC: " << local << endl;
}

void __stdcall functionS(int a, int b) {
    int local = a * b;
    cout << "functionS: " << local << endl;
}

void __fastcall functionF(int a, int b, int c) {
    int local = a - b - c;
    cout << "functionF: " << local << endl;
}

int main() {
    functionC(1, 2, 3); // cdecl
    functionS(4, 5);    // stdcall
    functionF(6, 7, 8); // fastcall
    return 0;
}

总结

  • 函数调用约定定义了参数传递、栈清理、返回值处理等规则。

  • 不同调用约定(如 cdecl, stdcall, fastcall)在参数传递方式和栈清理责任上有所不同。

  • 栈帧布局包含返回地址、参数、局部变量、旧的帧指针和保存的寄存器等。##栈


函数和返回值之前描述的调用约定,如: int _cdecl fuck(int n, int m)

调用方(caller)需要和被调方(callee)作出以下约定:

参数的传递方向

参数的传输媒介

函数返回值的位置

释放参数空间的负责方,有且仅有一方去释放参数空间。

  • __cdecl:参数使用栈空间传递,从右往左,函数返回值在寄存器,由调用方负责释放参数空间

  • __stdcall:参数使用栈空间传递,从右往左,函数返回值在寄存器,由被调方负责释放参数空间

  • __fastcall:左右前两个参数使用寄存器传递,其他参数使用栈空间传递,从右往左,函数返回值在寄存器,由被调方负责释放参数空间

  1. 参数信息

  2. 保存返回地址

  3. 保存调用方的栈信息(栈底)

  4. 更新当前栈底到栈顶. (把当前栈顶作为被调用方的栈底)

  5. 为局部变量申请空间 (抬高栈顶)

  6. 保存寄存器环境 (把即将使用的寄存器原值存在栈中)

  7. 如果编译选项有 /ZI /Zi , 则将局部变量初始化为0xcccccccc


  1. 执行函数体

  2. 恢复寄存器环境

  3. 释放局部变量的空间

  4. 恢复调用方的栈信息(栈底)

  5. _cdecl, 取出当前栈顶作为返回的流程地址,返回调用方后,由调用方清理参数空间

  6. 其他约定,取出当前栈顶作为返回的流程地址,同时由被调用方清理参数空间后才返回

内存构成