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”。
The foobar
函数现在将访问堆栈,从 TOS 开始,即格式字符串,正如您所记得的,它是最后推送的。想象stack
是你的堆栈指针,stack[0]
是 TOS 处的值,stack[1]
是高于 TOS 的一项,依此类推:
format_string <- stack[0]
...然后解析格式字符串。解析时,它识别%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
...
这当然是一个非常不完整的伪代码,它演示了函数如何必须依赖传递的参数来找出它必须从堆栈中加载和删除的量。
Security
这种对用户提供的参数的依赖也是目前最大的安全问题之一(请参阅https://cwe.mitre.org/top25/)。用户可能很容易错误地使用可变参数函数,要么因为他们没有阅读文档,要么忘记调整格式字符串或参数列表,要么因为它们是邪恶的,或者其他什么。也可以看看格式化字符串攻击.
C 实施
在 C 和 C++ 中,可变参数函数与va_list
界面。虽然压入堆栈是这些语言固有的(在 K+R C 中,你甚至可以前向声明一个函数而不声明它的参数,但仍然使用任何数量和种类的参数来调用它),从这样一个未知的参数列表中读取是通过接口进行的va_...
- 宏和va_list
-type,它基本上抽象了低级堆栈帧访问。