C++算法库如何检查输出的范围并且在小于输入的范围时不创建段错误?

问题描述

我只是好奇,当您只提供输入范围时,某些 C++ 算法如何检查结果/输出容器的范围? 例如对于下面的代码

#include <iostream>
#include <algorithm>
#include <vector>

int main()
{
  std::vector<int> a = {4,2,1,7};
  std::vector<int> b = {1,2};
  
  std::copy(a.begin(),a.end(),b.begin());
  for(auto val : b)
    std::cout << val << std::endl;
}

输出

4
2

我不明白算法如何知道输出容器 b 的容量是 2。我原以为它假定与输入容器的范围相同,因此会产生某种分段错误

解决方法

copy 算法 doesn't check 迭代器范围,您的程序有 heap-buffer-overflow 个问题。

copy 的实现很简单,只是一个简单的循环,可以查看 libcxx 的实现 here

这是一个普遍的内存问题,感谢编译器中强大的内存清理工具,我们可以快速定位问题。虽然你的程序现在用 SEGSEV 没有崩溃,但是它确实有内存缓冲区溢出,如果这是一个大程序,很可能导致其他代码崩溃(可能在不同的线程,不同的库中),并且很难解决原因,因为崩溃代码只是受害者。

使用-fsanitize=address构建,然后运行程序后得到清晰的报告:

==227==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000038 at pc 0x7f548b0f003d bp 0x7ffd43cf2ea0 sp 0x7ffd43cf2648                                                                            WRITE of size 16 at 0x602000000038 thread T0                                                                                                                                                                           #0 0x7f548b0f003c in __interceptor_memmove (/lib/x86_64-linux-gnu/libasan.so.5+0xa103c)                                                                                                                            #1 0x55afc64f43b5 in int* std::__copy_move<false,true,std::random_access_iterator_tag>::__copy_m<int>(int const*,int const*,int*) (/tmp/stackoverflow/a.out+0x43b5)                                            #2 0x55afc64f3eeb in int* std::__copy_move_a<false,int*,int*>(int*,int*) (/tmp/stackoverflow/a.out+0x3eeb)                                                                                                #3 0x55afc64f390c in __gnu_cxx::__normal_iterator<int*,std::vector<int,std::allocator<int> > > std::__copy_move_a2<false,__gnu_cxx::__normal_iterator<int*,std::allocator<int> > >,std::allocator<int> > > >(__gnu_cxx::__normal_iterator<int*,std::allocator<int> > >) (/tmp/stackoverflow/a.out+0x390c)                                                                                          #4 0x55afc64f322e in __gnu_cxx::__normal_iterator<int*,std::allocator<int> > > std::copy<__gnu_cxx::__normal_iterator<int*,std::allocator<int> > >) (/tmp/stackoverflow/a.out+0x322e)                                                                                                           #5 0x55afc64f2834 in main (/tmp/stackoverflow/a.out+0x2834)                                                                                                                                                        #6 0x7f548ac880b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)                                                                                                                                   #7 0x55afc64f238d in _start (/tmp/stackoverflow/a.out+0x238d)

0x602000000038 is located 0 bytes to the right of 8-byte region [0x602000000030,0x602000000038)
allocated by thread T0 here:
    #0 0x7f548b15e947 in operator new(unsigned long) (/lib/x86_64-linux-gnu/libasan.so.5+0x10f947)
    #1 0x55afc64f469c in __gnu_cxx::new_allocator<int>::allocate(unsigned long,void const*) (/tmp/stackoverflow/a.out+0x469c)
    #2 0x55afc64f431d in std::allocator_traits<std::allocator<int> >::allocate(std::allocator<int>&,unsigned long) (/tmp/stackoverflow/a.out+0x431d)
    #3 0x55afc64f3d35 in std::_Vector_base<int,std::allocator<int> >::_M_allocate(unsigned long) (/tmp/stackoverflow/a.out+0x3d35)
    #4 0x55afc64f3582 in void std::vector<int,std::allocator<int> >::_M_range_initialize<int const*>(int const*,std::forward_iterator_tag) (/tmp/stackoverflow/a.out+0x3582)
    #5 0x55afc64f2d98 in std::vector<int,std::allocator<int> >::vector(std::initializer_list<int>,std::allocator<int> const&) (/tmp/stackoverflow/a.out+0x2d98)
    #6 0x55afc64f27bf in main (/tmp/stackoverflow/a.out+0x27bf)
    #7 0x7f548ac880b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/lib/x86_64-linux-gnu/libasan.so.5+0xa103c) in __interceptor_memmove
Shadow bytes around the buggy address:
  0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa 00 00 fa fa 00[fa]fa fa fa fa fa fa fa fa
  0x0c047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c047fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):

Memory sanitizer 是一种通用的内存工具。在专注于 STL 迭代器调试时,STL 库确实有一些有用的工具:

对于 Visual Studio 2019,将 #define _ITERATOR_DEBUG_LEVEL 1 添加到您的代码中,或将其添加到项目配置中。运行您的代码时,调用堆栈出现异常:

>   iterator_check.exe!std::_Vector_const_iterator<std::_Vector_val<std::_Simple_types<int>>>::_Verify_offset(const __int64 _Off) Line 113  C++
    iterator_check.exe!std::_Get_unwrapped_n<std::_Vector_iterator<std::_Vector_val<std::_Simple_types<int>>> &,__int64>(std::_Vector_iterator<std::_Vector_val<std::_Simple_types<int>>> & _It,const __int64 _Off) Line 1318  C++
    iterator_check.exe!std::copy<std::_Vector_iterator<std::_Vector_val<std::_Simple_types<int>>>,std::_Vector_iterator<std::_Vector_val<std::_Simple_types<int>>>>(std::_Vector_iterator<std::_Vector_val<std::_Simple_types<int>>> _First,std::_Vector_iterator<std::_Vector_val<std::_Simple_types<int>>> _Last,std::_Vector_iterator<std::_Vector_val<std::_Simple_types<int>>> _Dest) Line 3813  C++
    iterator_check.exe!main() Line 13   C++
    [External Code] 

我们可以看到调用了一个额外的检查函数_Verify_offset,然后错误被捕获。

对于来自 gcc 的 libstdc++,还有 similar debugging mode.: compiler flag -D_GLIBCXX_DEBUG

在 GCC 9 下使用 compiler flag -D_GLIBCXX_DEBUG 构建您的代码并运行它,我们得到一个错误报告:

/usr/include/c++/9/bits/stl_algobase.h:471:
In function:
    _OI std::copy(_II,_II,_OI) [with _II =
    __gnu_debug::_Safe_iterator<__gnu_cxx::__normal_iterator<int*,std::__cxx1998::vector<int,std::__debug::vector<int>,std::random_access_iterator_tag>; _OI =
    __gnu_debug::_Safe_iterator<__gnu_cxx::__normal_iterator<int*,std::random_access_iterator_tag>]

Error: attempt to subscript a dereferenceable (start-of-sequence) iterator 4
step from its current position,which falls outside its dereferenceable
range.

Objects involved in the operation:
    iterator "__result" @ 0x0x7fff292a8dd0 {
      type = __gnu_cxx::__normal_iterator<int*,std::allocator<int> > > (mutable iterator);
      state = dereferenceable (start-of-sequence);
      references sequence with type 'std::__debug::vector<int,std::allocator<int> >' @ 0x0x7fff292a8e70
    }
Aborted

这也很有帮助!

对于来自 LLVM 的 libcxx,还有 something similar。不过文档有点不全,可以试一下。

请注意所有工具都有性能下降,因此它仅在测试构建中有用,不适合生产构建。