vararg 函数如何找出机器代码中的参数数量?

How do vararg functions find out the number of arguments in machine code?

提问人:masterxilo 提问时间:3/11/2011 最后编辑:Peter Cordesmasterxilo 更新时间:8/30/2019 访问量:9147

问:

printf 这样的可调参数函数如何找出它们得到的参数数量?

参数的数量显然不是作为(隐藏)参数传递的(请参阅此处的 asm 示例中对 printf 的调用)。

有什么诀窍?

程序集 printf calling-convention abi variadic

评论

0赞 Paul R 3/11/2011
对于 et al,您知道根据格式字符串需要多少个额外的参数。printf
0赞 sharptooth 3/11/2011
我希望在所有参数之后都会推送参数数量,但在您链接到的列表中并非如此。

答:

14赞 coldfix 3/11/2011 #1

诀窍是你以某种方式告诉他们。因为您必须提供一个格式字符串,其中甚至包含类型信息(尽管可能不正确)。提供这些信息的方式主要是用户合同,而且往往容易出错。printf

至于调用约定:通常参数从左到右推送到堆栈上,最后推送到后跳地址上。调用例程清除堆栈。因此,在技术上不需要被调用的例程来知道参数的数量。

编辑:在C++0x中,有一种安全的方法(甚至是类型安全!)来调用可变参数函数!

评论

0赞 masterxilo 3/11/2011
啊,这就是诀窍,它只是假设格式字符串是正确的。这种安全方式是什么?
0赞 2/1/2015
@OP C++也是“神秘的”(更难拆卸)。这就是我喜欢这种语言的原因。
9赞 Ruud Koot 3/11/2011 #2

隐式地,来自格式字符串。请注意,stdarg.h 不包含任何宏来检索传递的“变量”参数总数。这也是 C 调用约定要求调用方清理堆栈的原因之一,即使这会增加代码大小。

9赞 ninjalj 3/14/2011 #3

这就是为什么在 C 调用约定上以相反的顺序推送参数的原因,例如:

如果您致电:

printf("%s %s", foo, bar);

堆栈的最终结果是:

  ...
+-------------------+
| bar               |
+-------------------+
| foo               |
+-------------------+
| "%s %s"           |
+-------------------+
| return address    |
+-------------------+
| old frame pointer | <- frame pointer
+-------------------+
  ...

参数使用其与帧指针的偏移量间接访问(帧指针可以由知道如何从堆栈指针计算事物的智能编译器省略)。在此方案中,第一个参数始终位于已知地址,该函数访问的参数与其第一个参数告诉它的数量一样多。

请尝试以下操作:

printf("%x %x %x %x %x %x\n");

这将转储部分堆栈。

评论

0赞 Peter Cordes 8/26/2017
在 x86-64 SystemV 调用约定中,前 6 个整数/指针参数在寄存器中传递,因此只有最后一个参数会让 printf 从堆栈内存中获取值。%x
5赞 Ciro Santilli OurBigBook.com 7/20/2015 #4
  • AMD64 System V ABI(Linux、Mac OS X)确实传递了数向量 (SSE / AVX) varargs(RAX 的低字节),这与任何标准的 IA-32 调用约定不同。Смотритетакже: 为什么在调用 printf 之前将 %eax 归零?al

    但最多只能有 8 个(要使用的最大寄存器数)。和 IIRC,ABI 允许于 XMM/YMM/ZMM 参数的实际数量,但不得小于。因此,它通常不会总是告诉您 FP 参数的数量;你不能说出有多少超过8,并且被允许多算。alal

    它只能出于性能原因,跳过将不需要的向量寄存器保存到“3.5.7 变量参数列表”中提到的“寄存器保存区域”。例如,GCC 生成测试然后转储 XMM0 的代码。7 堆栈或什么都没有。(或者,如果该函数与任何地方一起使用,则 YMM0..7.)al!=0VA_ARG__m256

  • 在 C 级别,除了解析其他人提到的格式字符串之外,还有其他技术。您还可以:

    • 传递一个哨兵来指示最后一个参数,就像 execl 一样。(void *)0

      您将需要使用 function 属性来帮助 GCC 在编译时强制执行: C 警告 函数调用中缺少哨兵sentinel

    • 将其作为带有 varargs 个数的额外整数参数传递

    • 使用 function 属性帮助 GCC 强制执行已知类型的格式字符串,例如formatprintfstrftime

相关:变量参数如何在 gcc 中实现?

评论

1赞 Peter Cordes 8/26/2017
gcc/clang 目前只检查是否为零,如果非零,则将所有 8 个寄存器转储到堆栈。(而不是使用倒计时的循环。alxmmal
0赞 Peter Cordes 8/30/2019
是的,这只是出于性能原因。您不能设置为较低的值,以使被调用方在堆栈中查找 FP 参数而不是寄存器。调用约定要求为 >= FP 参数数,最多 8 个。在 GCC 的实现中,传递最终将导致被调用方查看其未初始化的转储区域。(在 asm 中违反 ABI 的后果与 C UB 非常相似:任何事情都可能发生。被调用者可能会认为这意味着没有 FP 参数,并进行一些稍后中断的优化。这取决于 asm 的编译器(或人类)作者。alal0al=0