首页 > 解决方案 > 可变参数变量如何在堆栈上表示?

问题描述

我试图使用这段代码实现我自己的可变参数函数。相反,我得到了 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呢?是否不期望参数1( of test()) 将写入函数堆栈帧中的低地址 ex: 0000 0004(因为0000 0000将保留用于返回地址)然后arg_a在第一个 arg 之后的较高地址中?

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

标签: cpointersstackvariadic-functions

解决方案


是否不期望参数1(test())将被写入函数堆栈帧中的低地址例如:0000 0004(因为0000 0000将保留用于返回地址)然后是arg_a在较高地址中紧随其后参数?

很久以前它就是这样工作的,但基本上所有用于全脂处理器(与微控制器相反)的ABI,自 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”,它指向。(正式地,它指向一个元素越过一个元素数组的末尾,因为所有非数组对象都被视为一个元素数组的唯一元素,用于指针算术目的。您可以执行计算这个的算术指针,但不能取消引用它。)取消引用arg_b会给程序带来未定义的行为,这意味着允许编译器任意“错误编译”它。


推荐阅读