关于C#:如何在gcc中实现变量参数?

How are variable arguments implemented in gcc?

1
int max(int n, ...)

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

我有兴趣知道宏va_endva_startva_arg是如何工作的?

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


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

1
2
3
4
5
6
Higher memory address    Last parameter
                         Penultimate parameter
                         ....
                         Second parameter
Lower memory address     First parameter
       StackPointer  ->  Return address

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

即使没有...参数类型,参数也总是像this1一样存储。

va_start宏只是设置指向第一个函数参数的指针,例如:-

1
2
3
4
5
 void func (int a, ...)
 {
   // va_start
   char *p = (char *) &a + sizeof a;
 }

这使得p指向第二个参数。 va_arg宏执行此操作: -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 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将参数存储在寄存器中而不是使用堆栈。 ...参数的存在将关闭此功能并使编译器使用堆栈。

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

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

    1
    max(a, b, c);

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

    1
    2
    3
    4
    5
    6
          +-----+
          |  c  |
          |  b  |
          |  a  |
          | ret |
    SP -> +-----+

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

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

    1
    2
    3
    4
    5
    6
          +-----+
          |  c  |
    ap -> |  b  |
          |  a  |
          | ret |
    SP -> +-----+

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

    1
    2
    3
    4
    5
    6
          +-----+
    ap -> |  c  |
          |  b  |
          |  a  |
          | 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()都会生成引用的内联代码用于在编译时验证预期返回的任何参数特定隐藏字段是与正在编译的表达式用法兼容的赋值,并执行任何所需的参数提升/强制。命名字段值大小和隐藏字段大小的总和确定在调用之后编译的值,或者在callee清理模型的函数结尾中,以在返回时调整帧。

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


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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,buffer);
    }