成员函数调用在VS2017中崩溃

时间:2019-02-26 17:37:22

标签: c++ visual-studio-2017 member-function-pointers

我一直在研究将类定义移动到其他模块后发生的奇怪崩溃,并得出结论,编译器对如何定义成员函数的指针感到困惑。

我无法包含整个代码,因为它是一个庞大的程序,因此我无法在一个较小的示例中复制它。

编辑:我设法通过一个小例子重现崩溃,因此我正在编辑整个问题,以包括新的代码和程序集。

StatesManager.h:

#pragma once

class StatesManager
{
public:
    bool action();
};

Toolbar.h:

#pragma once

class StatesManager;

class Toolbar
{
public:
    Toolbar( StatesManager* statesManager );

    void action( bool ( StatesManager::*action )( ), bool ( StatesManager::*enabled )( ) const = nullptr );

private:
    StatesManager* statesManager_;
};

Toolbar.cpp:

#include "Toolbar.h"
#include "StatesManager.h"

Toolbar::Toolbar( StatesManager* statesManager ) :
    statesManager_( statesManager )
{
}

void Toolbar::action( bool ( StatesManager::*action )( ), bool ( StatesManager::*enabled )( ) const )
{
    ( statesManager_->*action )( );
}

main.cpp:

#include "StatesManager.h"
#include "toolbar.h"

bool StatesManager::action()
{
    return true;
}

int main()
{
    StatesManager manager;
    Toolbar toolbar( &manager );
    toolbar.action( &StatesManager::action );
    return 0;
}

(从另一个模块)调用此代码时,我得到此程序集:

    ( statesManager_->*action )( );
00007FF743771860  mov         rax,qword ptr [&action]  
00007FF743771867  movsxd      rax,dword ptr [rax+8]  
00007FF74377186B  mov         rcx,qword ptr [this]  
00007FF743771872  add         rax,qword ptr [rcx]  
00007FF743771875  mov         rcx,rax  
00007FF743771878  mov         rax,qword ptr [&action]  
00007FF74377187F  call        qword ptr [rax]  

但是,如果我交换两个包含,或者从函数中删除第二个参数,则会得到完全不同的反汇编:

    ( statesManager_->*action )( );
00007FF68CB01860  mov         rax,qword ptr [this]  
00007FF68CB01867  mov         rcx,qword ptr [rax]  
00007FF68CB0186A  call        qword ptr [action]  

第一个代码在调用指令上崩溃。它尝试读取&action+8处从未初始化过的dword值,并导致call指令崩溃。

我找到了一个related bug from half a year ago,但是应该是在我目前使用15.9.7的15.9版本中已修复。

这是VS2017中的另一个错误,还是我在执行成员函数指针和前向声明意想不到的事情?

1 个答案:

答案 0 :(得分:3)

我几乎确定可以使用选项/vmg解决该问题。

否则,编译器将根据类定义优化成员指针的表示。与没有基类的类相比,具有多个基类的类需要不同的成员指针,而具有虚拟基类的类可能需要更复杂的成员指针。

在没有/vmg的情况下,编译器将根据是否看到IStatesManager的完整定义来生成不同的代码,根据我的名字,我假设它是与虚拟方法的接口。

所有使用此类的模块都必须使用/ vmg选项进行编译,这样才能传递正确的成员指针。

或者,您可以将IStatemanager的标头包含在ControlNode标头中,但是我认为有意使用了前向声明以减少依赖性。

编辑: 编译器still optimizes在知道类定义时会调用方法指针,因此可以排除复杂的虚拟派生情况,正如注释中所述,重要的区别在于方法指针的初始化,这是有保证的。与/vmg保持一致。

为这些功能生成的代码显示了不同之处:

struct VirtMethods
{
  virtual int m();
};

struct VDerived : public virtual VirtMethods
{
  virtual int m() override;
};

int invokeit2(VirtMethods &o, int (VirtMethods::*method)());
int invokeit2(VDerived &o, int (VDerived::*method)());

int test(VirtMethods &o)
{
    return invokeit2(o, &VirtMethods::m);
}

int test(VDerived &o)
{
    return invokeit2(o, &VDerived::m);
}

在没有/vmg的情况下,将生成以下代码,该代码仅在具有虚拟方法的类的寄存器中传递简单的函数指针。另一方面,具有虚拟基类的类在传递给内存的结构中需要更多的数据。

o$ = 8
int test(VirtMethods &) PROC                  ; test, COMDAT
        lea     rdx, OFFSET FLAT:[thunk]:VirtMethods::`vcall'{0,{flat}}' }' ; VirtMethods::`vcall'{0}'
        jmp     int invokeit2(VirtMethods &,int (__cdecl VirtMethods::*)(void)) ; invokeit2
int test(VirtMethods &) ENDP                  ; test

$T1 = 32
$T2 = 32
o$ = 64
int test(VDerived &) PROC               ; test, COMDAT
$LN4:
        sub     rsp, 56                             ; 00000038H
        and     DWORD PTR $T2[rsp+8], 0
        lea     rax, OFFSET FLAT:[thunk]:VDerived::`vcall'{0,{flat}}' }'     ; VDerived::`vcall'{0}'
        mov     QWORD PTR $T2[rsp], rax
        lea     rdx, QWORD PTR $T1[rsp]
        mov     DWORD PTR $T2[rsp+12], 4
        movaps  xmm0, XMMWORD PTR $T2[rsp]
        movdqa  XMMWORD PTR $T1[rsp], xmm0
        call    int invokeit2(VDerived &,int (__cdecl VDerived::*)(void)) ; invokeit2
        add     rsp, 56                             ; 00000038H
        ret     0
int test(VDerived &) ENDP               ; test


[thunk]:VDerived::`vcall'{0,{flat}}' }' PROC                         ; VDerived::`vcall'{0}', COMDAT
        mov     rax, QWORD PTR [rcx]
        jmp     QWORD PTR [rax]
[thunk]:VDerived::`vcall'{0,{flat}}' }' ENDP                         ; VDerived::`vcall'{0}'

[thunk]:VirtMethods::`vcall'{0,{flat}}' }' PROC                            ; VirtMethods::`vcall'{0}', COMDAT
        mov     rax, QWORD PTR [rcx]
        jmp     QWORD PTR [rax]
[thunk]:VirtMethods::`vcall'{0,{flat}}' }' ENDP    

另一方面,与/ vmg相比,简单类的代码看起来完全不同:

$T1 = 32
$T2 = 64
o$ = 112
int test(VirtMethods &) PROC                  ; test, COMDAT
$LN4:
        sub     rsp, 104                      ; 00000068H
        lea     rax, OFFSET FLAT:[thunk]:VirtMethods::`vcall'{0,{flat}}' }' ; VirtMethods::`vcall'{0}'
        mov     QWORD PTR $T1[rsp], rax
        lea     rdx, QWORD PTR $T2[rsp]
        xor     eax, eax
        mov     QWORD PTR $T1[rsp+8], rax
        movups  xmm0, XMMWORD PTR $T1[rsp]
        mov     DWORD PTR $T1[rsp+16], eax
        movsd   xmm1, QWORD PTR $T1[rsp+16]
        movaps  XMMWORD PTR $T2[rsp], xmm0
        movsd   QWORD PTR $T2[rsp+16], xmm1
        call    int invokeit2(VirtMethods &,int (__cdecl VirtMethods::*)(void)) ; invokeit2
        add     rsp, 104                      ; 00000068H
        ret     0
int test(VirtMethods &) ENDP                  ; test