初始化可变参数列表时,可以使用宏 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
。
那么如何找到可变参数的开始呢?
你总是知道:
- 堆栈顶部所在的位置。
- 在可变参数之前需要(固定)多少个参数。
因此,按相反顺序推送值是了解变量参数列表从何处开始的最简单方法。您将总是找到固定数量的变量(等于所需(固定)参数的数量,后跟所有变量参数(如果有)。这使得计算参数列表的开始位置成为可能,无论传递的参数数量,而不需要在其他任何地方传递附加信息。换句话说,可变参数的起始位置距堆栈顶部的偏移量始终相同,因为它仅取决于所需参数的数量。
一个例子会让这一点更清楚。我们假设一个函数定义为:
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/