Предотвратить или обнаружить «это» от удаления во время использования - PullRequest
0 голосов
/ 13 июня 2018

Одна ошибка, которую я часто вижу, это очистка контейнера во время его итерации.Я попытался собрать небольшой пример программы, демонстрирующей это.Следует отметить, что это часто может происходить во многих глубоких вызовах функций, поэтому их довольно сложно обнаружить.

Примечание. В этом примере намеренно показан некачественный код.Я пытаюсь найти решение для обнаружения ошибок, вызванных написанием такого кода, без тщательной проверки всей кодовой базы (~ 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;
}

Может кто-нибудь предложить способ гарантировать, что это не можетслучиться?Единственный способ, которым я в настоящее время могу обнаружить такие вещи, - это заменить глобальные новые и delete на mmap / mprotect и обнаружить использование послесвободный доступ к памяти.Это и Valgrind, однако, иногда не в состоянии подобрать его, если вектору не нужно перераспределять (то есть удаляются только некоторые элементы или новый размер еще не является резервным).В идеале я не хочу клонировать большую часть STL для создания версии std :: vector, которая всегда перераспределяет каждую вставку / удаление во время отладки / тестирования.

Один из способов, который почти работает, - это если std :: vector вместо этого содержит std :: weak_ptr , тогда использование .lock () для создания временной ссылки предотвращает ее удаление, пока выполнение находится внутри классовметод.Однако это не может работать с std :: shared_ptr , потому что вам не нужна lock () и то же самое с простыми объектами.Создание контейнера со слабыми указателями только для этого было бы расточительным.

Может кто-нибудь еще придумать, как защитить себя от этого.

Ответы [ 3 ]

0 голосов
/ 14 июня 2018

В итоге я выбрал собственный итератор, который, если владелец 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).Когда я закончу, я могу выполнить поиск / замену sstd :: vector на std :: vector , и не должно быть снижения производительности.

0 голосов
/ 14 июня 2018

В вашем конкретном примере страдание сводится не менее чем к двум недостаткам дизайна:

  1. Ваш вектор является глобальной переменной.Ограничьте область действия всех ваших объектов настолько, насколько это возможно, и подобные проблемы менее вероятны.
  2. Имея в виду принцип единой ответственности , я с трудом могу представить, как можно прийтис классом, который нуждается в , чтобы иметь какой-либо метод, который прямо или косвенно (возможно, через 100 слоев стека вызовов) удаляет объекты, которые могут оказаться this.

Я знаю, что ваш пример искусственный и намеренно плохой, поэтому, пожалуйста, не поймите меня неправильно: я уверен, что в вашем конкретном случае не так очевидно, как соблюдение некоторых основных правил дизайна может помешать вам сделать это.Но, как я уже сказал, я твердо верю, что хороший дизайн уменьшит вероятность появления таких ошибок.И на самом деле, я не могу вспомнить, чтобы когда-либо сталкивался с такой проблемой, но, может быть, я просто недостаточно опытен:)

Однако, если это действительно продолжает оставаться проблемой, несмотря на то, что я придерживался некоторых правил проектирования, тогда яесть идея как его обнаружить:

  1. Создайте член int recursionDepth в своем классе и инициализируйте его с 0
  2. В начале каждого не закрытого метода увеличивайте его.
  3. Используйте RAII, чтобы убедиться, что в конце каждого метода он снова уменьшается
  4. В деструкторе установите его как 0, в противном случае это означает, что деструктор прямо или косвенноВызванный каким-либо методом this.
  5. Вы можете #ifdef все это включить и включить только в отладочной сборке.По сути, это сделало бы это отладочным утверждением, некоторым людям они нравятся:)

Обратите внимание, что это не работает в многопоточной среде.

0 голосов
/ 13 июня 2018

Самый простой способ - запустить свои модульные тесты с подключенным Clang MemorySanitizer . Пусть некоторые Linux-системы с непрерывной интеграцией выполняют это автоматически при каждом продвижении в репо.

MemorySanitizer имеет «Использовать»-after -ничтожение обнаружение "(флаг -fsanitize-memory-use-after-dtor + переменная окружения MSAN_OPTIONS=poison_in_dtor=1), и поэтому он взорвет тест, который выполняет код и который превращает вашу непрерывную интеграцию в красный цвет.

Если у вас нет ни одной единицынет ни тестов, ни непрерывной интеграции, тогда вы также можете просто вручную отладить код с помощью MemorySanitizer, но это сложный путь по сравнению с самым простымПоэтому лучше начать использовать непрерывную интеграцию и писать модульные тесты.

Обратите внимание, что могут быть законные причины чтения и записи памяти после запуска деструктора, но память еще не освобождена.Например std::variant<std::string,double>.Это позволяет нам присвоить его std::string, затем double, и поэтому его реализация может уничтожить string и повторно использовать то же хранилище для double.К сожалению, на данный момент отфильтровывать такие случаи - ручная работа, но инструменты развиваются.

...