Статическое размещение и размещение нового результата в разыменовании нулевого указателя - PullRequest
0 голосов
/ 14 декабря 2018

Я работаю на встроенной платформе, где выделение кучи не рекомендуется.У меня также есть круговые зависимости во время строительства.Учитывая эти ограничения, моя команда разработала класс статического распределителя, который используется для выделения памяти в секции .bss, а затем конструирует объект отложенным способом.

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

Сбои могут быть устраненыпереупорядочение порядка построения классов.К сожалению, я не смог создать минимальное воспроизведение проблемы.Кроме того, проблема усугубляется и становится сложнее управлять, когда задействовано виртуальное наследование.

Мы столкнулись с проблемой нацеливания на компиляторы armclang и visual studio, поэтому похоже, что мы делаем что-то из спецификации C ++.

Код статического распределителя:

template <class UnderlyingType, typename... Args>
class StaticAllocator
{
private:
    typedef std::uint64_t BaseDataType;

    // Define a tuple of the variadic template parameters with the references removed
    using TupleWithRefsRemoved = std::tuple<typename std::remove_reference<Args>::type...>;

    // A function that strips return the ref-less template arguments
    template <typename... T>
    TupleWithRefsRemoved removeRefsFromTupleMembers(std::tuple<T...> const& t)
    {
        return TupleWithRefsRemoved{ t };
    }

public:
    StaticAllocator()
    {
        const auto ptr = reinterpret_cast<UnderlyingType *>(&m_underlyingData);
        assert(ptr != nullptr);
    }

    virtual StaticAllocator* clone() const
    {
        return new StaticAllocator<UnderlyingType, Args...>(*this);
    }

    UnderlyingType *getPtr()
    {
        return reinterpret_cast<UnderlyingType *>(&m_underlyingData);
    }

    const UnderlyingType *getPtr() const
    {
        return reinterpret_cast<const UnderlyingType *>(&m_underlyingData);
    }

    UnderlyingType *operator->()
    {
        return getPtr();
    }

    const UnderlyingType *operator->() const
    {
        return getPtr();
    }

    UnderlyingType &operator*()
    {
        return *getPtr();
    }

    const UnderlyingType &operator*() const
    {
        return *getPtr();
    }

    operator UnderlyingType *()
    {
        return getPtr();
    }

    operator const UnderlyingType *() const
    {
        return getPtr();
    }

    void construct(Args... args)
    {
        _construct(TupleWithRefsRemoved(args...), std::index_sequence_for<Args...>());
    }

    void destroy()
    {
        const auto ptr = getPtr();
        if (ptr != nullptr)
        {
            ptr->~T();
        }
    }

private:
    BaseDataType m_underlyingData[(sizeof(UnderlyingType) + sizeof(BaseDataType) - 1) / sizeof(BaseDataType)];

    // A function that unpacks the tuple of arguments, and constructs them
    template <std::size_t... T>
    void _construct(const std::tuple<Args...>& args, std::index_sequence<T...>)
    {
        new (m_underlyingData) UnderlyingType(std::get<T>(args)...);
    }
};

Простой пример использования:

class InterfaceA
{
    // Interface functions here
}

class InterfaceB
{
    // Interface functions here
}


class ObjectA : public virtual InterfaceA
{
public:
    ObjectA(InterfaceB* intrB) : m_intrB(intrB) {}

private:
    InterfaceB* m_intrB;
};

class ObjectB : public virtual InterfaceB
{
public:
    ObjectB(InterfaceA* intrA) : m_intrA(intrA) {}

private:
    InterfaceA* m_intrA;
}

StaticAllocator<ObjectA, InterfaceB*> objectAStorage;
StaticAllocator<ObjectB, InterfaceA*> objectBStorage;

// Crashes happen in this function, there are many more objects in our real
// system and the order of the objects effects if the crash occurs.
void initialize_objects()
{
    auto objA = objectAStorage.getPtr();
    auto objB = objectBStorage.getPtr();

    objectAStorage.construct(objB);
    objectBStorage.construct(objA);
}

1 Ответ

0 голосов
/ 19 декабря 2018

Этот ответ описывает проблему, возникающую во время выполнения, на примере GCC.Другие компиляторы будут генерировать другой код со схожей проблемой, поскольку в вашем коде заложена проблема отсутствия инициализации.

Без избежания динамического выделения памяти в целях эффективности, без универсального подхода, без шаблонов на каждом этапе.Разложенный, ваш код действительно сводится к:

class InterfaceA {};

class InterfaceB {};

class ObjectA : public virtual InterfaceA {
 public:
  ObjectA(InterfaceB *intrB) : m_intrB(intrB) {}

 private:
  InterfaceB *m_intrB;
};

