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

C堆栈变量的返回地址=NULL?,c,assembly,undefined-behavior,callstack,C,Assembly,Undefined Behavior,Callstack,在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\

在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();
}
由于堆栈帧内存没有被重置,B在堆栈中覆盖了A的内存记录

但是我做不到

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

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

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

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


是编译器在执行此操作吗?

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

其他编译器可能会有不同的行为,但任何适当的编译都会警告此错误。另见

或者在禁用优化的情况下,可以使用tmp变量对编译器隐藏UB。比如int*p=&C;返回p;因为gcc-O0不会跨语句进行优化。或者在启用优化的情况下,使该指针变量易失性,以便通过它清洗一个值,从而对优化器隐藏指针值的来源

#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
    }
}
有趣的是,INTC的死存储优化了,尽管它仍然有一个地址。它有自己的地址,但是保存地址的var直到int C在返回地址的同时退出作用域后才退出函数。因此,不可能对10值进行定义良好的访问,编译器进行此优化是有效的。让int C也变为volatile将为我们提供值

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个有效位。规范范围的下半部分始终有16个前导零位

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

这一切都不能保证。幸运的是,当优化功能被禁用时,这种情况恰好会出现。对于像-O2这样的正常优化级别,如果编译器在编译时可以看到,那么读取未初始化的变量可能只是读取为0。绝对不需要从堆栈中加载它

另一个功能会优化掉一个死掉的存储


GCC还警告未初始化的用户。

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

其他编译器可能会有不同的行为,但任何适当的编译都会警告此错误。另见

或者在禁用优化的情况下,可以使用tmp变量对编译器隐藏UB。比如int*p=&C;返回p;因为gcc-O0不会跨语句进行优化。或者在启用优化的情况下,使该指针变量易失性,以便通过它清洗一个值,从而对优化器隐藏指针值的来源

#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
    }
}
有趣的是,INTC的死存储优化了,尽管它仍然有一个地址。它有自己的地址,但是保存地址的var直到int C在返回地址的同时退出作用域后才退出函数。因此,不可能对10值进行定义良好的访问,编译器进行此优化是有效的。让int C也变为volatile将为我们提供值

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个有效位。规范范围的下半部分始终有16个前导零位

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

这一切都不能保证。幸运的是,当优化功能被禁用时,这种情况恰好会出现。对于像-O2这样的正常优化级别,如果编译器在编译时可以看到,那么读取未初始化的变量可能只是读取为0。绝对不需要从堆栈中加载它

另一个函数是wo uld优化了一个死掉的商店


GCC还警告未初始化的使用。

这是一种未定义的行为,但许多现代编译器在检测到它时,会返回对自动存储变量return NULL的引用,作为预防措施,例如新版本的GCC

示例如下:

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

示例如下:

这都是未定义的行为。您的程序不符合C语言的规则,因此上述规则中的任何保证都不适用。我的猜测是,编译器帮了您一个忙,使您的程序尽早崩溃。我刚刚在MSVC 2019中尝试了这个场景-得到了一个警告,D的值不是空的。编译器看到您正在返回一个本地地址,并且知道这有多错误。如果通过更微妙的机制返回本地地址,则可能会得到指向无效堆栈位置的实际指针。如果编译器优化处于活动状态,则大多数假设都可能是错误的。例如,任何好的优化器都会优化int A=5的行;离开,因为结果是不使用的。此外,编译器还可以将导致未定义行为的任何代码路径视为不可访问,这意味着它还可以简单地对它们进行优化。我建议你阅读更多信息。这都是未定义的行为。您的程序不符合C语言的规则,因此上述规则中的任何保证都不适用。我的猜测是,编译器帮了您一个忙,使您的程序尽早崩溃。我刚刚在MSVC 2019中尝试了这个场景-得到了一个警告,D的值不是空的。编译器看到您正在返回一个本地地址,并且知道这有多错误。如果通过更微妙的机制返回本地地址,则可能会得到指向无效堆栈位置的实际指针。如果编译器优化处于活动状态,则大多数假设都可能是错误的。例如,任何好的优化器都会优化int A=5的行;离开,因为结果是不使用的。此外,编译器还可以将导致未定义行为的任何代码路径视为不可访问,这意味着它还可以简单地对它们进行优化。我建议您阅读更多信息。如果调用者没有尝试对这样返回的指针执行任何操作,那么返回自动变量的地址不会调用未定义的行为。当控件返回给调用方时,指针的值将是不确定的,但由于指针在控件到达return语句之前不会变得不确定,因此return语句本身不会调用UB,并且如果在不使用返回值的情况下调用函数,调用方中也没有UB。很明显,如果不使用此指针,则没有UB。如果我们分析代码,这个案例就不有趣了。空指针是可以的,除非我们取消对它们的引用,等等。如果调用方没有尝试对这样返回的指针做任何事情,返回自动变量地址的典型指针不会调用未定义的行为。当控件返回给调用方时,指针的值将是不确定的,但由于指针在控件到达return语句之前不会变得不确定,因此return语句本身不会调用UB,并且如果在不使用返回值的情况下调用函数,调用方中也没有UB。很明显,如果不使用此指针,则没有UB。如果我们分析代码,这个案例就不有趣了。空指针是可以的,除非我们取消对它们的引用,等等。典型的吹毛求疵