堆栈变量的 C 返回地址 = NULL?

Atr*_*449 1 c assembly callstack undefined-behavior

在 C 中,当您有一个函数返回指向它的局部(在堆栈上)变量之一的指针时,调用函数会返回 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();
}
Run Code Online (Sandbox Code Playgroud)

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

但是我做不到

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

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

Run Code Online (Sandbox Code Playgroud)

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

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

是编译器在做这个吗?

Pet*_*des 6

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

其他编译器的行为可能有所不同,但任何体面的编译都会警告此错误。另请参阅每个 C 程序员应该了解的关于未定义行为的内容

或者在禁用优化的情况下,您可以使用 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
    }
}
Run Code Online (Sandbox Code Playgroud)

在 Godbolt 编译器资源管理器上编译和运行,使用 gcc10.1 -O3for x86-64:

0x7ffcdbf188e4
0x7ffc
Run Code Online (Sandbox Code Playgroud)

有趣的是,dead store 被int C优化掉了,虽然它仍然有一个地址。它获取了它的地址,但是保存地址的 var 不会转义函数,直到int C在返回该地址的同时超出范围。因此,不可能对该10值进行明确定义的访问,并且编译器进行此优化是有效的。使int C挥发性以及会给我们的价值。

C() 的汇编是:

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
Run Code Online (Sandbox Code Playgroud)

实际运行的版本被内联main并且行为类似。它正在从留在那里的调用堆栈中加载一些垃圾值,可能是地址的上半部分。(x86-64 的 64 位地址只有 48 个有效位。规范范围的低半部分总是有 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;
Run Code Online (Sandbox Code Playgroud)

没有任何保证。幸运的是,当优化被禁用时,这种情况会发生。使用像 一样的正常优化级别-O2,读取未初始化的变量可能只是读取,0好像编译器可以在编译时看到它。绝对不需要它从堆栈加载。

另一个功能会优化掉一个死商店。

GCC 还警告使用未初始化。