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

Bru*_*uce 23 c gcc variadic-functions calling-convention

int max(int n, ...)
Run Code Online (Sandbox Code Playgroud)

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

我想知道怎么做宏va_end,va_startva_arg工作?

调用者是否将参数数组的地址作为max的第二个参数传递?

Ski*_*izz 28

如果你看看C语言将参数存储在堆栈中的方式,宏的工作方式应该变得清晰: -

Higher memory address    Last parameter
                         Penultimate parameter
                         ....
                         Second parameter
Lower memory address     First parameter
       StackPointer  ->  Return address
Run Code Online (Sandbox Code Playgroud)

(注意,根据硬件的不同,堆栈指针可能会向下一行,而较高和较低的指针可能会被交换)

即使没有参数类型,参数总是像这样存储1....

va_start宏只设置了一个指向第一个功能参数,例如: -

 void func (int a, ...)
 { 
   // va_start
   char *p = (char *) &a + sizeof a;
 }
Run Code Online (Sandbox Code Playgroud)

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);
 }
Run Code Online (Sandbox Code Playgroud)

va_end宏仅设置pNULL.

笔记:

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

  • 这实际上是特定于平台的,因为*许多*调用约定(包括常见的x64,PPC,ARM)将大多数参数传递给寄存器.许多平台没有将返回地址放在堆栈上,一个或两个平台的堆栈向上而不是向下扩展,而一些调用约定以相反的顺序在堆栈上放置参数. (18认同)
  • 在64位上,仍然可以使用可变参数,但是[`va_arg()`实现会非常复杂](https://blog.nelhage.com/2010/10/amd64-and-va_arg/),需要编译器支持和不仅仅是用户模式代码. (3认同)
  • 所有主要的 C 编译器都选择遵循标准调用约定/ABI,因此您可以在同一目标平台上由不同编译器编译的库中调用库函数。在 x86-64 上,那些调用约定(x86-64 System V 和 x86-64 Windows 约定)都使用 register args,即使对于可变参数函数也是如此。有关调用 printf 的示例,请参阅 /sf/ask/434886581/。所以不,编译器不能只是编造一些东西,不,`...` 不会禁用寄存器 arg 传递。是的,当然不同的目标有不同的调用约定。 (3认同)
  • *“...”参数的存在将关闭此功能,并使编译器无法使用堆栈。*实际上不是,在 x86-64 上,Windows 和 System V 调用约定仍然在相同的寄存器中传递参数以用于可变参数或不。Windows 调用约定要求堆栈参数(如果有)之前的返回地址上方有“影子空间”,因此可变参数函数可以将 4 个寄存器转储到影子空间中,并将其参数作为数组进行索引。(调用者需要在可变参数函数的整数和 XMM 寄存器中复制 FP 参数)。 (2认同)

Som*_*ude 8

当参数在堆栈上传递时,va_"函数"(它们大部分时间都是作为宏实现的)只是简单地操作私有堆栈指针.此私有堆栈指针从传递给的参数中存储va_start,然后va_arg在迭代参数时从"堆栈""弹出"参数.

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

max(a, b, c);
Run Code Online (Sandbox Code Playgroud)

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

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

SP是真正的堆栈指针,它不是真的a,b而且c在堆栈上但它们的值.ret是返回地址,在函数完成时跳转到的位置.

什么va_start(ap, n)是参数的地址(n在你的函数原型中)并从中计算下一个参数的位置,所以我们得到一个新的私有堆栈指针:

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

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

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

该描述当然是简化的,但显示了原理.

  • 这就是它在32位平台上的工作方式.在64位平台上,一些参数通过寄存器传递,其他参数传递到堆栈,因此64位实现更复杂. (3认同)