变量如何在堆栈上表示?

How are variadic variables represented on the stack?

我试图使用这段代码实现我自己的可变参数函数。相反,我得到了 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() 的)将写入函数堆栈帧中的低地址 ex:0000 0004(因为 0000 0000 将保留给 return address) 然后在第一个 arg?

之后的更高地址中紧跟 arg_a

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

Is it not expected that argument 1 ( of test()) will be written in the function stack frame in a low address ex: 0000 0004 (since 0000 0000 will be reserved for return address) then followed by arg_a in the higher address following first arg?

这是很久以前的工作方式,但基本上所有 ABIs 自 20 世纪 90 年代中期以来定义的全功能处理器(相对于微控制器)都将前几个参数放在而是注册,以使函数调用更快。他们这样做不管被调用者是否可变。您无法使用指针算法访问寄存器,因此您尝试做的事情是完全不可能的。由于此更改,如果您查看任何当前一代编译器提供的 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 会给程序带来 未定义的行为 ,这意味着允许编译器任意“错误编译”它。