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++中,可变参数函数与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;
}
Run Code Online (Sandbox Code Playgroud)
假设大致如下.
...实际上什么也不做,只是告诉编译器做正确的事.va_start宏可以返回允许检索参数的对象.类型是va_list.va_list对象中可以va_arg迭代每个可变参数,并将其值强制转换为兼容类型.va_start因此va_end使事情便又.在最常见的基于堆栈的情况下,va_list它只是指向堆栈上的参数va_arg的指针,并递增指针,强制转换它并将其解引用到一个值.然后va_start通过一些简单的算术(和内部知识)初始化该指针并且va_end什么都不做.没有奇怪的汇编语言,只是知道堆栈中的东西.阅读标准标题中的宏以找出它是什么.
某些编译器(MSVC)将需要特定的调用序列,因此调用者将释放堆栈而不是被调用者.
printf像这样的功能就像这样.fixed参数是一个格式字符串,它允许计算参数的数量.
功能,如vsprintf传递va_list对象作为一个正常的参数类型.
如果您需要更多或更低级别的详细信息,请添加问题.