【问题标题】:How are variadic variables represented on the stack?可变参数变量如何在堆栈上表示?
【发布时间】:2026-01-15 13:25:02
【问题描述】:

我试图使用此代码实现我自己的可变参数函数。相反,我得到了 UB。

#include <stdio.h>

void test(int a, ...)
{
    char* arg_a = (char*)&a;
    char* arg_b = arg_a + sizeof(int);
    printf("%c", *arg_b);
}

int main(){
    test(1, 'a');
}

那么为什么这个程序不打印字母a?难道不期望参数1test())将被写入函数堆栈帧中的低地址例如:0000 0004(因为0000 0000将保留用于返回地址)然后是arg_a在第一个 arg 之后的更高地址中?

我猜这个结果是因为编译器优化的原因,还是有其他原因?

【问题讨论】:

  • (a) C 实现在寄存器中传递一些参数是很常见的,在这种情况下它们根本不会在堆栈上。 (如果取了参数的地址,编译器会将对应的寄存器存入栈中为其创建地址。问题代码中没有取地址,所以编译器没有这样做。) (b)即使参数在堆栈上传递,C 标准也没有定义尝试使用像这样的指针黑客访问它们的行为。允许编译器优化代码而不考虑此处尝试的内容。
  • 基本上,变量参数机制必须通过&lt;stdarg.h&gt;中声明的设施来访问。无法通过指针黑客来实现它。
  • 另外,栈可以在内存中向下增长。堆栈上的某个地方可能有一个返回地址。还有那些局部变量。 Var args 的传递方式可能比一次一个推入堆栈更复杂......我会坚持使用va_start, va_end, va_arg
  • @KhaledGaber:在test 中,值'a' 作为参数传递。该参数对应于声明中的...test 例程中没有任何内容获取该参数的地址。 (没有什么可以,因为没有可以形成&amp;name表达式的参数名称,因为声明中只有...,并且没有使用用于访问参数的&lt;stdarg.h&gt;功能。)所以参数只是留在寄存器中;它不会被复制到堆栈中。
  • 当我使用 gcc 构建此代码时,我收到以下警告:printf("%c", *arg_b);: warning: '*((void *)&a+4)' is used在此函数 [-Wuninitialized]| 中未初始化,即使它生成,Code::Blocks 反汇编程序也无法生成 void test() 函数的程序集。

标签: c pointers stack variadic-functions


【解决方案1】:

难道不期望参数 1(test() 的)将被写入函数堆栈帧中的低地址,例如:0000 0004(因为 0000 0000 将被保留用于返回地址),然后是更高的 arg_a第一个 arg 后面的地址?

这是很久以前它的工作方式,但基本上所有 ABIs 用于自 1990 年代中期以来定义的全脂处理器(与微控制器相反),将前几个参数放在寄存器中,以使函数调用更快。他们这样做无论是否被调用者是可变参数。您无法使用指针算术访问寄存器,因此您尝试做的事情完全不可能。由于这一变化,如果您查看任何当前一代编译器提供的stdarg.h 的内容,您将看到va_startva_argva_end 是使用编译器内在函数定义的,类似于

#define va_start(ap, last_named_arg) __builtin_va_start(ap, last_named_arg)
// etc

您可能会误以为参数仍在堆栈中,因为大多数 32 位 x86 ABI 是在 1980 年代后期定义的(与 80386 和 80486 同时代)并且它们确实将所有参数放在堆栈上。我记得的唯一例外是Win32“fastcall”。然而,64 位 x86 ABI 是在 2000 年代初(与 AMD K8 同期)定义的,它们将参数放入寄存器中。

您的代码将无法可靠地运行即使您为 32 位 x86(或任何其他将所有参数放入堆栈的旧 ABI)编译它,因为它违反了关于偏移指针的 C 标准。指针arg_b 不指向“内存中a 旁边的任何内容”,它指向nothing。 (正式地,它指向一个元素超过一个元素数组的末尾,因为所有非数组对象都被视为一个元素数组的唯一元素,用于指针算术目的。您可以执行计算这个的算术指针,但不能取消引用它。)取消引用arg_b 会给程序未定义的行为,这意味着允许编译器任意“错误编译”它。

【讨论】:

  • 非常感谢 zwol。但是如果代码是在 32 位操作系统上编译的,那么 c 偏移指针的破坏规则会使程序甚至不能在这些平台上运行
  • @KhaledGaber 请参阅我回答的最后一句话。举例来说,在我的计算机上,clang 10 会为您的函数 test 反编译为 void test(int unused, ...) { putchar('\0') } 的汇编语言。