在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专门为null,为什么如果尝试访问该内存地址却遇到分段错误,位是否还不存在?
是编译器这样做吗?
答案 0 :(得分:6)
GCC看到未定义的行为(UB)在编译时可见,并决定仅故意返回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
}
}
针对x86-64使用gcc10.1 -O3
编译并运行on the Godbolt compiler explorer:
0x7ffcdbf188e4
0x7ffc
有趣的是,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
实际运行的版本被内联到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;
没有任何保证。幸运的是,禁用优化后碰巧可以解决问题。使用正常的优化级别,例如-O2
,如果编译器可以在编译时看到未初始化的变量,则读取该变量可能只是读为0
。绝对不需要从堆栈加载它。
另一个功能将优化死区存储。
GCC还警告未使用。
答案 1 :(得分:2)
这是一个未定义的行为(UB),但是许多现代编译器在检测到该行为时会返回对自动存储变量的引用,返回NULL作为预防措施(例如,较新版本的gcc)。