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

Seb*_*ach 56 c c++ variadic-functions

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

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


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

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

Seb*_*ach 70

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

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

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

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

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

foobar("%d%d%d", 3,2,1);
Run Code Online (Sandbox Code Playgroud)

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

push 1
push 2
push 3
push "%d%d%d"
call foobar
Run Code Online (Sandbox Code Playgroud)

那些推送操作填满了堆栈:

              []   // 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!
Run Code Online (Sandbox Code Playgroud)

底部堆栈元素称为"堆栈顶部",通常缩写为"TOS".

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

format_string <- stack[0]
Run Code Online (Sandbox Code Playgroud)

...然后解析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
    ...
Run Code Online (Sandbox Code Playgroud)

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

安全

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

C实施

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

  • 请注意,该标准没有对其工作原理提出任何实际要求.对于它的价值,它也可以使用神奇的小马粉尘使它工作.(另外,我没有投票.) (16认同)
  • -1:这仅仅(并且详细地)描述了堆栈如何工作以传递固定数量的参数.它设法错过几乎所有关于如何在大多数体系结构中实际实现具有可变数量参数的Variadic函数调用的突出点:即,除了堆栈指针之外还使用*帧指针*或*参数计数器*.没有那些,被调用的函数不知道调用帧的底部在哪里. (11认同)
  • `stdcall`不能用作可变参数函数的调用约定.即使可变函数的编写者知道参数的数量,也许编译器无法知道它.标准允许通过调用`va_start`乘法或使用`va_copy`来使用多个`va_list`,因此`pop_arg`不是由`pop`实现的,而是通过直接读取堆栈(例如`mov eax,[valist]`).所以编译器无法弄清楚在编译可变参数函数时应该有多少堆栈 - 只有"调用者"知道这一点.所以,应该使用`cdecl`. (4认同)
  • 当然,如果堆栈向上增长而不是向下,则一切都会逆转.即使你描述它,它也不是真的.访问它们时,参数不会真正弹出.通常,`va_list`将定义指针类型,`va_arg`将根据提取的参数的类型更新它.(这就是为什么`va_arg`的_type_参数必须对应于提升类型,而不是你可能想要的类型.) (4认同)
  • @ikh _stdcall_和_cdecl_都是纯粹的Microsoft约定.大多数其他系统只有一个基本约定,并以相同的方式将所有参数传递给所有函数.少数没有(除了Microsoft)使用标准定义的机制来指定调用约定:`extern"C"`(或其他东西而不是`C`). (3认同)

dav*_*pfx 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;
}
Run Code Online (Sandbox Code Playgroud)

假设大致如下.

  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对象作为一个正常的参数类型.

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