静态分配和放置 new 导致空指针取消引用
Static allocation and placement new result in null pointer dereference
我在一个不鼓励堆分配的嵌入式平台上工作。我在构建过程中也有循环依赖。鉴于这些限制,我的团队设计了一个静态分配器 class,用于在 .bss
部分分配内存,然后以延迟方式构造对象。
我们面临的问题是在延迟构造期间,编译器生成的代码会尝试引用尚未构造的静态分配内存中的数据 - 未构造时数据在我们的平台上为零 - 这会导致空指针取消引用导致系统崩溃。
可以通过重新排序 classes 的构造顺序来解决崩溃问题。不幸的是,我无法创建该问题的最低限度重现。此外,当涉及虚拟继承时,问题会变得更糟且更难管理。
我们遇到了针对 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);
}
这个答案描述了在 运行 时间发生的问题,使用 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);
}
那个gives us a very nice assembly code。我们可以看到语句:
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 , %rax
movq (%rax), %rax
addq %rdx, %rax
jmp .L7
.L6:
movl [=13=], %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 , %rax
movq (%rax), %rax
addq %rdx, %rax
movq %rax, ia(%rip)
所以premature_ObjectA
指向的一个值被解引用,解释为指针,减24,得到的指针用于读取一个值,该值被添加到原始指针premature_ObjectA
.由于 premature_ObjectA
的内容是未初始化的,那显然是行不通的。
发生的事情是编译器正在获取 vptr(vtable 指针)以从 0 级读取位于 -3 "quad" (3*8 = 24) 的条目(像建筑物这样的 vtable 可以有负楼层,就是说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 for ObjectA"之后,我们可以在ObjectA::ObjectA(InterfaceB*)
的编译代码中看到:
movl $vtable for ObjectA+24, %edx
...
movq %rdx, (%rax)
所以在构建时,vptr设置为第一个虚函数之前的vtable的"floor 0",如果没有虚函数,则在最后。
-3 层是 vtable 的开头:
vtable for ObjectA:
.quad 0
值 0 表示“InterfaceA
位于完整 ObjectA
对象内的偏移量 0”。
vtable 布局的细节将取决于编译器,原则:
- 在构造函数中初始化 vptr 隐藏数据成员(可能还有多个其他隐藏成员)
- 在转换为
InterfaceA
基础时使用这些隐藏成员 class
保持不变。
我的解释没有提供修复:我们甚至不知道你有什么样的高级问题,也不知道你为什么使用这些构造函数参数和相互依赖classes.
了解这些 class 代表什么,我们或许可以提供更多帮助。
我在一个不鼓励堆分配的嵌入式平台上工作。我在构建过程中也有循环依赖。鉴于这些限制,我的团队设计了一个静态分配器 class,用于在 .bss
部分分配内存,然后以延迟方式构造对象。
我们面临的问题是在延迟构造期间,编译器生成的代码会尝试引用尚未构造的静态分配内存中的数据 - 未构造时数据在我们的平台上为零 - 这会导致空指针取消引用导致系统崩溃。
可以通过重新排序 classes 的构造顺序来解决崩溃问题。不幸的是,我无法创建该问题的最低限度重现。此外,当涉及虚拟继承时,问题会变得更糟且更难管理。
我们遇到了针对 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);
}
这个答案描述了在 运行 时间发生的问题,使用 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);
}
那个gives us a very nice assembly code。我们可以看到语句:
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 , %rax
movq (%rax), %rax
addq %rdx, %rax
jmp .L7
.L6:
movl [=13=], %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 , %rax
movq (%rax), %rax
addq %rdx, %rax
movq %rax, ia(%rip)
所以premature_ObjectA
指向的一个值被解引用,解释为指针,减24,得到的指针用于读取一个值,该值被添加到原始指针premature_ObjectA
.由于 premature_ObjectA
的内容是未初始化的,那显然是行不通的。
发生的事情是编译器正在获取 vptr(vtable 指针)以从 0 级读取位于 -3 "quad" (3*8 = 24) 的条目(像建筑物这样的 vtable 可以有负楼层,就是说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 for ObjectA"之后,我们可以在ObjectA::ObjectA(InterfaceB*)
的编译代码中看到:
movl $vtable for ObjectA+24, %edx
...
movq %rdx, (%rax)
所以在构建时,vptr设置为第一个虚函数之前的vtable的"floor 0",如果没有虚函数,则在最后。
-3 层是 vtable 的开头:
vtable for ObjectA:
.quad 0
值 0 表示“InterfaceA
位于完整 ObjectA
对象内的偏移量 0”。
vtable 布局的细节将取决于编译器,原则:
- 在构造函数中初始化 vptr 隐藏数据成员(可能还有多个其他隐藏成员)
- 在转换为
InterfaceA
基础时使用这些隐藏成员 class
保持不变。
我的解释没有提供修复:我们甚至不知道你有什么样的高级问题,也不知道你为什么使用这些构造函数参数和相互依赖classes.
了解这些 class 代表什么,我们或许可以提供更多帮助。