g++ 带有悬空引用的不完整警告行为
g++ incomplete warning behaviour with dangling references
我们看到 2 个示例都有悬空引用:
示例 A:
int& getref()
{
int a;
return a;
}
示例 B:
int& getref()
{
int a;
int&b = a;
return b;
}
我们称它们都具有相同的主要功能:
int main()
{
cout << getref() << '\n';
cout << "- reached end" << std::endl;
return 0;
}
在示例 A 中,我在读取悬空引用时收到编译器警告和预期的段错误。
在示例 B 中,我既没有收到警告也没有收到段错误,并且 returns 意外地得到了正确的值。
为什么B没有警告?
目前已在 2 台机器上测试。
- 编译器 7.4.0 Ubuntu
- 编译器 7.5.0 Ubuntu
这不是关于什么是悬空引用的问题,而是关于警告和扩展编译器行为的问题!
这是未定义的行为。是的,程序 理论上可以做任何事情,甚至可以炸毁世界或实际工作。
“这是未定义的行为”不是一个令人满意的答案,因为它只回答了程序能够做什么,而不是为什么编译器在示例 B 中甚至没有检测到这一点。
因此这不是 this question 的副本。
程序在示例 B 中似乎没有可重现的运行时错误,也没有警告,这一事实可能只是巧合。
我冒昧地使用 Compiler Explorer 查看生成的代码
在 g++ 7.5 下,特别是 getref()
在汇编中的作用。
示例 A:
getref():
push rbp
mov rbp, rsp
mov eax, 0
pop rbp
ret
示例 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
B ... returns the correct value of a unexpectedly.
由于程序的行为是未定义的,所以没有任何行为是意外的。
此外,返回的任何值都不“正确”。简直就是垃圾
I am surprised by the compiler detecting the dangling reference whilst only ...
编译器几乎不可能检测到所有通过无效引用的间接寻址。因此,一定存在编译器检测不到的复杂点。您已经在该比喻性“点”的不同侧面找到了两个示例。不清楚为什么这让您感到惊讶。
Maybe anyone here as any insight as to why B is harder to detect than A.
比较复杂。返回的引用不是直接从本地对象初始化的,而是从理论上可以引用非本地对象的另一个引用初始化的。直到分析了那个中间引用的初始化程序,我们才可能发现它确实引用了一个本地对象。
所以,C++的观点被“It's UB”彻底回答了。也许您可能想知道为什么生成的汇编程序表现不同。
mov eax, 0
这仅仅是因为案例 A 生成的程序 returns 内存值为 0,即 null。地址 0 处的内存当然不会映射为您的进程可以访问的内容,因此当程序尝试读取该内存时,操作系统会发出 SEGFAULT 信号。
mov rax, QWORD PTR [rbp-8]
另一方面,B 程序 returns 指向堆栈的指针。由于该地址已映射到进程,因此操作系统没有理由发出信号。
值得一提的是,GCC 确实会检测到错误并在启用优化后为不同的函数生成相同的程序集。
我们看到 2 个示例都有悬空引用: 示例 A:
int& getref()
{
int a;
return a;
}
示例 B:
int& getref()
{
int a;
int&b = a;
return b;
}
我们称它们都具有相同的主要功能:
int main()
{
cout << getref() << '\n';
cout << "- reached end" << std::endl;
return 0;
}
在示例 A 中,我在读取悬空引用时收到编译器警告和预期的段错误。 在示例 B 中,我既没有收到警告也没有收到段错误,并且 returns 意外地得到了正确的值。
为什么B没有警告?
目前已在 2 台机器上测试。
- 编译器 7.4.0 Ubuntu
- 编译器 7.5.0 Ubuntu
这不是关于什么是悬空引用的问题,而是关于警告和扩展编译器行为的问题! 这是未定义的行为。是的,程序 理论上可以做任何事情,甚至可以炸毁世界或实际工作。 “这是未定义的行为”不是一个令人满意的答案,因为它只回答了程序能够做什么,而不是为什么编译器在示例 B 中甚至没有检测到这一点。
因此这不是 this question 的副本。
程序在示例 B 中似乎没有可重现的运行时错误,也没有警告,这一事实可能只是巧合。
我冒昧地使用 Compiler Explorer 查看生成的代码
在 g++ 7.5 下,特别是 getref()
在汇编中的作用。
示例 A:
getref():
push rbp
mov rbp, rsp
mov eax, 0
pop rbp
ret
示例 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
B ... returns the correct value of a unexpectedly.
由于程序的行为是未定义的,所以没有任何行为是意外的。
此外,返回的任何值都不“正确”。简直就是垃圾
I am surprised by the compiler detecting the dangling reference whilst only ...
编译器几乎不可能检测到所有通过无效引用的间接寻址。因此,一定存在编译器检测不到的复杂点。您已经在该比喻性“点”的不同侧面找到了两个示例。不清楚为什么这让您感到惊讶。
Maybe anyone here as any insight as to why B is harder to detect than A.
比较复杂。返回的引用不是直接从本地对象初始化的,而是从理论上可以引用非本地对象的另一个引用初始化的。直到分析了那个中间引用的初始化程序,我们才可能发现它确实引用了一个本地对象。
所以,C++的观点被“It's UB”彻底回答了。也许您可能想知道为什么生成的汇编程序表现不同。
mov eax, 0
这仅仅是因为案例 A 生成的程序 returns 内存值为 0,即 null。地址 0 处的内存当然不会映射为您的进程可以访问的内容,因此当程序尝试读取该内存时,操作系统会发出 SEGFAULT 信号。
mov rax, QWORD PTR [rbp-8]
另一方面,B 程序 returns 指向堆栈的指针。由于该地址已映射到进程,因此操作系统没有理由发出信号。
值得一提的是,GCC 确实会检测到错误并在启用优化后为不同的函数生成相同的程序集。