std :: bind和stack-use-after-scope

时间:2018-02-12 14:58:34

标签: c++ c++11

所以,今天我运行了一些使用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构建它们,它们都产生类似的汇编输出。

2 个答案:

答案 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);预计会在这里正常工作。但是,在后一种情况下,临时bindint的堆栈帧中创建,在构成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尝试并将其转换为左值,并且这是未定义的行为(对象的使用情况)发生后,堆栈发生了。