防止或检测“this”在使用过程中被删除

时间:2018-06-13 13:34:42

标签: c++ memory stl containers unsafe

我经常看到的一个错误是在迭代它时清除容器。我试图整理一个小例子程序来证明这种情况。需要注意的一点是,这通常会发生很多深度函数调用,因此很难检测到。

注意:此示例故意显示一些设计不良的代码。我试图找到一个解决方案,以检测编写这样的代码所导致的错误,而无需仔细检查整个代码库(~500 C ++单位)

#include <iostream>
#include <string>
#include <vector>

class Bomb;

std::vector<Bomb> bombs;

class Bomb
{
  std::string name;

public:
  Bomb(std::string name)
  {
    this->name = name;
  }

  void touch()
  {
    if(rand() % 100 > 30)
    {
      /* Simulate everything being exploded! */
      bombs.clear();

      /* An error: "this" is no longer valid */
      std::cout << "Crickey! The bomb was set off by " << name << std::endl;
    }
  }
};

int main()
{
  bombs.push_back(Bomb("Freddy"));
  bombs.push_back(Bomb("Charlie"));
  bombs.push_back(Bomb("Teddy"));
  bombs.push_back(Bomb("Trudy"));

  for(size_t i = 0; i < bombs.size(); i++)
  {
    bombs.at(i).touch();
  }

  return 0;
}

有人可以建议一种保证这种情况不会发生的方法吗? 我目前能够检测到此类事情的唯一方法是将全局删除替换为 mmap / mprotect 并在空闲内存访问后检测使用。然而,如果向量不需要重新分配(即只删除了一些元素或者新大小还不是保留大小),这和Valgrind有时无法获取它。理想情况下,我不希望克隆大部分STL来创建一个std :: vector版本,它总是在调试/测试期间重新分配每个插入/删除。

一种几乎可行的方法是,如果 std :: vector 包含 std :: weak_ptr ,那么 .lock()的使用创建临时引用可防止在执行时在类方法中删除它。但是,这不适用于 std :: shared_ptr ,因为您不需要 lock(),并且与普通对象相同。为此创建一个弱指针容器将是浪费。

任何人都可以想办法保护自己免受这种伤害。

3 个答案:

答案 0 :(得分:3)

最简单的方法是在链接到Clang MemorySanitizer的情况下运行单元测试。 让一些持续集成的Linux机箱在每次推送时自动完成 回购。

MemorySanitizer具有&#34;破坏后使用检测&#34; (标记-fsanitize-memory-use-after-dtor +环境变量MSAN_OPTIONS=poison_in_dtor=1)因此它会破坏执行代码的测试,并使持续集成变为红色。

如果您既没有单元测试也没有持续集成,那么您也可以使用MemorySanitizer手动调试代码,但这与最简单的方法相比很难。因此,最好开始使用持续集成和编写单元测试。

请注意,在运行析构函数但内存尚未释放后,可能存在内存读取和写入的合理原因。例如std::variant<std::string,double>。它允许我们分配std::string然后double,因此其实现可能会破坏string并为double重用相同的存储空间。不幸的是,过滤此类案例是目前的手工工作,但工具也在不断发展。

答案 1 :(得分:1)

在你的特定例子中,苦难归结为不少于两个设计缺陷:

  1. 您的矢量是一个全局变量。尽可能限制所有对象的范围,这样的问题不太可能发生。
  2. 考虑到单一责任原则,我很难想象如何能够提出一个需要的类来直接或间接地拥有某种方法(也许通过100层调用堆栈)删除可能恰好是this的对象。
  3. 我知道你的例子是人为的,故意不好的,所以请不要误解我的意思:我确信在你的实际案例中,如何坚持一些基本的设计规则会阻止你这样做。但正如我所说,我坚信良好的设计将减少这些错误的可能性。事实上,我不记得我曾经遇到过这样的问题,但也许我只是没有足够的经验:)

    然而,如果这仍然是一个问题,尽管坚持一些设计规则,那么我有这个想法如何检测它:

    1. 在您的班级中创建成员int recursionDepth,并使用0
    2. 对其进行初始化
    3. 在每个非私有方法的开头递增它。
    4. 使用RAII确保在每个方法结束时再次递减
    5. 在析构函数中检查它是0,否则意味着通过this的某种方法直接或间接调用析构函数。
    6. 您可能希望#ifdef所有这些并仅在调试版本中启用它。这基本上使它成为调试断言,有些人喜欢它们:)
    7. 请注意,这在多线程环境中不起作用。

