27
int max(int n, ...)

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

我有兴趣知道宏是如何va_end工作va_startva_arg

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

4

3 回答 3

36

如果您查看 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 将参数存储在寄存器中,而不是使用堆栈。参数的存在...将关闭此功能并让编译器使用堆栈。
于 2012-09-11T14:27:07.517 回答
10

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

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

max(a, b, c);

max函数内部,堆栈基本上是这样的:

      +-----+
      | c |
      | 乙 |
      | 一个 |
      | 回复 |
SP -> +-----+

SP是真正的堆栈指针,它不是真的a,堆栈上bc那个,而是它们的值。ret是返回地址,函数完成后跳转到哪里。

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

      +-----+
      | c |
ap -> | 乙 |
      | 一个 |
      | 回复 |
SP -> +-----+

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

      +-----+
ap -> | c |
      | 乙 |
      | 一个 |
      | 回复 |
SP -> +-----+

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

于 2012-09-11T14:13:35.577 回答
0

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

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

Each of these steps has processor and calling convention dependencies, encapsulated in the config/proc/proc.c and proc.h files, that override the simplistic default definitions of va_start() and va_arg() that assume each argument has a fixed size allocated some distance above the first named argument on a stack. For some platforms or languages parameter frames implemented as separate malloc()s are more desirable than a fixed size stack. Also note these usages are not thread safe; it is unsafe to pass a va_list reference to another thread without unspecified means of ensuring the parameter frame is not made invalid due to function return or abort of the thread.

于 2018-06-10T19:24:22.057 回答