繁体   English   中英

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

[英]Technically, how do variadic functions work? How does printf work?

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

例如, printf如何采用可变数量的参数?


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

*注意:最初给出的答案如何输出printf函数可以输出数字中的可变参数? ,但它似乎不适用于提问者

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

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

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

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

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

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!

底部堆栈元素称为“堆栈顶部”,通常缩写为“TOS”。

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

format_string <- stack[0]

...然后解析format-string。 解析时,它识别%d -tokens,并为每个加载一个来自堆栈的值:

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/ )。 用户可能会错误地使用可变参数函数,因为他们没有阅读文档,或者忘记调整格式字符串或参数列表,或者因为它们是邪恶的,或者其他什么。 另请参见格式字符串攻击

C实施

在C和C ++中,可变参数函数与va_list接口一起使用。 虽然推入堆栈是这些语言固有的( 在K + RC中你甚至可以在不声明其参数的情况下向前声明一个函数 ,但仍然用任何数字和类型的参数调用它),从这样一个未知的参数列表中读取是接口的通过va_... -macros和va_list -type,它基本上抽象了低级别的堆栈帧访问。

变量函数由标准定义,几乎没有明确的限制。 这是一个例子,取自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再次使事情变得正确。

在最常见的基于堆栈的情况下, va_list仅仅是指向堆栈上的参数的指针,并且va_arg递增指针,强制转换它并将其解引用为值。 然后va_start通过一些简单的算术(和内部知识)初始化该指针,而va_end什么都不做。 没有奇怪的汇编语言,只是知道堆栈中的东西。 阅读标准标题中的宏以找出它是什么。

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM