【问题标题】:Calling convention for variadic function [closed]可变参数函数的调用约定[关闭]
【发布时间】:2020-11-04 20:22:51
【问题描述】:

初始化可变参数列表时,使用宏 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)

【问题讨论】:

标签: c calling-convention cdecl


【解决方案1】:

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

正如您已经注意到的,参数以与声明相反的顺序被压入堆栈:这意味着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,假设你的函数甚至使用它,因为它不是必需的)做一些数学运算,要么传递一些额外的信息。


当变量参数被压入堆栈时,函数何时知道它们何时结束?

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

【讨论】:

  • 当变量参数被压入堆栈时,函数何时知道它们何时结束?是不是您发现可变参数部分之后的第一个参数并且您知道列表刚刚结束?为什么这个参数这么特别?它不是被视为压入堆栈的正常值吗?谢谢你的回答。
  • @CătălinaSîrbu:“为什么这个参数如此特别?它不被视为一个普通值被压入堆栈吗?” - va_start 接受参数的 name,而不是 value。知道参数名称允许获取参数在堆栈上的位置(地址)。因为连续函数的参数在堆栈附近传递,所以通过知道函数参数的位置,可以计算下一个(按声明顺序)参数的位置。因此,知道 last named 参数的位置允许计算 first variadic 参数的位置。
  • @CătălinaSîrbu 已编辑,请参阅我的答案底部。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-12
  • 2021-02-14
  • 1970-01-01
  • 1970-01-01
  • 2015-05-08
相关资源
最近更新 更多