1

当你初始化一个可变参数列表时,你使用宏va_start并传递list_namethe 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)

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

4

1 回答 1

1

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

正如您已经注意到的,参数以与声明相反的顺序被压入堆栈:这意味着void f(int a, ...)调用 as f(1, 2, 3)pushes first 3, then 2, finally1在调用之前。

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

你总是知道:

  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(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计算出来,函数根据系统调用号(第一个参数)计算出来,等等...

于 2020-11-04T21:44:26.150 回答