如何在gcc中实现变量参数?

时间:2012-09-11 13:58:55

标签: c gcc variadic-functions calling-convention

int max(int n, ...)

我正在使用cdecl调用约定,其中调用者在被调用者返回后清理变量。

我很想知道宏va_endva_startva_arg是如何工作的?

调用者是否将参数数组的地址作为max?

的第二个参数传递

4 个答案:

答案 0 :(得分:28)

如果你看看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

注意:

  1. 优化编译器和一些RISC CPU将参数存储在寄存器中而不是使用堆栈。 ...参数的存在将关闭此功能并使编译器使用堆栈。

答案 1 :(得分:8)

当参数在堆栈上传递时,va_“函数”(它们大部分时间都是作为宏实现的)只是操作私有堆栈指针。这个私有堆栈指针存储在传递给va_start的参数中,然后va_arg“弹出”来自“堆栈”的参数,因为它迭代参数。

假设您使用三个参数调用函数max,如下所示:

max(a, b, c);

max函数中,堆栈基本上如下所示:

      +-----+
      |  c  |
      |  b  |
      |  a  |
      | ret |
SP -> +-----+

SP是真正的堆栈指针,并不是堆栈上的abc,而是它们的值。 ret是返回地址,在函数完成时跳转到的位置。

va_start(ap, n)做的是获取参数的地址(函数原型中的n)并从中计算下一个参数的位置,因此我们得到一个新的私有堆栈指针:

      +-----+
      |  c  |
ap -> |  b  |
      |  a  |
      | ret |
SP -> +-----+

当你使用va_arg(ap, int)时,它返回私有堆栈指针所指向的内容,然后通过将私有堆栈指针更改为现在指向下一个参数来“弹出”它。堆栈现在看起来像这样:

      +-----+
ap -> |  c  |
      |  b  |
      |  a  |
      | ret |
SP -> +-----+

这种描述当然是简化的,但显示了原则。

答案 2 :(得分:0)

一般来说,当我使用(,...)声明函数原型时,我如何理解target.def,编译器会设置一个标记有varargs标志的解析树,并引用指定参数的类型。对于严格的C一致性,当该参数是va_start的命名字段并且可能返回到va_arg()时,每个命名参数应该获得附加到设置va_list所需的任何附加信息,但是大多数编译器只为最后命名的参数生成此信息。当定义函数时,它的序言生成器注意到varargs标志已设置并添加了设置任何隐藏字段所需的代码,它添加到具有va_start宏可以引用的已知偏移的帧。

当找到对该函数的引用时,它会为表示...的每个参数创建额外的解析和代码生成树,这可能会引入附加到字段的运行时类型信息的其他隐藏字段,例如数组边界为命名参数设置va_start和va_arg。这个组合树确定生成什么代码以将参数值复制到框架上,序言设置了va_start从任意或最后命名参数开始创建va_list所需的内容,并且每次调用va_arg()都会生成内联代码,引用用于在编译时验证的任何参数特定隐藏字段,预期返回是与正在编译的表达式用法兼容的赋值,并执行任何所需的参数提升/强制。命名字段值大小和隐藏字段大小的总和决定了在调用之后编译的值,或者在callee清理模型的函数结尾中,以在返回时调整帧。

这些步骤中的每一步都有处理器和调用约定依赖关系,封装在config / proc / proc.c和proc.h文件中,它们覆盖了va_start()和va_arg()的简单默认定义,假设每个参数都有一个固定大小在堆栈上的第一个命名参数上方分配一些距离。对于某些平台或语言,作为单独的malloc()实现的参数帧比固定大小的堆栈更令人满意。另请注意,这些用法不是线程安全的;将va_list引用传递给另一个线程是不安全的,没有未指定的方法确保参数帧因函数返回或线程中止而无效。

答案 3 :(得分:-2)

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);
}