我试图了解可变长参数在C语言中的工作原理。
基本上是在调用变长参数函数(例如:printf(const char * format,...);)时,将参数复制到哪里(堆栈/寄存器?)?以及被调用函数如何获取有关通过调用函数传递的参数的信息?
我非常感谢任何形式的帮助。 预先感谢。
答案 0 :(得分:0)
将参数复制到哪里(堆栈/寄存器?)?
不同。在x64上,使用常规约定:前几个参数(取决于类型)可能进入寄存器,而其他参数进入堆栈。 C标准要求编译器至少支持一个函数的127个参数,因此不可避免的是其中一些将进入堆栈。
被调用函数如何获取有关通过调用函数传递的参数的信息?
使用初始参数,例如printf格式字符串。 C语言中的varargs支持工具不允许该函数检查参数的数量和类型,而只能一次获取一个(如果它们被强制转换,或者如果访问的参数多于传递的参数,则结果)是未定义的行为)。
答案 1 :(得分:0)
大多数实现将参数推入堆栈,使用寄存器在寄存器匮乏的体系结构上无法正常工作,或者如果参数通常比寄存器多,则无法使用
。并且被调用函数完全不了解参数,其数量或类型。这就是为什么printf
和相关功能使用格式说明符。然后,被调用的函数将根据该格式说明符(使用va_arg
“函数”)解释堆栈的下一部分。
如果va_arg
提取的类型与参数的实际类型不匹配,您将具有未定义的行为。
答案 2 :(得分:0)
传统上,无论其他寄存器传递优化如何,参数总是“总是”压入堆栈,然后va_list基本上只是指向堆栈的指针,以标识va_arg的下一个参数。但是,在新处理器和编译器优化设置上非常喜欢寄存器传递,以至于甚至将varargs也作为寄存器放置。
有了这个,va_list
变成一个小的数据结构(或指向该数据结构的指针),该结构捕获所有这些寄存器参数,如果参数数量太多,则/ and /具有指向堆栈的指针。 。 va_arg
宏首先遍历捕获的寄存器,然后遍历堆栈条目,因此va_list
也具有“当前索引”。
请注意,至少在gcc实现va_list
中是一个混合对象:在主体中声明时,它是结构的实例,但是当作为参数传递时,它神奇地成为了指针,就像C ++引用,即使C没有引用的概念。
在某些平台上,va_list
还分配了一些动态内存,这就是为什么您应该始终调用va_end
的原因。
答案 3 :(得分:0)
使用变量参数列表是'C'语言的标准功能,因此必须在存在C编译器的任何计算机上强制使用。
当我们说任何机器时,我们的意思是独立于参数传递,寄存器,堆栈或两者的使用方式,我们必须具有该功能。
实际上,实现功能真正需要的是过程的确定性。参数是以堆栈,寄存器,两者还是以其他MCU自定义方式传递都无关紧要,重要的是完成方式定义良好且始终相同。
如果尊重此属性,我们可以确保始终可以遍历参数列表,并访问每个参数。
实际上,在 ABI ( A 应用程序 B 初始我界面,请参见https://en.wikipedia.org/wiki/Application_binary_interface),按照规则,相反,您始终可以回溯参数。
无论如何,在绝大多数系统上,ABI的简单反向工程不足以恢复参数,即参数大小与标准CPU寄存器/堆栈大小不同,在这种情况下,您需要有关参数的更多信息正在寻找:操作数大小。
让我们回顾一下C语言中的变量参数处理。首先,您声明一个具有单个整数类型参数的函数,其中包含作为变量参数传递的参数数量,以及变量部分的3个点:
int foo(int cnt, ...);
要正常访问变量参数,请按以下方式使用<stdarg.h>
标头中的定义:
int foo(int cnt, ...)
{
va_list ap; //pointer used to iterate through parameters
int i, val;
va_start(ap, cnt); //Initialize pointer to the last known parameter
for (i = 0; i > cnt; i++)
{
val = va_arg(ap, int); //Retrieve next parameter using pointer and size
printf("%d ", val); // Print parameter, an integer
}
va_end(ap); //Release pointer. Normally do_nothing
putchar('\n');
}
在基于堆栈的计算机(即x86-32位)上,按顺序推送参数,上面的代码或多或少如下:
int foo(int cnt, ...)
{
char *ap; //pointer used to iterate through parameters
int i, val;
ap = &cnt; //Initialize pointer to the last known parameter
for (i = 0; i > cnt; i++)
{
/*
* We are going to update pointer to next parameter on the stack.
* Please note that here we simply add int size to pointer because
* normally the stack word size is the same of natural integer for
* that machine, but if we are using different type we **must**
* adjust pointer to the correct stack bound by rounding to the
* larger multiply size.
*/
ap = (ap + sizeof(int));
val = *((int *)ap); //Retrieve next parameter using pointer and size
printf("%d ", val); // Print parameter, an integer
}
putchar('\n');
}
请注意,如果我们访问的类型不同于int
的e / o,其大小不同于本机堆栈字的大小,则必须将指针调整为始终增加堆栈字大小的倍数。
现在考虑使用寄存器传递参数的机器,为简单起见,我们认为操作数不能大于寄存器大小,并且分配是使用寄存器顺序进行的(还请注意伪汇编器指令{{1} }将变量mov val, rx
加载到寄存器val
的内容中):
rx
希望这个概念现在已经很清楚了。
答案 4 :(得分:0)
从ABI文档中提取的,存储所有参数的方法由体系结构的ABI文档提供。
参考链接: https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf(第56页)。
注册保存区: 一个函数的序言,该函数采用可变参数列表并已知调用 宏va_start应该将参数寄存器保存到寄存器保存区域。每个自变量寄存器在寄存器保存区域中都有一个固定的偏移量。
答案 5 :(得分:-1)
那么您正在考虑汇编堆栈或注册吗?
在简单的情况下,我的意思是函数的参数数量少,参数存储在寄存器A4,B4,C4中,返回值存储在A4中。
但是,如果复杂的函数具有很多参数,则无法列出全部寄存器,因此在那时我们使用堆栈。
如果您对此答案有疑问,请随时问我。
答案 6 :(得分:-1)
C是访问这些参数的标准机制。宏在stdarg.h
http://www.cse.unt.edu/~donr/courses/4410/NOTES/stdarg/
在这里,您可以非常简单地实现sniprintf
int ts_formatstring(char *buf, size_t maxlen, const char *fmt, va_list va)
{
char *start_buf = buf;
maxlen--;
while(*fmt && maxlen)
{
/* Character needs formating? */
if (*fmt == '%')
{
switch (*(++fmt))
{
case 'c':
*buf++ = va_arg(va, int);
maxlen--;
break;
case 'd':
case 'i':
{
signed int val = va_arg(va, signed int);
if (val < 0)
{
val *= -1;
*buf++ = '-';
maxlen--;
}
maxlen = ts_itoa(&buf, val, 10, maxlen);
}
break;
case 's':
{
char * arg = va_arg(va, char *);
while (*arg && maxlen)
{
*buf++ = *arg++;
maxlen--;
}
}
break;
case 'u':
maxlen = ts_itoa(&buf, va_arg(va, unsigned int), 10, maxlen);
break;
case 'x':
case 'X':
maxlen = ts_itoa(&buf, va_arg(va, int), 16, maxlen);
break;
case '%':
*buf++ = '%';
maxlen--;
break;
}
fmt++;
}
/* Else just copy */
else
{
*buf++ = *fmt++;
maxlen--;
}
}
*buf = 0;
return (int)(buf - start_buf);
}
int sniprintf(char *buf, size_t maxlen, const char *fmt, ...)
{
int length;
va_list va;
va_start(va, fmt);
length = ts_formatstring(buf, maxlen, fmt, va);
va_end(va);
return length;
}
它是来自atollic工作室的微型printf。
此处显示了所有机制(包括将参数列表传递给另一个函数的情况。