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
在 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