C return 堆栈变量的地址 = NULL?

C return address of stack variable = NULL?

在 C 中,当您有一个函数 returns 一个指向它的局部(在堆栈上)变量之一的指针时,调用函数将返回 null。为什么会这样?

我可以在我的硬件上用 C 来做这个

void A() {
    int A = 5;
}

void B() {
    // B will be 5 even when uninitialised due to the B stack frame using
    // the old memory layout of A
    int B;
    printf("%d\n", B);
}

int main() {
    A();
    B();
}

由于栈帧内存没有被重置,B在栈中覆盖了A的内存记录。

可是我做不到

int* C() {
    int C = 10;
    return &C;
}

int main() {
    // D will be null ?
    int* D = C();
}

我知道我不应该做这段代码,它是 UB,在不同的硬件上是不同的,编译器可以优化它来改变这个例子的行为,当我们下次调用这个例子中的另一个函数时它会被破坏无论如何。

但我想知道为什么在使用 GCC 编译时特别是 D 为 null,以及为什么在我尝试访问该内存地址时出现分段错误,这些位不应该仍然存在吗?

是编译器干的吗?

GCC 看到编译时可见的未定义行为 (UB),并决定故意 return NULL。这很好:第一次使用一个值时立即出现嘈杂的故障更容易调试。 返回 NULL 是 GCC5 的一个新特性;正如 @P__J__ 在 Godbolt 上的回答所示,GCC4.9 打印非空堆栈地址。

其他编译器的行为可能不同,但任何正常的编译都会警告此错误。另见 What Every C Programmer Should Know About Undefined Behavior

或者在禁用优化的情况下,您可以使用 tmp 变量从编译器中隐藏 UB。喜欢 int *p = &C; return p; 因为 gcc -O0 不会跨语句优化。 (或者在启用优化的情况下,使该指针变量 volatile 通过它清洗一个值,从优化器中隐藏指针值的来源。)

#include <stdio.h>

int* C() {
    int C = 10;
    int *volatile p = &C;    // volatile pointer to plain int
    return p;                // still UB, but hidden from the compiler
}

int main()
{
    int* D = C();
    printf("%p\n", (void *)D);
    if (D){
        printf("%#x\n", *D);   // in theory should be passing an unsigned int for %x
    }
}

编译和 运行 on the Godbolt compiler explorer,使用 gcc10.1 -O3 for x86-64:

0x7ffcdbf188e4
0x7ffc

有趣的是,int C 的死存储被优化掉了,尽管它仍然有一个地址。它的地址已被占用,但保存地址的 var 不会转义函数,直到 int C 在地址被 returned 的同时超出范围。因此,不可能对 10 值进行明确定义的访问,并且编译器进行此优化是有效的。使 int C 易失性 以及 将为我们提供价值。

C() 的 asm 是:

C:
        lea     rax, [rsp-12]            # address in the red-zone, below RSP
        mov     QWORD PTR [rsp-8], rax   # store to a volatile local var, also in the red zone
        mov     rax, QWORD PTR [rsp-8]   # reload it as return value
        ret

实际运行的版本被内联到 main 并且行为相似。它从留在那儿的调用堆栈加载一些垃圾值,可能是地址的上半部分。 (x86-64 的 64 位地址只有 48 个有效位。规范 运行ge 的低半部分总是有 16 个前导零位)。

但它不是由 main 写入的内存,所以可能是 运行 在 main 之前的某个函数使用的地址。


// B will be 5 even when uninitialised due to the B stack frame using
// the old memory layout of A
int B;

没有什么是 gua运行teed 的。幸运的是,当禁用优化时,这恰好可以解决。对于像 -O2 这样的正常优化级别,如果编译器可以在编译时看到,读取未初始化的变量可能只是读作 0。绝对不需要从堆栈加载它。

另一个函数会优化掉死存储。

GCC 还会对未初始化的使用发出警告。

这是一个未定义的行为 (UB) 但许多现代编译器在检测到它时 return 对自动存储变量的引用 return NULL 作为预防措施(例如较新版本的 gcc) .

这里的例子: https://godbolt.org/z/H-zU4C