gpt4 book ai didi

c++ - g++ 带有悬空引用的不完整警告行为

转载 作者:行者123 更新时间:2023-12-03 06:53:51 24 4
gpt4 key购买 nike

我们看到两个都有悬挂引用的例子:示例 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 中,我既没有收到警告也没有收到段错误,而是意外地返回了正确的 a 值。

为什么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 生成的程序返回内存值 0,即 null。地址 0 处的内存当然不会映射为您的进程可以访问的内容,因此当程序尝试读取该内存时,操作系统会发出 SEGFAULT 信号。

 mov     rax, QWORD PTR [rbp-8]

另一方面,B 程序返回一个指向堆栈的指针。由于该地址已映射到进程,因此操作系统没有理由发出信号。


就其值(value)而言,GCC 确实会检测到错误并在启用优化后为不同的函数生成相同的程序集。

关于c++ - g++ 带有悬空引用的不完整警告行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64849568/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com