【问题标题】:How do Variable length arguments work in C?可变长度参数如何在 C 中工作?
【发布时间】:2019-11-19 00:27:45
【问题描述】:

我试图了解可变长度参数在 C 中是如何工作的。

基本上当可变长度参数函数(例如:printf(const char *format, ...);)被调用时,参数被复制到哪里(堆栈/寄存器?)?以及被调用函数如何获取调用函数传递的参数信息?

我非常感谢任何形式的帮助。 提前致谢。

【问题讨论】:

  • 这不是从平台到平台的通用事物,但通常它们会被最后推送。然后该函数必须使用其他信息(例如printf 的格式字符串)来了解所使用的类型。基于此,可以访问参数。
  • 具体情况取决于实现(即编译器、主机系统等)。
  • 相关的,也许是一个真正的骗子:stackoverflow.com/questions/44084884/…
  • C 是一个规范。它只指定行为,而不是用于实现它的机制。
  • 通过“可变长度参数函数”,您似乎是指 variadic 函数 - 即接受可变数量参数的函数,而不是可变长度的单个参数。

标签: c


【解决方案1】:

变量参数列表的使用是“C”语言的标准特性,因此必须在任何存在 C 编译器的机器上强制执行。

当我们说任何机器时,我们的意思是独立于用于参数传递、寄存器、堆栈或两者的方式,我们必须具有该功能。

实际上,实现功能真正需要的是过程的确定性。参数是否以堆栈、寄存器、两者或其他 MCU 自定义方式传递无关紧要,重要的是其完成方式定义明确且始终相同

如果尊重此属性,我们确信我们始终可以遍历参数列表,并访问它们中的每一个。

实际上,用于为每台机器或系统传递参数的方法,在ABIA应用程序Binary I接口,见https://en.wikipedia.org/wiki/Application_binary_interface),遵循规则,反之,可以随时回溯参数。

无论如何,在大多数系统上,ABI 的简单逆向工程不足以恢复参数,即与标准 CPU 寄存器/堆栈大小不同的参数大小,在这种情况下,您需要有关您的参数的更多信息正在寻找:操作数大小

让我们回顾一下 C 中的可变参数处理。首先,您声明一个具有单个整数类型参数的函数,保存作为可变参数传递的参数的计数,以及可变部分的 3 个点:

int foo(int cnt, ...);

要正常访问变量参数,您可以按以下方式使用 <stdarg.h> 标头中的定义:

int foo(int cnt, ...)
{
  va_list ap;  //pointer used to iterate through parameters
  int i, val;

  va_start(ap, cnt);    //Initialize pointer to the last known parameter

  for (i=0; i<cnt; i++)
  {
    val = va_arg(ap, int);  //Retrieve next parameter using pointer and size
    printf("%d ", val);     // Print parameter, an integer
  }

  va_end(ap);    //Release pointer. Normally do_nothing

  putchar('\n');
}

在基于堆栈的机器(即 x86-32 位)上,参数按顺序推送,上面的代码或多或少如下工作:

int foo(int cnt, ...)
{
  char *ap;  //pointer used to iterate through parameters
  int i, val;

  ap = &cnt;    //Initialize pointer to the last known parameter

  for (i=0; i<cnt; i++)
  {
    /*
     * We are going to update pointer to next parameter on the stack.
     * Please note that here we simply add int size to pointer because
     * normally the stack word size is the same of natural integer for
     * that machine, but if we are using different type we **must**
     * adjust pointer to the correct stack bound by rounding to the
     * larger multiply size.
     */
    ap = (ap + sizeof(int));
    val = *((int *)ap);  //Retrieve next parameter using pointer and size
    printf("%d ", val);     // Print parameter, an integer
  }

  putchar('\n');
}

请注意,如果我们访问不同于int e/o 且大小与本机堆栈字长不同的类型,必须调整指针以始终增加堆栈字长的倍数

现在考虑一台使用寄存器传递参数的机器,为简单起见,我们认为任何操作数都不能大于寄存器大小,并且分配是使用寄存器顺序进行的(另请注意伪汇编指令mov val, rx用寄存器rx的内容加载变量val):

int foo(int cnt, ...)
{
  int ap;  //pointer used to iterate through parameters
  int i, val;

/*
 * Initialize pointer to the last known parameter, in our
 * case the first in the list (see after why)
 */
  ap = 1;

  for (i=0; i<cnt; i++)
  {
    /*
     * Retrieve next parameter
     * The code below obviously isn't real code, but should give the idea.
     */
    ap++;      //Next parameter
    switch(ap)
    {
      case 1:
        __asm mov val, r1;  //Get value from register
        break;
      case 2:
        __asm mov val, r2;
        break;
      case 3:
        __asm mov val, r3;
        break;
      .....
      case n:
        __asm mov val, rn;
        break;
     }
    printf("%d ", val);     // Print parameter, an integer
  }

  putchar('\n');
}

希望这个概念现在已经足够清楚了。

