C++ g++;带有悬空引用的不完整警告行为
我们看到两个例子都有悬垂的参考: 示例A:C++ g++;带有悬空引用的不完整警告行为,c++,compiler-construction,g++,warnings,C++,Compiler Construction,G++,Warnings,我们看到两个例子都有悬垂的参考: 示例A: int& getref() { int a; return a; } getref(): push rbp mov rbp, rsp mov eax, 0 pop rbp ret 例B: int& getref() { int a; int&b = a; re
int& getref()
{
int a;
return a;
}
getref():
push rbp
mov rbp, rsp
mov eax, 0
pop rbp
ret
例B:
int& getref()
{
int a;
int&b = a;
return b;
}
getref():
push rbp
mov rbp, rsp
lea rax, [rbp-12]
mov QWORD PTR [rbp-8], rax
mov rax, QWORD PTR [rbp-8]
pop rbp
ret
我们使用相同的主要功能将它们称为:
int main()
{
cout << getref() << '\n';
cout << "- reached end" << std::endl;
return 0;
}
例B:
int& getref()
{
int a;
int&b = a;
return b;
}
getref():
push rbp
mov rbp, rsp
lea rax, [rbp-12]
mov QWORD PTR [rbp-8], rax
mov rax, QWORD PTR [rbp-8]
pop rbp
ret
现在我的程序集有点生锈了,但在示例B中,似乎涉及到更多的堆栈内存,从理论上讲,这将产生更多的内存被危险引用的可能性,因此更易于检测,因为它不太可能受到优化。令我惊讶的是,编译器在只处理寄存器时检测到了悬空引用,而在涉及实际内存时却没有检测到,就像在示例B的汇编中一样
也许这里的任何人都能理解为什么B比A更难被发现
下面是示例B的完整组件,以防感兴趣:
getref():
push rbp
mov rbp, rsp
lea rax, [rbp-12]
mov QWORD PTR [rbp-8], rax
mov rax, QWORD PTR [rbp-8]
pop rbp
ret
.LC0:
.string "- reached end"
main:
push rbp
mov rbp, rsp
call getref()
mov eax, DWORD PTR [rax]
mov esi, eax
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov esi, 10
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char)
mov esi, OFFSET FLAT:.LC0
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
mov esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
mov eax, 0
pop rbp
ret
__static_initialization_and_destruction_0(int, int):
push rbp
mov rbp, rsp
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
cmp DWORD PTR [rbp-4], 1
jne .L7
cmp DWORD PTR [rbp-8], 65535
jne .L7
mov edi, OFFSET FLAT:_ZStL8__ioinit
call std::ios_base::Init::Init() [complete object constructor]
mov edx, OFFSET FLAT:__dso_handle
mov esi, OFFSET FLAT:_ZStL8__ioinit
mov edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
call __cxa_atexit
.L7:
nop
leave
ret
_GLOBAL__sub_I_getref():
push rbp
mov rbp, rsp
mov esi, 65535
mov edi, 1
call __static_initialization_and_destruction_0(int, int)
pop rbp
ret
getref():
推动rbp
mov rbp,rsp
lea rax,[rbp-12]
mov QWORD PTR[rbp-8],rax
mov rax,QWORD PTR[rbp-8]
流行限制性商业惯例
ret
.LC0:
.string“-已到达末尾”
主要内容:
推动rbp
mov rbp,rsp
调用getref()
mov eax,DWORD PTR[rax]
电影esi,eax
移动edi,偏移平面:_ZSt4cout
调用std::basic_ostream::operator
B。。。返回值的正确值
由于程序的行为是未定义的,因此任何行为都不应是意外的
此外,返回的任何值都不“正确”。这简直是垃圾
我很惊讶编译器检测到了悬空引用,而只有
编译器几乎不可能通过无效引用检测所有间接寻址。因此,一定存在编译器无法检测到的复杂程度。你在这个比喻性的“点”的不同侧面找到了两个例子。不清楚为什么你会感到惊讶
也许这里的任何人都能理解为什么B比A更难被发现
它更复杂。返回的引用不是直接从本地对象初始化的,而是另一个在理论上可能引用非本地对象的引用。直到分析了中间引用的初始值设定项,我们才发现它确实引用了一个局部对象
< C++的透视图完全被“UB”所回答。也许您可能想知道为什么生成的汇编程序的行为不同
这仅仅是因为从案例A生成的程序返回一个内存值0,即null。地址0处的内存当然没有映射为进程可以访问的内容,因此当程序试图读取该内存时,操作系统会发出SEGFAULT信号
另一方面,B程序返回指向堆栈的指针。由于该地址已映射到进程,因此操作系统没有理由发出信号
值得一提的是,当启用优化时,GCC确实检测到错误,并为不同的功能生成相同的程序集。“…读取悬挂参考时出现预期的SEGFULT…”您为什么会期望这样?这基本上是一个QOI问题。请注意,clang是这样做的。标准中到处都是“无需诊断”的实例。如果不是必需的,那么您是否能得到诊断就不可能了。@Meph说得很清楚:我不认为GCC在调试生成时没有提供与优化后相同的警告是愚蠢的。这并没有改变GCC的运作方式这一事实。据我记忆中的阅读,这可能是由于GCC的一些优化发生在语义/代码分析之前——因此我猜测在-O2
处,引用被折叠,并且分析部分捕捉到这是一个本地引用。我对不比较未优化的程序集的评论与这一点没有直接关系。未优化的代码生成几乎总是对您编写的代码的1-1转录。因此,比较两段不同源代码的未优化汇编通常意义不大。使用函数本地引用对象的示例会生成不同的程序集,因为在未优化时,GCC会像分配指针一样在堆栈上为引用分配存储空间。这通常不是一个有用的比较,因此不值得尝试从中得出结论。这就是我所说的“不清楚为什么你会感到惊讶。”并不是说OP已经尝试过了,而是gcc在clang和msvc发出警告时没有发出警告,这至少有点令人惊讶。@cigien我想期望是非常主观的。我已经看到了足够多的例子,其中一个编译器警告,另一个编译器没有警告,它一点也不让我吃惊。也许遇到这个例子会让程序员期待它。是的,这就是我的观点。我同意有一点经验并不奇怪,但措辞表明OP应该已经知道这一点。比如“我知道这可能会令人惊讶,但过一段时间你就会习惯于UB诊断的不同QOI”怎么样?Meph接受答案并不能解决问题。如果您更喜欢稍后的答案,您可以自由更改您接受的答案。@cigien“当clang和msvc发出警告时,gcc不会发出警告,这一事实至少有点令人惊讶”它确实发出了警告,但您必须做到这一点。IIRC这与GCC在进行代码分析之前执行优化的方式有关,因此可能在-O2
之后引用会崩溃,从而允许检测到这种情况。