如何在gcc中实现变量参数?
int max(int n, ...)
我正在使用cdecl
调用约定,其中调用者在被调用者返回后清理变量。
我有兴趣知道宏va_end
, va_start
和va_arg
工作的?
调用者是否将参数数组的地址作为max的第二个参数传递?
如果你看看C语言将参数存储在堆栈中的方式,宏的工作方式应该变得清晰: –
Higher memory address Last parameter Penultimate parameter .... Second parameter Lower memory address First parameter StackPointer -> Return address
(注意,根据硬件的不同,堆栈指针可能会向下一行,而较高和较低的指针可能会被交换)
即使没有...
参数类型,参数总是像这样存储1 。
va_start
宏只是设置指向第一个函数参数的指针,例如: –
void func (int a, ...) { // va_start char *p = (char *) &a + sizeof a; }
这使得p
指向第二个参数。 va_arg
宏执行此操作: –
void func (int a, ...) { // va_start char *p = (char *) &a + sizeof a; // va_arg int i1 = *((int *)p); p += sizeof (int); // va_arg int i2 = *((int *)p); p += sizeof (int); // va_arg long i2 = *((long *)p); p += sizeof (long); }
va_end
宏只是将p
值设置为NULL
。
笔记:
- 优化编译器和一些RISC CPU将参数存储在寄存器中而不是使用堆栈。
...
参数的存在将关闭此function并使编译器使用堆栈。
当参数在堆栈上传递时, va_
“函数”(它们大部分时间都是作为宏实现的)只是简单地操作私有堆栈指针。 这个私有堆栈指针存储在传递给va_start
的参数中,然后va_arg
在迭代参数时“弹出”来自“堆栈”的参数。
让我们说你用三个参数调用函数max
,如下所示:
max(a, b, c);
在max
函数内部,堆栈基本上如下所示:
+ ----- + | c | | b | | 一个| | ret | SP - > + ----- +
SP
是真正的堆栈指针,它不是堆栈上的a
, b
和c
,而是它们的值。 ret
是返回地址,在函数完成时跳转到的位置。
va_start(ap, n)
所做的是获取参数的地址(函数原型中的n
)并从中计算下一个参数的位置,因此我们得到一个新的私有堆栈指针:
+ ----- + | c | ap - > | b | | 一个| | ret | SP - > + ----- +
当你使用va_arg(ap, int)
它返回私有堆栈指针所指向的内容,然后通过将私有堆栈指针更改为现在指向下一个参数来“弹出”它。 堆栈现在看起来像这样:
+ ----- + ap - > | c | | b | | 一个| | ret | SP - > + ----- +
该描述当然是简化的,但显示了原理。
一般来说,当我使用(,…)声明函数原型时,我如何理解target.def,编译器会设置一个标记有varargs标志的解析树,并引用指定参数的类型。 对于严格的C一致性,当该参数是va_start的命名字段并且可能返回到va_arg()时,每个命名参数应该获得附加到设置va_list所需的任何附加信息,但是大多数编译器只为最后命名的参数生成此信息。 当函数被定义时,它的序言生成器注意到varargs标志被设置并添加了设置任何隐藏字段所需的代码,它添加到具有va_start宏可以引用的已知偏移的帧。
当它找到对该函数的引用时,它会为表示…的每个参数创建额外的解析和代码生成树,这可能会引入运行时类型信息的其他隐藏字段,例如数组边界,这些字段会附加到va_start的字段设置中和va_arg用于命名参数。 这个组合树确定生成什么代码以将参数值复制到框架上,序言设置了va_start从任意或最后命名参数开始创建va_list所需的内容,并且每次调用va_arg()都会生成引用的内联代码用于在编译时validation预期返回的任何参数特定隐藏字段是与正在编译的表达式用法兼容的赋值,并执行任何所需的参数提升/强制。 命名字段值大小和隐藏字段大小的总和确定在调用之后编译的值,或者在callee清理模型的函数结尾中,以在返回时调整帧。
这些步骤中的每一步都有处理器和调用约定依赖关系,封装在config / proc / proc.c和proc.h文件中,它们覆盖了va_start()和va_arg()的简单默认定义,假定每个参数都有一个固定的大小分配在堆栈上第一个命名参数之上一定距离。 对于某些平台或语言,作为单独的malloc()实现的参数帧比固定大小的堆栈更令人满意。 另请注意,这些用法不是线程安全的; 将va_list引用传递给另一个线程是不安全的,没有未指定的方法确保参数帧因函数返回或线程中止而无效。
int max(int n, const char *msg,...) { va_list args; char buffer[1024]; va_start(args, msg); nb_char_written = vsnprintf(buffer, 1024, msg, args); va_end(args); printf("(%d):%s\n",n,buffer); }