如果我正确理解了您的疑问,那么您基本上要问的是:如果所有参数都被推入堆栈而没有其他信息,那么可变参数函数如何确定其可变参数的开始位置?
正如您已经注意到的,参数以与声明相反的顺序被压入堆栈:这意味着void f(int a, ...)
调用 as f(1, 2, 3)
pushes first 3
, then 2
, finally1
在调用之前。
那么如何找到可变参数的开始呢?
你总是知道:
- 栈顶在哪里。
- 在变量参数之前需要(固定)多少个参数。
因此,以相反的顺序推送值是了解变量参数列表从哪里开始的最简单方法。您将始终找到固定数量的变量(等于所需(固定)参数的数量,然后是所有变量参数(如果有)。这使得无论传递的参数数量如何,都可以计算参数列表的开头,不需要在其他任何地方传递额外的信息。换句话说,可变参数的开始从堆栈顶部的偏移量总是相同的,因为它只取决于所需参数的数量。
一个例子将使这一点更清楚。让我们假设一个函数定义为:
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(nb:这是假设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
计算出来,函数根据系统调用号(第一个参数)计算出来,等等...