可变长度参数在C中如何工作?

时间:2019-07-09 12:34:41

标签: c

我试图了解可变长参数在C语言中的工作原理。

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

我非常感谢任何形式的帮助。 预先感谢。

7 个答案:

答案 0 :(得分:0)

  

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

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

  

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

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

答案 1 :(得分:0)

大多数实现将参数推入堆栈,使用寄存器在寄存器匮乏的体系结构上无法正常工作,或者如果参数通常比寄存器多,则无法使用

并且被调用函数完全不了解参数,其数量或类型。这就是为什么printf和相关功能使用格式说明符。然后,被调用的函数将根据该格式说明符(使用va_arg“函数”)解释堆栈的下一部分。

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

答案 2 :(得分:0)

传统上,无论其他寄存器传递优化如何,参数总是“总是”压入堆栈,然后va_list基本上只是指向堆栈的指针,以标识va_arg的下一个参数。但是,在新处理器和编译器优化设置上非常喜欢寄存器传递,以至于甚至将varargs也作为寄存器放置。

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

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

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

答案 3 :(得分:0)

使用变量参数列表是'C'语言的标准功能,因此必须在存在C编译器的任何计算机上强制使用。

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

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

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

实际上,在 ABI A 应用程序 B 初始界面,请参见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,其大小不同于本机堆栈字的大小,则必须将指针调整为始终增加堆栈字大小的倍数

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

rx

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

答案 4 :(得分:0)

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

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

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

答案 5 :(得分:-1)

那么您正在考虑汇编堆栈或注册吗?

在简单的情况下,我的意思是函数的参数数量少,参数存储在寄存器A4,B4,C4中,返回值存储在A4中。

但是,如果复杂的函数具有很多参数,则无法列出全部寄存器,因此在那时我们使用堆栈。

如果您对此答案有疑问,请随时问我。

答案 6 :(得分:-1)

C是访问这些参数的标准机制。宏在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工作室的微型printf。

此处显示了所有机制(包括将参数列表传递给另一个函数的情况。