假定以下C++源文件:

#include <stdio.h>

class BaseTest {
  public:
  int a;

  BaseTest(): a(2){}

  virtual int gB() {
    return a;
  };
};

class SubTest: public BaseTest {
  public:
  int b;

  SubTest(): b(4){}
};

class TriTest: public BaseTest {
  public:
  int c;
  TriTest(): c(42){}
};

class EvilTest: public SubTest, public TriTest {
  public:
  virtual int gB(){
    return b;
  }
};

int main(){
  EvilTest * t2 = new EvilTest;

  TriTest * t3 = t2;

  printf("%d\n",t3->gB());
  printf("%d\n",t2->gB());
  return 0;
}
-fdump-class-hierarchy给我:
[...]
Vtable for EvilTest
EvilTest::_ZTV8EvilTest: 6u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI8EvilTest)
16    (int (*)(...))EvilTest::gB
24    (int (*)(...))-16
32    (int (*)(...))(& _ZTI8EvilTest)
40    (int (*)(...))EvilTest::_ZThn16_N8EvilTest2gBEv

Class EvilTest
   size=32 align=8
   base size=32 base align=8
EvilTest (0x0x7f1ba98a8150) 0
    vptr=((& EvilTest::_ZTV8EvilTest) + 16u)
  SubTest (0x0x7f1ba96df478) 0
      primary-for EvilTest (0x0x7f1ba98a8150)
    BaseTest (0x0x7f1ba982ba80) 0
        primary-for SubTest (0x0x7f1ba96df478)
  TriTest (0x0x7f1ba96df4e0) 16
      vptr=((& EvilTest::_ZTV8EvilTest) + 40u)
    BaseTest (0x0x7f1ba982bae0) 16
        primary-for TriTest (0x0x7f1ba96df4e0)

拆卸显示:
34  int main(){
   0x000000000040076d <+0>: push   rbp
   0x000000000040076e <+1>: mov    rbp,rsp
   0x0000000000400771 <+4>: push   rbx
   0x0000000000400772 <+5>: sub    rsp,0x18

35    EvilTest * t2 = new EvilTest;
   0x0000000000400776 <+9>: mov    edi,0x20
   0x000000000040077b <+14>:    call   0x400670 <_Znwm@plt>
   0x0000000000400780 <+19>:    mov    rbx,rax
   0x0000000000400783 <+22>:    mov    rdi,rbx
   0x0000000000400786 <+25>:    call   0x4008a8 <EvilTest::EvilTest()>
   0x000000000040078b <+30>:    mov    QWORD PTR [rbp-0x18],rbx

36
37    TriTest * t3 = t2;
   0x000000000040078f <+34>:    cmp    QWORD PTR [rbp-0x18],0x0
   0x0000000000400794 <+39>:    je     0x4007a0 <main()+51>
   0x0000000000400796 <+41>:    mov    rax,QWORD PTR [rbp-0x18]
   0x000000000040079a <+45>:    add    rax,0x10
   0x000000000040079e <+49>:    jmp    0x4007a5 <main()+56>
   0x00000000004007a0 <+51>:    mov    eax,0x0
   0x00000000004007a5 <+56>:    mov    QWORD PTR [rbp-0x20],rax

38
39    printf("%d\n",t3->gB());
   0x00000000004007a9 <+60>:    mov    rax,QWORD PTR [rbp-0x20]
   0x00000000004007ad <+64>:    mov    rax,QWORD PTR [rax]
   0x00000000004007b0 <+67>:    mov    rax,QWORD PTR [rax]
   0x00000000004007b3 <+70>:    mov    rdx,QWORD PTR [rbp-0x20]
   0x00000000004007b7 <+74>:    mov    rdi,rdx
   0x00000000004007ba <+77>:    call   rax
   0x00000000004007bc <+79>:    mov    esi,eax
   0x00000000004007be <+81>:    mov    edi,0x400984
   0x00000000004007c3 <+86>:    mov    eax,0x0
   0x00000000004007c8 <+91>:    call   0x400640 <printf@plt>

40    printf("%d\n",t2->gB());
   0x00000000004007cd <+96>:    mov    rax,QWORD PTR [rbp-0x18]
   0x00000000004007d1 <+100>:   mov    rax,QWORD PTR [rax]
   0x00000000004007d4 <+103>:   mov    rax,QWORD PTR [rax]
   0x00000000004007d7 <+106>:   mov    rdx,QWORD PTR [rbp-0x18]
   0x00000000004007db <+110>:   mov    rdi,rdx
   0x00000000004007de <+113>:   call   rax
   0x00000000004007e0 <+115>:   mov    esi,eax
   0x00000000004007e2 <+117>:   mov    edi,0x400984
   0x00000000004007e7 <+122>:   mov    eax,0x0
   0x00000000004007ec <+127>:   call   0x400640 <printf@plt>

41    return 0;
   0x00000000004007f1 <+132>:   mov    eax,0x0

42  }
   0x00000000004007f6 <+137>:   add    rsp,0x18
   0x00000000004007fa <+141>:   pop    rbx
   0x00000000004007fb <+142>:   pop    rbp
   0x00000000004007fc <+143>:   ret

现在您已经有适当的时间从第一个代码块中的致命钻石(实际问题)中恢复。

