使用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();
}
消息:
foo.exe中0x00007FF60940180B抛出异常:0xC0000005:访问冲突读取位置0x0000000000000004。
答案 0 :(得分:7)
这似乎是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*
请注意,Foo::clone()
无论如何都会返回Foo*
,从Foo*
到IDO*
或D*
的转换不是简单的无操作。在完整的Foo
对象中,IDO
子对象位于偏移量32处(假设MSVC ++和64位编译),D
子对象位于偏移量8处。来自{{{ 1}}到Foo*
表示向指针添加8,获得D*
实际上意味着从IDO*
加载Foo*
子对象所在的位置信息。
但是,让我们看看为所有这些类生成的vtable。 IDO
的vtable具有以下布局:
IDO
0: destructor
1: const char* get_identifier() const
2: IDO* clone() const
的vtable具有以下布局:
D
插槽2是因为基类0: destructor
1: const char* get_identifier() const
2: IDO* clone() const
3: D* clone() const
具有此功能。插槽3在那里因为这个功能也存在。我们可以省略此广告位,而是在通话中生成额外的代码,以便从IDO
转换为IDO*
吗?或许,但效率会降低。
D*
的vtable看起来像这样:
Foo
同样,它继承了0: destructor
1: const char* get_identifier() const
2: IDO* clone() const
3: D* clone() const
4: Foo* clone() const
5: Foo* clone() const
的布局并附加了自己的插槽。我实际上不知道为什么有两个新的插槽 - 它可能只是一个次优的算法,因为兼容性原因而坚持。
现在,我们将这些插槽放入D
类型的具体对象中?插槽4和5简单得到Foo
。但是该函数返回Foo::clone()
,因此它不适合插槽2和3.对于这些,编译器创建调用主版本并转换结果的存根(称为thunks),即编译器创建插槽3的内容如下:
Foo*
现在我们得到错误编译:出于某种原因,编译器在看到这个调用时:
D* Foo::clone$D() const {
Foo* real = clone();
return static_cast<D*>(real);
}
调用不是插槽4或5,而是插槽3.但插槽3返回foo->clone();
!然后代码继续使用D*
作为D*
,换句话说,您获得的行为与您完成的行为相同:
Foo*
这显然不会很好地结束。
具体来说,在调用Foo* wtf = reinterpret_cast<Foo*>(
reinterpret_cast<char*>(foo_clone) + 8);
中,编译器希望将foo_clone->get_identifier();
强制转换为Foo* foo_clone
(IDO*
需要其get_identifier
指针是this
,因为它最初在IDO*
中声明。正如我之前提到的,任何IDO
对象中IDO
对象的确切位置并不固定;它取决于对象的完整类型(如果完整对象是Foo
则为32,但如果它是从Foo
派生的类,则可能是其他类型)。因此,要进行转换,编译器必须从对象中加载偏移量。具体来说,它可以加载&#34;虚拟基指针&#34; (vbptr)位于任何Foo
对象的偏移0处,该对象指向&#34;虚拟基表&#34; (vbtable),包含偏移量。
但请记住,我们有一个已损坏的Foo
已经指向真实对象的偏移量8。所以我们访问偏移量8的偏移量0,那里有什么?好吧,当它发生时,Foo*
对象中的weak_ptr
是什么,它是空的。因此我们为vbptr获取null,并尝试取消引用它以使对象崩溃。 (虚拟基础的偏移量存储在vbtable中的偏移量4处,这就是为什么你得到的崩溃地址是0x000 ... 004。)
如果删除所有协变恶作剧,则vtable缩小为enable_shared_from_this
的单个条目,并且不会出现错误编译。
但是如果删除clone()
,问题为什么会消失呢?那么,因为那时偏移量8处的东西不是enable_shared_from_this
内的一些空指针,而是weak_ptr
子对象的vbptr。 (一般来说,继承图的每个分支都有自己的vbptr。DO
有一个IA
共享,Foo
有一个DO
共享。)和那个vbptr包含将D
转换为D*
所需的信息。我们的IDO*
实际上是伪装的Foo*
,所以一切恰好都能正常运作。
<强>附录强>
MSVC ++编译器有一个未记录的选项来转储对象布局。以下是D*
的输出 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
这里有一些清理过的反汇编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
垫片的反汇编:
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
这里有一些清理过的崩溃电话的反汇编:
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)