【讨论】:

    【解决方案2】:

    传统上,参数“总是”压入堆栈,而不管其他寄存器传递优化如何,然后 va_list 基本上只是一个指向堆栈的指针,用于标识 va_arg 的下一个参数。然而,寄存器传递在新处理器和编译器优化设置上如此受欢迎,甚至可变参数也被当作寄存器。

    这样,va_list 成为一个小型数据结构(或指向该数据结构的指针),它捕获所有这些寄存器参数,如果参数数量太多,/和/有一个指向堆栈的指针。 va_arg 宏首先遍历捕获的寄存器,然后遍历堆栈条目,因此va_list 也有一个“当前索引”。

    请注意,至少在 gcc 实现中va_list 是一个混合对象:当在主体中声明时,它是结构的一个实例,但当作为参数传递时,它神奇地变成了一个指针,甚至像 C++ 引用尽管 C 没有引用的概念。

    在某些平台上,va_list 还分配了一些动态内存,这就是为什么您应该始终调用va_end

    【讨论】:

      【解决方案3】:

      参数被复制到哪里(堆栈/寄存器?)?

      它因人而异。在 x64 上使用常规约定:前几个参数(取决于类型)可能进入寄存器,其他参数进入堆栈。 C 标准要求编译器至少支持一个函数的 127 个参数,因此其中一些参数将不可避免地进入堆栈。

      被调用函数如何获取调用函数传递的参数信息?

      通过使用初始参数,例如 printf 格式字符串。 C 中的 varargs 支持工具不允许函数检查参数的数量和类型,只能一次获取一个(如果它们被不正确地转换,或者如果访问的参数多于传递的参数,则结果是未定义的行为)。

      【讨论】:

        【解决方案4】:

        大多数实现将参数推送到堆栈上,使用寄存器在寄存器匮乏的架构上或者如果参数通常比寄存器多时效果不佳。

        而被调用的函数对参数、它们的数量或类型一无所知。这就是为什么例如printf 和相关函数使用格式说明符。然后被调用的函数将根据该格式说明符解释堆栈的下一部分(使用va_arg“函数”)。

        如果va_arg 获取的类型与参数的实际类型不匹配,您将有未定义的行为

        【讨论】:

          【解决方案5】:

          从 ABI 文档中提取,存储所有参数的方法由架构的 ABI 文档提供。

          参考链接:https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf(第 56 页)。

          注册保存区: 函数的序言采用变量参数列表并已知调用 宏 va_start 应将参数寄存器保存到 寄存器保存区。每个参数寄存器在寄存器保存区都有一个固定的偏移量。

          【讨论】:

            【解决方案6】:

            C h\s 访问这些参数的标准机制。宏定义在stdarg.h

            http://www.cse.unt.edu/~donr/courses/4410/NOTES/stdarg/

            这里有一个非常简单的 sniprintf 实现

            int ts_formatstring(char *buf, size_t maxlen, const char *fmt, va_list va)
            {
                char *start_buf = buf;
            
                maxlen--;
                while(*fmt && maxlen)
                {
                    /* Character needs formating? */
                    if (*fmt == '%')
                    {
                        switch (*(++fmt))
                        {
                          case 'c':
                            *buf++ = va_arg(va, int);
                            maxlen--;
                            break;
                          case 'd':
                          case 'i':
                            {
                                signed int val = va_arg(va, signed int);
                                if (val < 0)
                                {
                                    val *= -1;
                                    *buf++ = '-';
                                    maxlen--;
                                }
                                maxlen = ts_itoa(&buf, val, 10, maxlen);
                            }
                            break;
                          case 's':
                            {
                                char * arg = va_arg(va, char *);
                                while (*arg && maxlen)
                                {
                                    *buf++ = *arg++;
                                    maxlen--;
                                }
                            }
                            break;
                          case 'u':
                                maxlen = ts_itoa(&buf, va_arg(va, unsigned int), 10, maxlen);
                            break;
                          case 'x':
                          case 'X':
                                maxlen = ts_itoa(&buf, va_arg(va, int), 16, maxlen);
                            break;
                          case '%':
                              *buf++ = '%';
                              maxlen--;
                              break;
                        }
                        fmt++;
                    }
                    /* Else just copy */
                    else
                    {
                        *buf++ = *fmt++;
                        maxlen--;
                    }
                }
                *buf = 0;
            
                return (int)(buf - start_buf);
            }
            
            
            
            int sniprintf(char *buf, size_t maxlen, const char *fmt, ...)
            {
                int length;
                va_list va;
                va_start(va, fmt);
                length = ts_formatstring(buf, maxlen, fmt, va);
                va_end(va);
                return length;
            }
            

            它来自 atollic studio tiny printf。

            这里显示了所有机制(包括将参数列表传递给另一个函数。

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2011-07-24
              • 2017-02-06
              • 2012-07-26
              • 2021-10-21
              • 2011-06-02
              • 1970-01-01
              相关资源
              最近更新 更多