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 以及为什么如果我尝试访问该内存地址会出现分段错误,这些位不应该仍然存在吗?
是编译器在做这个吗?
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 还警告使用未初始化。