class ObjectB : public virtual InterfaceB {
 public:
  ObjectB(InterfaceA *intrA) : m_intrA(intrA) {}

 private:
  InterfaceA *m_intrA;
};

#include <new>

void simple_init() {
  void *ObjectA_mem = operator new(sizeof(ObjectA));
  void *ObjectB_mem = operator new(sizeof(ObjectB));
  ObjectA *premature_ObjectA = static_cast<ObjectA *>(ObjectA_mem);  // still not constructed
  ObjectB *premature_ObjectB = static_cast<ObjectB *>(ObjectB_mem);
  InterfaceA *ia = premature_ObjectA;  // derived-to-base conversion
  InterfaceB *ib = premature_ObjectB;
  new (ObjectA_mem) ObjectA(ib);
  new (ObjectB_mem) ObjectB(ia);
}

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

void *ObjectA_mem;
void *ObjectB_mem;
ObjectA *premature_ObjectA;
ObjectB *premature_ObjectB;
InterfaceA *ia;
InterfaceB *ib;

void simple_init() {
  ObjectA_mem = operator new(sizeof(ObjectA));
  ObjectB_mem = operator new(sizeof(ObjectB));
  premature_ObjectA = static_cast<ObjectA *>(ObjectA_mem);  // still not constructed
  premature_ObjectB = static_cast<ObjectB *>(ObjectB_mem);
  ia = premature_ObjectA;  // derived-to-base conversion
  ib = premature_ObjectB;
  new (ObjectA_mem) ObjectA(ib);
  new (ObjectB_mem) ObjectB(ia);
}

Что дает нам очень хорошийкод сборки .Мы можем видеть, что оператор:

  ia = premature_ObjectA;  // derived-to-base conversion

компилируется в:

        movq    premature_ObjectA(%rip), %rax
        testq   %rax, %rax
        je      .L6
        movq    premature_ObjectA(%rip), %rdx
        movq    premature_ObjectA(%rip), %rax
        movq    (%rax), %rax
        subq    $24, %rax
        movq    (%rax), %rax
        addq    %rdx, %rax
        jmp     .L7
.L6:
        movl    $0, %eax
.L7:
        movq    %rax, ia(%rip)

Сначала мы видим, что (неоптимизированный) код проверяет нулевой указатель, эквивалентный

if (premature_ObjectA == 0) 
  ia = 0;
else
  // real stuff

Реальные вещи:

    movq    premature_ObjectA(%rip), %rdx
    movq    premature_ObjectA(%rip), %rax
    movq    (%rax), %rax
    subq    $24, %rax
    movq    (%rax), %rax
    addq    %rdx, %rax
    movq    %rax, ia(%rip)

Таким образом, значение, на которое указывает premature_ObjectA, разыменовывается, интерпретируется как указатель, уменьшается на 24, результирующий указатель используется для чтениязначение, это значение добавляется к исходному указателю premature_ObjectA.Поскольку содержимое premature_ObjectA неинициализировано, это, очевидно, не может работать.

То, что происходит, - то, что компилятор выбирает vptr (указатель vtable), чтобы прочитать запись в -3 "quad" (3 * 8 =24) с уровня 0 (виртуальная таблица, подобная зданию, может иметь отрицательные этажи, это просто означает, что 0-й этаж не является самым нижним этажом):

vtable for ObjectA:
        .quad   0
        .quad   0
        .quad   typeinfo for ObjectA
vtable for ObjectB:
        .quad   0
        .quad   0
        .quad   typeinfo for ObjectB

vtable (каждого из этих объектов) начинается в конце , после «typeinfo для ObjectA», как мы видим в скомпилированном коде для ObjectA::ObjectA(InterfaceB*):

        movl    $vtable for ObjectA+24, %edx
...
        movq    %rdx, (%rax)

Так что во время построения vptr устанавливается на «floor»0 "виртуальной таблицы, которая находится перед первой виртуальной функцией, в конце, если виртуальной функции нет.

На 3 этаже находится начало виртуальной таблицы:

vtable for ObjectA:
        .quad   0

Значение 0 для «InterfaceA со смещением 0 внутри полного ObjectA объекта».

Мелкие детали макета vtable будут зависеть от компилятора, принципы:

  • инициализация скрытого элемента данных vptr (и, возможно,несколько других скрытых элементов) в конструкторе
  • с использованием этих скрытых элементов во время преобразования в InterfaceA базовый класс

остается прежним.

Мое объяснениене дает исправления: мы даже не знаем, какая у вас проблема высокого уровня и почему вы используете эти аргументы конструктора и взаимозависимые классы.

Зная, что представляют эти классы,мы могли бы помочь больше.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...