函数参数反序入栈

ZaynPei Lv6

函数参数反序入栈,也称为从右至左参数压栈,指的是在调用一个函数时,传递给该函数的参数不是按照代码中从左到右的顺序,而是从右到左的顺序依次被推入(push)到程序的调用栈(Call Stack)中。

这是一种由调用约定(Calling Convention)规定的行为。最常见的采用这种方式的调用约定是 cdecl,它是 C 和 C++ 程序在 x86 上的默认调用约定。

为什么需要反序入栈?

你可能会觉得从左到右的顺序更符合直觉,为什么大多数语言(如C/C++)会选择这种看起来“奇怪”的反序方式呢?最核心的原因是为了支持可变参数函数(Variadic Functions)。

可变参数函数是指那些可以接受不定数量参数的函数,最典型的例子就是 C 语言中的 printf 和 scanf 函数。

让我们来看一个 printf 的调用:

1
printf("Name: %s, Age: %d, Score: %.2f\n", "Alice", 20, 95.5);

这个 printf 函数调用了4个参数。但 printf 函数本身在被编译时,并不知道它未来会被多少个参数调用。它唯一能确定的就是第一个参数,即格式化字符串 (“Name: %s, Age: %d, Score: %.2f”)。

函数需要通过解析这个格式化字符串来确定后面还有多少个、以及分别是什么类型的参数。

如果采用顺序(从左到右)入栈: - 栈底(高位) - push “Name: %s, Age: %d, Score: %.2f” - push “Alice” - push 20 - push 95.5 (栈顶(低位))

在这种情况下,当 printf 函数开始执行时,它能直接访问到的栈顶元素是 95.5。它无法直接定位到最重要的格式化字符串,因为格式化字符串被压在栈的深处,其具体位置依赖于后面参数的数量,而这个数量本身又是未知的, 这使得函数无法确定读取参数读到哪里停止。

如果采用反序(从右到左)入栈(实际情况): - push 95.5 - push 20 - push “Alice” - push “Name: %s, Age: %d, Score: %.2f”

在这种情况下,当 printf 函数开始执行时,无论后面有多少个参数,格式化字符串始终位于栈的固定、可预测的位置(紧邻着函数返回地址的上方)。函数可以轻松地访问到这个字符串,通过解析它(发现 %s, %d, %.2f),就能准确地知道接下来需要从栈上读取一个字符串指针、一个整数和一个浮点数。

因此,反序入栈确保了函数的第一个(或固定)参数的位置是确定的,这为实现可变参数函数提供了基础。

cdecl的其他规则

cdecl(C Declaration 的缩写)是 C 语言中最常见的一种函数调用约定(Calling Convention)。它定义了函数参数如何传递、栈如何维护、返回值如何传递等规则。

  • 参数从右到左压栈:在函数调用时,最后一个参数最先被压入栈中,最先被处理。
  • 调用者负责清理栈:函数调用结束后,调用者需要手动调整栈指针,以清理函数调用时压入栈中的参数。
  • 支持可变参数:cdecl 允许函数接受不定数量的参数,这使得它能够支持像 printf 这样的可变参数函数。
  • 返回值: 通过寄存器 eax(对于整数和指针类型)或 st0(对于浮点类型)返回。
  • 函数名修饰:在编译时,函数名通常会被加上一个前缀下划线(_),例如,函数 foo 在汇编中可能表示为 _foo。

不同的平台和编译器可能会有不同的调用约定。例如,Windows 平台上常用的调用约定是 stdcall 和 fastcall,它们在参数传递和栈清理方面与 cdecl 有所不同。

而在 RISC-V 等现代架构中,调用约定更倾向于使用寄存器传递参数,以提高性能,减少对栈的依赖。具体内容可以参考RISC-V 的调用约定

On this page
函数参数反序入栈