具有协变返回类型的方法在VC++上崩溃

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.

Seb*_*edl 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*
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)