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

时间:2014-04-16 09:00:29

标签: c++ c variadic-functions

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

例如,printf如何获取可变数量的参数?


<子> *没有例外的规则。没有语言C / C ++,但是,这两个问题都可以解答

<子> *注意:最初给予How can printf function can take variable parameters in number while output them?的答案,但它似乎不适用于提问者

2 个答案:

答案 0 :(得分:70)

C和C ++标准对它的工作方式没有任何要求。一个符合规范的编译器可能决定发布链式列表,std::stack<boost::any>甚至是魔法小马(根据@ Xeo的评论)。

但是,它通常按如下方式实现,即使在CPU寄存器中内联或传递参数等转换也不会留下讨论代码的任何内容。

请注意,此答案专门描述了以下视觉效果中向下增长的堆栈;此外,这个答案仅仅是为了演示该方案的简化(请参阅https://en.wikipedia.org/wiki/Stack_frame)。

如何使用非固定数量的参数调用函数

这是可能的,因为底层机器架构有一个所谓的#34;堆栈&#34;为每个线程。堆栈用于将参数传递给函数。例如,当你有:

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!

底部堆栈元素称为&#34;堆栈顶部&#34;,通常缩写为&#34; TOS&#34;。

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

format_string <- stack[0]

...然后解析format-string。在解析时,它会识别%d - 令牌,并为每个令牌从堆栈中加载一个值:

format_string <- stack[0]
offset <- 1
while (parsing):
    token = tokenize_one_more(format_string)
    if (needs_integer (token)):
        value <- stack[offset]
        offset = offset + 1
    ...

这当然是一个非常不完整的伪代码,它演示了函数如何依赖传递的参数来确定从堆栈中加载和删除它的程度。

安全

这种对用户提供的参数的依赖也是目前最大的安全问题之一(见https://cwe.mitre.org/top25/)。用户可能会错误地使用可变参数函数,因为他们没有阅读文档,或者忘记调整格式字符串或参数列表,或者因为它们是邪恶的,或者其他什么。另请参阅Format String Attack

C实施

在C和C ++中,可变参数函数与va_list接口一起使用。虽然推送到堆栈是这些语言固有的(in K+R C you could even forward-declare a function without stating its arguments,但仍然用任何数字和类型的参数调用它),从这样一个未知的参数列表中读取通过va_... - 宏和{ {1}} - type,它基本上抽象了低级别的堆栈帧访问。

答案 1 :(得分:5)

可变参数函数由标准定义,几乎没有明确的限制。这是一个例子,取自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;
}

假设大致如下。

  1. 必须有(至少一个)第一个固定的命名参数。除了告诉编译器做正确的事情之外,...实际上什么都不做。
  2. 固定参数通过未指定的机制提供有关有多少可变参数的信息。
  3. 从固定参数中,va_start宏可以返回一个允许检索参数的对象。类型为va_list
  4. va_list对象可以va_arg遍历每个可变参数,并将其值强制转换为兼容类型。
  5. va_start可能发生了一些奇怪的事情,所以va_end再次使事情变得正确。
  6. 在最常见的基于堆栈的情况下,va_list只是指向堆栈上的参数的指针,va_arg递增指针,强制转换它并将其取消引用到一个值。然后va_start通过一些简单的算术(和内部知识)初始化该指针,而va_end什么都不做。没有奇怪的汇编语言,只是知道堆栈中的东西。阅读标准标题中的宏以找出它是什么。

    某些编译器(MSVC)需要特定的调用序列,调用者将释放堆栈而不是被调用者。

    printf这样的功能就像这样。 fixed参数是一个格式字符串,它允许计算参数的数量。

    vsprintf这样的函数将va_list对象作为普通参数类型传递。

    如果您需要更多或更低级别的详细信息,请添加问题。