答案 2 :(得分:0)

最后我使用了一个自定义迭代器,如果所有者std :: vector调整大小,而迭代器仍然在范围内,它将记录错误或中止(给我一个程序的堆栈跟踪)。这个例子有点复杂,但我试图尽可能地简化它并从迭代器中删除未使用的功能。

该系统已标记出约50种此类错误。有些可能是重复。然而,Valgrind和ElecricFence在这一点上变得干净,这令人失望(总共他们标记了大约10,我已经修复了自代码清理开始以来。)

在这个例子中,我使用 clear(),Valgrind将其标记为错误。然而在实际的代码库中,它是随机访问擦除(即 vec.erase(vec.begin()+ 9)),我需要检查它,而Valgrind不幸错过了很多。

<强>的main.cpp

#include "sstd_vector.h"

#include <iostream>
#include <string>
#include <memory>

class Bomb;

sstd::vector<std::shared_ptr<Bomb> > bombs;

class Bomb
{
  std::string name;

public:
  Bomb(std::string name)
  {
    this->name = name;
  }

  void touch()
  {
    if(rand() % 100 > 30)
    {
      /* Simulate everything being exploded! */
      bombs.clear(); // Causes an ABORT

      std::cout << "Crickey! The bomb was set off by " << name << std::endl;
    }
  }
};

int main()
{
  bombs.push_back(std::make_shared<Bomb>("Freddy"));
  bombs.push_back(std::make_shared<Bomb>("Charlie"));
  bombs.push_back(std::make_shared<Bomb>("Teddy"));
  bombs.push_back(std::make_shared<Bomb>("Trudy"));

  /* The key part is the lifetime of the iterator. If the vector
   * changes during the lifetime of the iterator, even if it did
   * not reallocate, an error will be logged */
  for(sstd::vector<std::shared_ptr<Bomb> >::iterator it = bombs.begin(); it != bombs.end(); it++)
  {
    it->get()->touch();
  }

  return 0;
}

<强> sstd_vector.h

#include <vector>

#include <stdlib.h>

namespace sstd
{

template <typename T>
class vector
{
  std::vector<T> data;
  size_t refs;

  void check_valid()
  {
    if(refs > 0)
    {
      /* Report an error or abort */
      abort();
    }
  }

public:
  vector() : refs(0) { }

  ~vector()
  {
    check_valid();
  }

  vector& operator=(vector const& other)
  {
    check_valid();
    data = other.data;

    return *this;
  }

  void push_back(T val)
  {
    check_valid();
    data.push_back(val);
  }

  void clear()
  {
    check_valid();
    data.clear();
  }

  class iterator
  {
    friend class vector;

    typename std::vector<T>::iterator it;
    vector<T>* parent;

    iterator() { }
    iterator& operator=(iterator const&) { abort(); }

  public:
    iterator(iterator const& other)
    {
      it = other.it;
      parent = other.parent;
      parent->refs++;
    }

    ~iterator()
    {
      parent->refs--;
    }

    bool operator !=(iterator const& other)
    {
      if(it != other.it) return true;
      if(parent != other.parent) return true;

      return false;
    }

    iterator operator ++(int val)
    {
      iterator rtn = *this;
      it ++;

      return rtn;
    }

    T* operator ->()
    {
      return &(*it);
    }

    T& operator *()
    {
      return *it;
    }
  };

  iterator begin()
  {
    iterator rtn;

    rtn.it = data.begin();
    rtn.parent = this;
    refs++;

    return rtn;
  }

  iterator end()
  {
    iterator rtn;

    rtn.it = data.end();
    rtn.parent = this;
    refs++;

    return rtn;
  }
};

}

这个系统的缺点是我必须使用迭代器而不是 .at(idx) [idx] 。我个人不太介意这个。如果需要随机访问,我仍然可以使用 .begin()+ idx

它有点慢(虽然与Valgrind相比没什么)。当我完成后,我可以用 std :: vector 搜索/替换 sstd :: vector ,并且不应该有性能下降。