可变参数函数的调用约定

标签 c calling-convention cdecl

初始化可变参数列表时,可以使用宏 va_start并通过 list_name接下来是 the last fixed parameter before the va list starts因为“最后一个固定参数与第一个变量相邻”并且不知何故这有助于识别堆栈中的var arg长度/位置(我说是因为我不明白如何)。

使用 cdecl调用约定(意味着从右到左将参数压入堆栈) the last fixed parameter before the va list starts 是怎样的?对于确定列表长度有用吗?例如,如果该参数是整数 3并且变量参数也有 3被调用者如何知道可变参数列表没有在这里结束,因为还有另一个 3 (固定参数)应该结束吗?例如f(int a, int b, ... ) -> 调用 f(1, 3, 1, 2, 3) )

相反,有监护人“样式”,您可以在其中添加例如 NULL调用函数时位于可变参数末尾的指针。再说一遍:怎么样NULL如果将第一个插入堆栈,有用吗? NULL 不应该被插入参数的固定部分和可变部分之间吗? (例如 f(int a, int b, ... ) -> 调用 f(a, b, NULL, param1, param2) )

最佳答案

如果我正确理解你的疑虑,你基本上要问的是:如果所有参数都被插入堆栈而没有附加信息,那么可变参数函数如何确定其可变参数开始的位置?

正如您已经注意到的,参数以与声明相反的顺序压入堆栈:这意味着调用 void f(int a, ...)f(1, 2, 3) 在调用之前首先压入 3 ,然后压入 2 ,最后压入 1

那么如何找到可变参数的开始呢?

你总是知道:

  1. 堆栈顶部所在的位置。
  2. 在可变参数之前需要(固定)多少个参数。

因此,按相反顺序推送值是了解变量参数列表从何处开始的最简单方法。您将总是找到固定数量的变量(等于所需(固定)参数的数量,后跟所有变量参数(如果有)。这使得计算参数列表的开始位置成为可能,无论传递的参数数量,而不需要在其他任何地方传递附加信息。换句话说,可变参数的起始位置距堆栈顶部的偏移量始终相同,因为它仅取决于所需参数的数量。


一个例子会让这一点更清楚。我们假设一个函数定义为:

int f(int n, ...) {
    // ...
}

然后,编译调用 f(2, 123, 456) 。在 cdecl 下,这会产生:

push 456
push 123
push 2
call f

f启动时,它会发现堆栈处于以下状态:

--- lower addresses ----
[ return address ] <-- esp
[ 2              ]
[ 123            ]
[ 456            ]
--- higher addresses ---

现在 f 很容易知道参数列表从哪里开始,知道 n 是最后一个“固定”(非可变参数)参数:它只需要计算 esp - 4 - 4 。也就是说:从 esp 中减去保存的返回地址的固定量 (4),然后为每个固定参数减去 4(注意:这是假设 sizeof(int) == 4 )。这样做您最终将得到第一个可变参数的位置。

这适用于任意数量的可变参数:

; f(5, 1, 2, 3, 4, 5)      --- lower addresses ----
push 5                     [ return address ] <-- esp
push 4                     [ 5              ]
push 3                     [ 1              ]
push 2                     [ 2              ]
push 1                     [ 3              ]
push 5                     [ 4              ]
call f                     [ 5              ]
                           --- higher addresses ---

现在想象一下相反的场景,其中参数以相反的顺序推送,最终 f(2, 123, 456) 编译为:

; f(2, 123, 456)     --- lower addresses ----
push 2               [ return address   ] <-- esp
push 123             [ 456              ]
push 456             [ 123              ]
call f               [ 2                ]
                     --- higher addresses ---

f(5, 1, 2, 3, 4, 5) 编译为:

; f(5, 1, 2, 3, 4, 5)      --- lower addresses ----
push 5                     [ return address ] <-- esp
push 1                     [ 5              ]
push 2                     [ 4              ]
push 3                     [ 3              ]
push 4                     [ 2              ]
push 5                     [ 1              ]
call f                     [ 5              ]
                           --- higher addresses ---

现在参数列表从哪里开始?仅根据堆栈指针(ESP)的值和所需参数的数量是无法判断的,因为距堆栈顶部的偏移量不再相同,而是随数量而变化可变参数。为了弄清楚它,您要么必须使用基指针(EBP,假设您的函数甚至使用它,因为它不是必需的)进行一些数学运算,要么传递一些附加信息。


When the variable arguments are pushed into the stack, when do the function knows when they ended?

这不是调用约定所规定的。程序员必须找出一种方法来了解基于非可变参数(或其他参数)存在多少可变参数。例如,在上面的示例中,我只是将 n 作为第一个参数传递,printf 系列函数根据字符串中格式标识符的数量(例如 %d%s )计算出它,syscall 函数根据系统调用编号计算出它(第一个参数),依此类推...

关于可变参数函数的调用约定,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64687205/

相关文章:

assembly - 为什么使用向量寄存器而不是将标准参数保存在堆栈中?

rust - 为什么 Rust 同时具有按值调用和按引用调用?

C++ 和全动态函数

c++ - 有没有办法在没有 .* 或 ->* 运算符的情况下调用成员函数

c - 在cdecl调用约定中,signed char和short如何作为参数传递

c - 为什么使用 AVX2 的加速比预期的要低?

c - 如何对 char var[][] 进行 shmat,C

assembly - x86 函数必须保留哪些寄存器?

c - 用无符号整数中的另一个字节替换第 n 个字节

c - 为什么输出始终为零(0.0000)?