调用t3->gB()时,我看到以下问题(t3TriTest类型,gB()是虚拟方法EvilTest::gB()):
   0x00000000004007a9 <+60>:    mov    rax,QWORD PTR [rbp-0x20]
   0x00000000004007ad <+64>:    mov    rax,QWORD PTR [rax]
   0x00000000004007b0 <+67>:    mov    rax,QWORD PTR [rax]
   0x00000000004007b3 <+70>:    mov    rdx,QWORD PTR [rbp-0x20]
   0x00000000004007b7 <+74>:    mov    rdi,rdx
   0x00000000004007ba <+77>:    call   rax

第一个 Action 将vtable移到rax,然后下一个取消引用(现在我们在vtable中)

之后的那个取消引用以获取指向该函数的指针,并在其底部粘贴call ed。

到目前为止,一切都很好,但这带来了一些问题。

哪里是this
我假设this通过+70和+74处的rdi s加载到mov中,但这与vtable相同,这意味着它是指向TriTest类的指针,而该类根本就没有SubTest s b成员。 linux thiscall约定是否在被调用方法内部而不是外部处理虚拟转换?

This was answered by rodrigo here

如何分解虚拟方法?
如果我知道这一点,我可以自己回答前面的问题。 disas EvilTest::gB给我:
Cannot reference virtual member function "gB"

call之前设置一个断点,运行info reg raxdisas唱给我:
(gdb) info reg rax
rax            0x4008a1 4196513
(gdb) disas 0x4008a14196513
No function contains specified address.
(gdb) disas *0x4008a14196513
Cannot access memory at address 0x4008a14196513

为什么vtable(显然)彼此之间只有8个字节? fdump表示第一个和第二个&vtable之间有16个字节(适合64位指针和2个整数),但是第二个gB()调用的缺点是:
   0x00000000004007cd <+96>:    mov    rax,QWORD PTR [rbp-0x18]
   0x00000000004007d1 <+100>:   mov    rax,QWORD PTR [rax]
   0x00000000004007d4 <+103>:   mov    rax,QWORD PTR [rax]
   0x00000000004007d7 <+106>:   mov    rdx,QWORD PTR [rbp-0x18]
   0x00000000004007db <+110>:   mov    rdi,rdx
   0x00000000004007de <+113>:   call   rax
[rbp-0x18]与上一个调用([rbp-0x20])仅相距8个字节。这是怎么回事?

Answered by 500 in the comments

我忘记了对象是堆分配的,只有它们的指针在堆栈上

最佳答案

免责声明:我不是GCC内部专家,但我将尽力解释我的想法。还要注意,您不是在使用虚拟继承,而是普通的多重继承,因此EvilTest对象实际上包含两个BaseTest子对象。您可以通过尝试在this->a中使用EvilTest来看到这种情况:您将得到一个模棱两可的引用错误。

首先要知道,每个VTable在负偏移量中都有2个值:

  • -2:this偏移量(稍后会详细介绍)。
  • -1:指向此类的运行时类型信息的指针。

  • 然后,从0开始,将有指向虚拟函数的指针:

    考虑到这一点,我将使用易于读取的名称编写类的VTable:

    用于BaseTest的VTable:
    [-2]: 0
    [-1]: typeof(BaseTest)
    [ 0]: BaseTest::gB
    

    用于子测试的VTable:
    [-2]: 0
    [-1]: typeof(SubTest)
    [ 0]: BaseTest::gB
    

    TriTest的VTable
    [-2]: 0
    [-1]: typeof(TriTest)
    [ 0]: BaseTest::gB
    

    到目前为止,没有什么太有趣了。

    用于EvilTest的VTable
    [-2]: 0
    [-1]: typeof(EvilTest)
    [ 0]: EvilTest::gB
    [ 1]: -16
    [ 2]: typeof(EvilTest)
    [ 3]: EvilTest::thunk_gB
    

    现在,这很有趣!看起来很容易工作:
    EvilTest * t2 = new EvilTest;
    t2->gB();
    

    此代码在VTable[0]处调用函数,即EvilTest::gB,一切正常。

    但是然后您这样做:
    TriTest * t3 = t2;
    

    由于TriTest不是EvilTest的第一基类,因此t3的实际二进制值不同于t2的二进制值。即,强制类型转换使指针前进N个字节。确切的数量由编译器在编译时知道,因为它仅取决于表达式的静态类型。在您的代码中,它是16个字节。请注意,如果指针是NULL,那么它一定不能前进,因此是反汇编程序中的分支。

    在这一点上,有趣的是看到EvilTest对象的内存布局:
    [ 0]: pointer to VTable of EvilTest-as-BaseTest
    [ 1]: BaseTest::a
    [ 2]: SubTest::b
    [ 3]: pointer to VTable of EvilTest-as-TriTest
    [ 4]: BaseTest::a
    [ 5]: TriTest::c
    

    如您所见,将EvilTest*转换为TriTest*时,必须将this推进到元素[3],即在64位系统中为8 + 4 + 4 = 16字节。
    t3->gB();
    

    现在,您可以使用该指针来调用gB()。和以前一样,使用VTable的[0]元素完成此操作。但是由于该函数实际上来自EvilTest,因此必须先将this指针移回16个字节,然后才能调用EvilTest::gB()。那是EvilTest::thunk_gB()的工作,这是一个读取VTable[-1]值并将该值减为this的小函数。现在一切都匹配了!

    值得注意的是,EvilTest的完整VTable是EvilTest-as-BaseTest的VTable加上EvilTest-as-TriTest的VTable的串联。

    07-24 09:44
    查看更多