所以,今天我运行了一些使用Address Sanitizer构建的代码,偶然发现了一个奇怪的堆栈使用后范围错误。 我有这个简化的例子:
#include <functional>
class k
{
public: operator int(){return 5;}
};
const int& n(const int& a)
{
return a;
}
int main()
{
k l;
return std::bind(n, l)();
}
ASAN抱怨最后一个代码行:
==27575==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7ffeab375210 at pc 0x000000400a01 bp 0x7ffeab3750e0 sp 0x7ffeab3750d8
READ of size 4 at 0x7ffeab375210 thread T0
#0 0x400a00 (/root/tstb.exe+0x400a00)
#1 0x7f97ce699730 in __libc_start_main (/lib64/libc.so.6+0x20730)
#2 0x400a99 (/root/tstb.exe+0x400a99)
Address 0x7ffeab375210 is located in stack of thread T0 at offset 288 in frame
#0 0x40080f (/root/tstb.exe+0x40080f)
This frame has 6 object(s):
[32, 33) '<unknown>'
[96, 97) '<unknown>'
[160, 161) '<unknown>'
[224, 225) '<unknown>'
[288, 292) '<unknown>' <== Memory access at offset 288 is inside this variable
[352, 368) '<unknown>'
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-scope (/root/tstb.exe+0x400a00)
Shadow bytes around the buggy address:
0x1000556669f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100055666a00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100055666a10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1
0x100055666a20: f1 f1 f8 f2 f2 f2 f2 f2 f2 f2 f8 f2 f2 f2 f2 f2
0x100055666a30: f2 f2 f8 f2 f2 f2 f2 f2 f2 f2 f8 f2 f2 f2 f2 f2
=>0x100055666a40: f2 f2[f8]f2 f2 f2 f2 f2 f2 f2 00 00 f2 f2 f3 f3
0x100055666a50: f3 f3 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100055666a60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100055666a70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100055666a80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x100055666a90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==27575==ABORTING
如果我理解正确,它表示我们正在访问堆栈变量,因为它已经超出了范围。
看看untrutrumented和unoptimized反汇编,我确实看到它发生在实例化__invoke_impl
内:
Dump of assembler code for function std::__invoke_impl<int const&, int const& (*&)(int const&), k&>(std::__invoke_other, int const& (*&)(int const&), k&):
0x0000000000400847 <+0>: push %rbp
0x0000000000400848 <+1>: mov %rsp,%rbp
0x000000000040084b <+4>: push %rbx
0x000000000040084c <+5>: sub $0x28,%rsp
0x0000000000400850 <+9>: mov %rdi,-0x28(%rbp)
0x0000000000400854 <+13>: mov %rsi,-0x30(%rbp)
0x0000000000400858 <+17>: mov -0x28(%rbp),%rax
0x000000000040085c <+21>: mov %rax,%rdi
0x000000000040085f <+24>: callq 0x4007a2 <std::forward<int const& (*&)(int const&)>(std::remove_reference<int const& (*&)(int const&)>::type&)>
0x0000000000400864 <+29>: mov (%rax),%rbx
0x0000000000400867 <+32>: mov -0x30(%rbp),%rax
0x000000000040086b <+36>: mov %rax,%rdi
0x000000000040086e <+39>: callq 0x4005c4 <std::forward<k&>(std::remove_reference<k&>::type&)>
0x0000000000400873 <+44>: mov %rax,%rdi
0x0000000000400876 <+47>: callq 0x40056a <k::operator int()>
0x000000000040087b <+52>: mov %eax,-0x14(%rbp)
0x000000000040087e <+55>: lea -0x14(%rbp),%rax
0x0000000000400882 <+59>: mov %rax,%rdi
0x0000000000400885 <+62>: callq *%rbx
=> 0x0000000000400887 <+64>: add $0x28,%rsp
0x000000000040088b <+68>: pop %rbx
0x000000000040088c <+69>: pop %rbp
0x000000000040088d <+70>: retq
End of assembler dump.
在调用k::operator int()
之后,它将返回的值放在堆栈上并将其地址传递给n()
,后者立即将其返回,然后从__invoke_impl
本身返回(并且一直到主要的回归)。
所以,它看起来像ASAN就在这里,我们真的有一个堆栈使用后范围访问。
问题是:我的代码出了什么问题?
我尝试用gcc,clang和icc构建它们,它们都产生类似的汇编输出。
答案 0 :(得分:5)
std::bind
本质上生成一个实现函数对象,该对象使用所需的参数调用绑定函数。在您的情况下,此实现函数对象大约相当于
struct Impl
{
const int &operator()() const
{
int tmp = k_;
return n(tmp);
}
private:
k k_;
Impl(/*unspecified*/);
};
由于n
将其参数作为const引用返回,因此Impl
的调用运算符将返回对局部变量的引用,该变量是一个悬空引用,然后从{{1}中读取}}。因此,堆栈在范围错误之后使用。
您的困惑可能源于这样一个事实:main
没有return n(l);
预计会在这里正常工作。但是,在后一种情况下,临时bind
在int
的堆栈帧中创建,在构成main
的参数的完整表达式的持续时间内生效到return
。
换句话说,虽然临时生存直到创建它的完整表达式结束,但对于在该完整表达式中调用的函数内生成的临时数,情况并非如此。这些被认为是不同的完整表达式的一部分,并在评估该表达式时被销毁。
PS:出于这个原因,将签名int
的任何函数(对象)绑定到R(Args...)
会导致在调用时保证返回悬空引用 - 这是IMO应该拒绝的IMO的构造编译时间。
答案 1 :(得分:3)
如果您不了解std::bind
的详细信息,那么这很难。
将参数绑定到std::bind
的可调用对象时,参数的副本是maid(source):
绑定的参数被复制或移动,除非包含在std :: ref或std :: cref中,否则永远不会通过引用传递。
std::bind(n, l)
返回一个未指定类型的可调用对象,其类型k
的成员对象构建为l
的副本。请注意这个可调用对象是一个临时对象( rvalue )我会给它起一个名字: bindtmp 。
调用时,bindtmp()
创建一个临时( inttemp )整数(5),以便将bindtmp::lcopy
应用于bindtmp::ncopy
(这些是构造的成员对象)来自main::l
和::n
)。 ::n
在return语句中返回inttemp
范围内的bindtmp()
的const引用。
这是事情变得棘手的问题(source):
每当引用绑定到临时或其子对象时,临时的生命周期将延长以匹配引用的生命周期,但以下情况除外:
- 一个临时绑定到return语句中函数的返回值不会被扩展:它会在返回表达式的末尾立即销毁。这样的功能总是返回一个悬空参考 - ......
这意味着,inttemp
返回后,临时::n
会被销毁。
从这一点来说,一切都崩溃了。 bindtmp()
返回对其生命周期已结束的对象的引用,main
尝试并将其转换为左值,并且这是未定义的行为(对象的使用情况)发生后,堆栈发生了。