【问题标题】:How does va_arg actually work in variadic functions?va_arg 如何在可变参数函数中实际工作?
【发布时间】:2021-07-25 01:22:48
【问题描述】:

我阅读了可变参数函数以及数据类型和宏的工作原理,我以为我理解了它的工作原理,但我发现有些不同,这是我一直在尝试的,我对不同宏和数据的理解输入工作:

typedef char * va_list;
#define va_start( ap, v ) ap = (char *)&v + sizeof( v )
#define va_arg( ap, t ) ( (t *) ( ap += sizeof( t ) )[-1]
#define va_end( ap ) ap = NULL

所以这只是一个指针,它一次移动 1 个字节并移动 t 数据类型使用的字节数,所以我尝试在不使用此宏或 <stdarg.h> 的情况下复制行为,只是为了更好地理解它是如何工作的,使用标头的函数的代码是这样的:

int _print_ints(int n, ...)
{
    va_list listArgs;

    va_start( listArgs, n );

    for(int i = 0; i < n; i++)
    {
        printf("%3d ", va_arg( listArgs, int ) );
    }

    va_end( listArgs );
}

它按预期工作,我制作的代码是这样的:

int print_ints(int n, ...)
{
    char *arg = (char *)&n;

    for(int i = 0; i < n; i++)
    {
        int val = *( (int *) (arg += sizeof( int ) * 2 ) );
        printf("%3d ", val);
    }

    arg = NULL;
}

这个也可以按预期工作,但是,你看我必须移动两倍于我最初必须移动的假定大小,而不是移动每个变量的 4 bytes(我的系统中的 int 大小),我有移动 8 bytes 这是一个长长的大小,代码中的常量变量是 int 所以我真的不明白为什么在堆栈中为了从一个变量移动到另一个我必须移动两倍的大小,这里是满的代码:

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

int _print_ints(int n, ...)
{
    va_list listArgs;

    va_start( listArgs, n );

    for(int i = 0; i < n; i++)
    {
        printf("%3d ", va_arg( listArgs, int ) );
    }

    va_end( listArgs );
}

int print_ints(int n, ...)
{
    char *arg = (char *)&n;

    for(int i = 0; i < n; i++)
    {
        int val = *( (int *) (arg += sizeof( int ) * 2 ) );
        printf("%3d ", val);
    }

    arg = NULL;
}

int main( void )
{
    _print_ints(5, 1, 2, 3, 4, 5);
    printf("\n");
    print_ints(5, 6, 7, 8, 9, 10);

    return EXIT_SUCCESS;
}

它输出:

1  2  3  4  5
6  7  8  9 10

总结一下,为什么我必须移动sizeof( int ) * 2 而不是sizeof( int )

可能是我得到信息的书已经过时了

【问题讨论】:

  • 当今流行的架构——特别是标准的 x86 和 x86_64 ABI——在堆栈上传递所有参数。某些参数是在寄存器中传递的。因此,像这样的简单、传统、基于指针的 va_arg 实现不起作用并且不再使用。

标签: c variadic


【解决方案1】:

首先,我不确定您是否直接从系统的 stdarg.h 标头中获取了这些宏,或者您只是认为编译器正在使用这些宏,因为它可能很好 不是。使用-E -P 编译您的源代码以仅应用预处理器并查看预处理后_print_ints() 的样子。在现代编译器上,va_* 宏只是编译器内置函数(您将在输出中看到类似 __builtin_va_arg(...) 的内容),因此没有 C 等效项,只有编译器知道它们是如何实现的以及它们所需的代码。

现在,如果您显示的确实是系统上正在使用的宏,那么您对 ​​va_start()va_arg() 的重新实现是完全错误的:

char *arg = (char *)&n;             // wrong
char *arg = (char *)&n + sizeof(n); // correct

int val = *( (int *) (arg += sizeof( int ) * 2 ) ); // wrong
int val = *((int *)(arg += sizeof(int)) - 1);       // correct

但是,由于您正在执行 += sizeof(int) * 2 并且奇怪地得到了正确的结果,这可能意味着您的编译器确实以与您的想法不同的方式实现 va_start()va_arg() ,最有可能将您的 4 字节整数与堆栈上的 8 字节边界对齐(如果您在 64 位系统上可能有意义)。

在任何情况下,由于您使用的是野指针,因此您很可能会因为纯粹的运气而获得正确的结果,因此如果您不小心,您正在做的事情很容易最终成为未定义的行为。

【讨论】:

  • 我从书中得到了这些宏,我认为这是一个通用实现,因为它没有提到编译器的不同之处,或者我没有看到它,所以一定是这样,只是编译器的不同实现,谢谢
  • @AntonioEscorcia 不客气!如果这回答了您的问题,您可以点击左上角的复选标记按钮接受我的回答。
猜你喜欢
  • 1970-01-01
  • 2012-08-20
  • 2015-02-13
  • 1970-01-01
  • 1970-01-01
  • 2015-05-27
  • 1970-01-01
  • 2022-01-05
  • 1970-01-01
相关资源
最近更新 更多