C++ 从技术上讲,可变函数是如何工作的?printf是如何工作的?

C++ 从技术上讲,可变函数是如何工作的?printf是如何工作的?,c++,c,variadic-functions,C++,C,Variadic Functions,我知道我可以使用va_arg编写我自己的可变函数,但是可变函数如何在引擎盖下工作,即在汇编指令级别上 例如,printf如何可能接受数量可变的参数 *没有规则就没有例外。没有C/C++语言,但是,这两种语言都可以回答这个问题 *注:最初给出的答案是,但似乎不适用于提问者 < P> C和C++标准对它的工作方式没有任何要求。遵从规则的编译器可能会决定在引擎盖下发出链表、std::stack甚至是神奇的小马灰尘(根据@Xeo的评论) 然而,它通常是按如下方式实现的,即使像CPU寄存器中的内

我知道我可以使用
va_arg
编写我自己的可变函数,但是可变函数如何在引擎盖下工作,即在汇编指令级别上

例如,
printf
如何可能接受数量可变的参数


*没有规则就没有例外。没有C/C++语言,但是,这两种语言都可以回答这个问题

*注:最初给出的答案是,但似乎不适用于提问者

< P> C和C++标准对它的工作方式没有任何要求。遵从规则的编译器可能会决定在引擎盖下发出链表、
std::stack
甚至是神奇的小马灰尘(根据@Xeo的评论)

然而,它通常是按如下方式实现的,即使像CPU寄存器中的内联或传递参数这样的转换可能不会留下任何讨论过的代码

还请注意,这个答案在下面的图片中特别描述了向下增长的堆栈;此外,此答案只是为了演示该方案而进行的简化(请参见)

如何使用非固定数量的参数调用函数 这是可能的,因为底层机器体系结构对每个线程都有一个所谓的“堆栈”。堆栈用于将参数传递给函数。例如,当您有:

foobar("%d%d%d", 3,2,1);
然后编译成这样的汇编代码(示例性的和示意性的,实际代码可能看起来不同);请注意,参数是从右向左传递的:

push 1
push 2
push 3
push "%d%d%d"
call foobar
这些推送操作填充堆栈:

              []   // empty stack
-------------------------------
push 1:       [1]  
-------------------------------
push 2:       [1]
              [2]
-------------------------------
push 3:       [1]
              [2]
              [3]  // there is now 1, 2, 3 in the stack
-------------------------------
push "%d%d%d":[1]
              [2]
              [3]
              ["%d%d%d"]
-------------------------------
call foobar   ...  // foobar uses the same stack!
format_string <- stack[0]
offset <- 1
while (parsing):
    token = tokenize_one_more(format_string)
    if (needs_integer (token)):
        value <- stack[offset]
        offset = offset + 1
    ...
底部堆栈元素称为“堆栈顶部”,通常缩写为“TOS”

foobar
函数现在将访问堆栈,从TOS开始,即格式字符串,正如您所记得的,格式字符串是最后一次按下的。想象一下,
stack
是堆栈指针,
stack[0]
是TOS上的值,
stack[1]
是TOS上的值,依此类推:

format_string <- stack[0]
这当然是一个非常不完整的伪代码,它演示了函数如何依赖传递的参数来确定它必须从堆栈中加载和删除多少

安全 这种对用户提供参数的依赖也是当前最大的安全问题之一(请参阅)。用户可能很容易错误地使用变量函数,可能是因为他们没有阅读文档,或者忘记调整格式字符串或参数列表,或者是因为他们非常邪恶,或者是其他原因。另见

C执行
<>在C和C++中,变量函数与VAYList接口一起使用。虽然推送到堆栈上是这些语言的固有特性(但仍然使用任何数字和种类的参数调用),但从这样一个未知参数列表中读取是通过
va_…
-宏和
va_list
-类型连接的,它基本上抽象了底层堆栈帧访问。

可变函数由标准定义,几乎没有明确的限制。下面是一个例子,摘自cplusplus.com

/* va_start example */
#include <stdio.h>      /* printf */
#include <stdarg.h>     /* va_list, va_start, va_arg, va_end */

void PrintFloats (int n, ...)
{
  int i;
  double val;
  printf ("Printing floats:");
  va_list vl;
  va_start(vl,n);
  for (i=0;i<n;i++)
  {
    val=va_arg(vl,double);
    printf (" [%.2f]",val);
  }
  va_end(vl);
  printf ("\n");
}

int main ()
{
  PrintFloats (3,3.14159,2.71828,1.41421);
  return 0;
}
/*va_开始示例*/
#包括/*printf*/
#包括/*va_列表、va_开始、va_参数、va_结束*/
无效打印浮动(整数n,…)
{
int i;
双val;
printf(“打印浮动:”);
va_列表vl;
va_启动(vl,n);

对于(i=0;i@B这些都是“猜测”;我将细化文本。@B挈挈挈挈挈:
您只是复制并粘贴了答案。因此,这个问题与其他问题重复。
这是一个非序列。重复的答案不会重复问题。可能会重复@MatthieuM。:我不确定这是否是“技术性的”够了。我会改进我的问题。@phresnel:这似乎比你自己的答案更专业(或至少更精确),尽管它专门针对一种架构。请注意,该标准对如何工作没有实际要求。值得一提的是,它还可以使用神奇的小马灰尘来工作。(此外,我没有投反对票。)
stdcall
不能用作变量函数的调用约定。即使变量函数的编写者知道参数的数量,可能编译器也不知道。标准允许通过调用
va_start
乘法或使用
va_copy
来使用多个
va_列表
,因此
va_arg
并不简单通过
pop
,但通过直接读取堆栈(例如,
mov eax,[valist]
)实现。因此编译器无法计算在编译变量函数时应弹出多少堆栈-仅“调用者”知道这一点。因此,应该使用
cdecl
。当然,如果堆栈向上增长,而不是向下增长,则所有内容都是相反的。即使您描述它,这也不完全正确。在访问它们时,参数不会真正弹出。通常,
va_list
将定义指针类型,
va_arg
将根据t更新它正在提取的参数的类型。(这就是为什么
va_arg
的类型参数必须对应于提升的类型,而不是您实际需要的类型。)@ikh stdcall和cdecl都是纯Microsoft约定。大多数其他系统只有一个基本约定,并以相同的方式将所有参数传递给所有函数。少数不使用(Microsoft除外)的系统使用标准定义的机制指定调用约定:
extern“C”
(或者用别的东西代替
C
)-1:这仅仅是(而且是详细的)描述堆栈如何传递固定数量的参数。它几乎忽略了在大多数体系结构中使用可变数量参数的可变函数调用如何实际实现的所有要点:即,除了堆栈指针之外,还使用帧指针或参数计数器。如果没有这些,则调用nction不知道调用帧的底部在哪里。
在通常期望被调用函数清理推送参数的实现中可能是至关重要的