初始化可变参数列表时,使用宏
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)
)
相反,有 guardian “样式”,例如,在调用函数时,您可以在可变参数的末尾添加
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,假设您的函数甚至使用它,因为它不是必需的)进行一些数学运算,要么传递一些附加信息。
当变量参数被压入堆栈时,函数什么时候知道它们何时结束?
这不是调用约定所规定的。程序员必须找出一种方法来了解基于非可变参数(或其他参数)存在多少可变参数。例如,在上面的示例中,我只是将
n
作为第一个参数传递,printf
函数系列根据字符串中格式标识符的数量(例如 %d
、%s
)、syscall
函数根据系统调用号(第一个参数)计算出来,依此类推...