在多重继承中拆解虚拟方法。vtable是如何工作的?

问题描述 投票:6回答:3

假设下面的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() 调用时,我看到了下面的das(t3 是类型 TriTest, 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

第一个mov把vtable移到rax中,下一个dereferences它(现在我们在vtable中)。

后面的那句是贬义的 得到一个指向函数的指针,并在底部粘贴它的 called.

到目前为止还不错,但这带来了几个问题。

哪里是 this? 我想 this 被装入 rdi 经由 mov的+70和+74,但这与vtable的指针是一样的,这意味着它是一个指向一个 TriTest 类,它不应该有 SubTest的b成员。linux的thiscall惯例是在被调用的方法内部处理虚拟铸造,而不是在外部?

这个问题已经由 rodrigo 回答了

如何拆解虚拟方法? 如果我知道这个,我就可以自己回答前面的问题了。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

为什么vtables(显然)之间只有8个字节的距离? 这个 fdump 说在第一和第二之间有16个字节的 &vtable (这符合64位指针和2个ints),但从第二种解体的 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] 离前一个调用只有8个字节的距离([rbp-0x20]). 到底是怎么回事?

由500人在评论中回答

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

c++ assembly vtable
3个回答
8
投票

声明:我不是GCC内部的专家,但我会试着解释我认为是怎么回事。另外请注意,你没有使用虚拟继承,而是普通的多重继承,所以你的 EvilTest 对象实际上包含两个 BaseTest 子对象。你可以通过尝试使用 this->aEvilTest:你会得到一个模棱两可的引用错误。

首先要注意,每个VTable都有2个负偏移值。

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

然后,从 0 上,会有虚拟函数的指针。

考虑到这一点,我将写下这些类的VTable, 用容易阅读的名字。

VTable for BaseTest:

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

VTable for SubTest:

[-2]: 0
[-1]: typeof(SubTest)
[ 0]: BaseTest::gB

VTable for TriTest

[-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(). 这是用 [0] 的函数,和之前一样。但由于该函数实际上是来自 EvilTestజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజజ this 指针必须向后移动16个字节,然后才可使用。EvilTest::gB() 可谓。这是由 EvilTest::thunk_gB(),这是一个读取 VTable[-1] 值,并将该值减去 this. 现在一切都匹配了!

值得注意的是,完整的VTable中的 EvilTest 是EvilTest-as-BaseTest的VTable加上EvilTest-as-TriTest的VTable的连接。


2
投票

首先:这个对象不包含一个vtable,它包含一个 指针 到一个vtable。第一个 mov 你所说的不是加载vtable,而是加载了 this. 第二个 mov 加载指向vtable的指针,该指针位于偏移量为 0 的对象中。

第二件事:在多重继承的情况下,你会得到多个vtables,因为从一个类型到另一个类型的每一次投掷都需要用到 this 的二进制布局,以使其与铸造类型兼容。在这种情况下,您正在铸造 EvilTest*TriTest*. 那是什么 add rax,0x10 在做什么。


0
投票

如何拆解虚拟方法? 如果我知道这个,我就可以自己回答前面的问题了。disas EvilTest::gB 给我。

Cannot reference virtual member function "gB"

我也遇到了同样的问题,我利用断点信息来获取方法的地址来解决这个问题,以便分解它。

(gdb) disassemble cSimpleChannel::deliver(cMessage*, double)
Cannot reference virtual member function "deliver"
(gdb) break cSimpleChannel::deliver
(gdb) info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000003ef50 in cSimpleChannel::deliver(cMessage*, double) at libs/sim/cchannel.cc:345
(gdb)  disassemble 0x000000000003ef50
Dump of assembler code for function cSimpleChannel::deliver(cMessage*, double):
...
...

© www.soinside.com 2019 - 2024. All rights reserved.