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

时间:2017-11-28 08:50:04

标签: c++ visual-c++ c++14 clang++ virtual-inheritance

使用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。

1 个答案:

答案 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_cloneIDO*需要其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)