spk*_*ten 5 c++ virtual-inheritance visual-c++ clang++ c++14
当使用clang或gcc(在macOS上)编译时,以下代码似乎运行正常,但在使用MS Visual C++ 2017编译时崩溃.在后者上,foo_clone对象似乎已损坏,程序崩溃时出现访问冲突foo_clone->get_identifier().
如果我删除协变返回类型(所有clone-methods返回IDO*),或何时std::enable_shared_from_this删除,或者所有继承都是虚拟的,它确实适用于VC++ .
为什么它适用于clang/gcc而不适用于VC++?
#include <memory>
#include <iostream>
class IDO {
public:
virtual ~IDO() = default;
virtual const char* get_identifier() const = 0;
virtual IDO* clone() const = 0;
};
class DO
: public virtual IDO
, public std::enable_shared_from_this<DO>
{
public:
const char* get_identifier() const override { return "ok"; }
};
class D : public virtual IDO, public DO {
D* clone() const override {
return nullptr;
}
};
class IA : public virtual IDO {};
class Foo : public IA, public D {
public:
Foo* clone() const override {
return new Foo();
}
};
int main(int argc, char* argv[]) {
Foo* foo = new Foo();
Foo* foo_clone = foo->clone();
foo_clone->get_identifier();
}
Run Code Online (Sandbox Code Playgroud)
信息:
foo.exe中0x00007FF60940180B处抛出异常:0xC0000005:访问冲突读取位置0x0000000000000004.
这似乎是VC++的错误编译.当enable_shared_from_this没有红鲱鱼时它会消失; 问题只是掩盖了.
一些背景知识:在C++中解析被覆盖的函数通常是通过vtables实现的.但是,在存在多个虚拟继承和共同变体返回类型的情况下,必须满足一些挑战,以及满足它们的不同方式.
考虑:
Foo* foo = new Foo();
IDO* ido = foo;
D* d = foo;
foo->clone(); // must call Foo::clone() and return a Foo*
ido->clone(); // must call Foo::clone() and return an IDO*
d->clone(); // must call Foo::clone() and return a D*
Run Code Online (Sandbox Code Playgroud)
请记住,无论如何都会Foo::clone()返回Foo*,并且转换Foo*为IDO*或D*不是简单的无操作.在完整Foo对象中,IDO子对象位于偏移32处(假设MSVC++和64位编译),并且D子对象位于偏移8处.从a Foo*到a D*意味着向指针添加8,并且IDO*实际上意味着从中加载信息在Foo*这里完全IDO子对象的位置.
但是,让我们看一下为所有这些类生成的vtable.vtable for IDO有这样的布局:
0: destructor
1: const char* get_identifier() const
2: IDO* clone() const
Run Code Online (Sandbox Code Playgroud)
vtable for D有这样的布局:
0: destructor
1: const char* get_identifier() const
2: IDO* clone() const
3: D* clone() const
Run Code Online (Sandbox Code Playgroud)
插槽2是因为基类IDO具有此功能.插槽3在那里因为这个功能也存在.我们可以省略这个插槽,而是在callites上生成额外的代码,从而转换IDO*为D*?或许,但效率会降低.
Foo看起来像这样的vtable :
0: destructor
1: const char* get_identifier() const
2: IDO* clone() const
3: D* clone() const
4: Foo* clone() const
5: Foo* clone() const
Run Code Online (Sandbox Code Playgroud)
同样,它继承了D自己的插槽并附加了自己的插槽.我实际上不知道为什么有两个新的插槽 - 它可能只是一个次优的算法,因为兼容性原因而坚持.
现在,我们将这些插槽放入这些类型的具体对象中Foo?插槽4和5简单得到Foo::clone().但是该函数返回a Foo*,所以它不适合插槽2和3.对于这些,编译器创建调用主版本并转换结果的存根(称为thunks),即编译器为插槽3创建类似的东西:
D* Foo::clone$D() const {
Foo* real = clone();
return static_cast<D*>(real);
}
Run Code Online (Sandbox Code Playgroud)
现在我们得到错误的编译:由于某种原因,编译器在看到这个调用时:
foo->clone();
Run Code Online (Sandbox Code Playgroud)
呼叫不是插槽4或5,而是插槽3.但插槽3返回一个D*!然后代码继续使用它D*作为一个Foo*,或者换句话说,你得到的行为就像你做的那样:
Foo* wtf = reinterpret_cast<Foo*>(
reinterpret_cast<char*>(foo_clone) + 8);
Run Code Online (Sandbox Code Playgroud)
这显然不会结束.
具体来说,在调用中foo_clone->get_identifier();,编译器希望将其转换Foo* foo_clone为a IDO*(get_identifier要求其this指针为a,IDO*因为它最初是在声明中IDO).正如我之前提到的,IDO对象在任何Foo对象中的确切位置并不固定; 它取决于对象的完整类型(如果完整对象是a Foo,则为32 ,但如果它是派生自的类,则可能是其他类型Foo).因此,要进行转换,编译器必须从对象中加载偏移量.具体来说,它可以加载位于任何Foo对象的偏移0处的"虚拟基指针"(vbptr),其指向包含偏移的"虚基表"(vbtable).
但请记住,我们已经损坏Foo*了已经指向偏移8的真实对象.所以我们访问偏移量8的偏移量0,那是什么?嗯,因为它发生,那里的东西是weak_ptr从enable_shared_from_this对象,它为空.因此我们为vbptr获取null,并尝试取消引用它以使对象崩溃.(虚拟基础的偏移量存储在vbtable中的偏移量4处,这就是为什么你得到的崩溃地址是0x000 ... 004.)
如果删除所有协变恶作剧,则vtable缩小为一个很好的单个条目,clone()并且不会出现错误编译.
但是如果你删除了问题,为什么问题会消失enable_shared_from_this呢?好吧,因为那时偏移量8处的东西不是a里面的一些空指针weak_ptr,而是DO子对象的vbptr .(一般来说,继承图的每个分支都有自己的vbptr.IA有一个Foo股份,并DO拥有一个D股票.)这vbptr包含一个转换所需的信息D*到IDO*.我们Foo*真的是D*伪装的,所以一切恰好都能正常运作.
附录
MSVC++编译器有一个未记录的选项来转储对象布局.下面是它的输出Foo 与在enable_shared_from_this:
class Foo size(40):
+---
0 | +--- (base class IA)
0 | | {vbptr}
| +---
8 | +--- (base class D)
8 | | +--- (base class DO)
8 | | | +--- (base class std::enable_shared_from_this<class DO>)
8 | | | | ?$weak_ptr@VDO@@ _Wptr
| | | +---
24 | | | {vbptr}
| | +---
| +---
+---
+--- (virtual base IDO)
32 | {vfptr}
+---
Foo::$vbtable@IA@:
0 | 0
1 | 32 (Food(IA+0)IDO)
Foo::$vbtable@D@:
0 | -16
1 | 8 (Food(DO+16)IDO)
Foo::$vftable@:
| -32
0 | &Foo::{dtor}
1 | &DO::get_identifier
2 | &IDO* Foo::clone
3 | &D* Foo::clone
4 | &Foo* Foo::clone
5 | &Foo* Foo::clone
Foo::clone this adjustor: 32
Foo::{dtor} this adjustor: 32
Foo::__delDtor this adjustor: 32
Foo::__vecDelDtor this adjustor: 32
vbi: class offset o.vbptr o.vbte fVtorDisp
IDO 32 0 4 0
Run Code Online (Sandbox Code Playgroud)
这里没有:
class Foo size(24):
+---
0 | +--- (base class IA)
0 | | {vbptr}
| +---
8 | +--- (base class D)
8 | | +--- (base class DO)
8 | | | {vbptr}
| | +---
| +---
+---
+--- (virtual base IDO)
16 | {vfptr}
+---
Foo::$vbtable@IA@:
0 | 0
1 | 16 (Food(IA+0)IDO)
Foo::$vbtable@D@:
0 | 0
1 | 8 (Food(DO+0)IDO)
Foo::$vftable@:
| -16
0 | &Foo::{dtor}
1 | &DO::get_identifier
2 | &IDO* Foo::clone
3 | &D* Foo::clone
4 | &Foo* Foo::clone
5 | &Foo* Foo::clone
Foo::clone this adjustor: 16
Foo::{dtor} this adjustor: 16
Foo::__delDtor this adjustor: 16
Foo::__vecDelDtor this adjustor: 16
vbi: class offset o.vbptr o.vbte fVtorDisp
IDO 16 0 4 0
Run Code Online (Sandbox Code Playgroud)
这是一些清理后的反调整clone垫片的拆卸:
mov rcx,qword ptr [this]
call Foo::clone ; the real clone
cmp rax,0 ; null pointer remains null pointer
je fin
add rax,8 ; otherwise, add the offset to the D*
jmp fin
fin: ret
Run Code Online (Sandbox Code Playgroud)
这是对错误调用的一些清理反汇编:
mov rax,qword ptr [foo]
mov rcx,rax
mov rax,qword ptr [rax] ; load vbptr
movsxd rax,dword ptr [rax+4] ; load offset to IDO subobject
add rcx,rax ; add offset to Foo* to get IDO*
mov rax,qword ptr [rcx] ; load vtbl
call qword ptr [rax+24] ; call function at position 3 (D* clone)
Run Code Online (Sandbox Code Playgroud)
这里有一些清理过的崩溃调用的反汇编:
mov rax,qword ptr [foo_clone]
mov rcx,rax
mov rax,qword ptr [rax] ; load vbptr, loads null in the crashing case
movsxd rax,dword ptr [rax+4] ; load offset to IDO subobject, crashes
add rcx,rax ; add offset to Foo* to get IDO*
mov rax,qword ptr [rcx] ; load vtbl
call qword ptr [rax+8] ; call function at position 1 (get_identifier)
Run Code Online (Sandbox Code Playgroud)