在多重继承中反汇编虚拟方法。 vtable如何工作?

时间:2014-05-07 22:33:01

标签: c++ assembly vtable

假设以下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()后,我会看到以下disas(t3类型为TriTestgB()为虚方法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中,下一个取消引用它(现在我们在vtable中)

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

到目前为止一切顺利,但这带来了一些问题。

哪里有this
我假设this通过rdi在+70和+74加载到mov,但它与vtable的指针相同,这意味着它是指针到一个TriTest类,根本不应该有SubTest个成员。 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

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

3 个答案:

答案 0 :(得分:6)

免责声明:我不是海湾合作委员会内部的专家,但我会尝试解释我的想法。另请注意,您没有使用虚拟继承,而是使用普通多重继承,因此您的EvilTest对象实际上包含两个BaseTest子对象。您可以尝试在this->a中使用EvilTest来查看情况:您将收到模糊的引用错误。

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

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

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

考虑到这一点,我将编写类的VTable,并且易于阅读:

VTable for BaseTest:

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

Subable的VTable:

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

VTT for TriTest

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

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

VTable for EvilTest

[-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],即8 + 4 + 4 = 64位系统中的16个字节。

t3->gB();

现在您使用该指针调用gB()。这是使用VTable的元素[0]完成的,如前所述。但由于该函数实际上来自EvilTest,因此this指针必须在调用EvilTest::gB()之前移回16个字节。这是EvilTest::thunk_gB()的工作,这是一个小函数,它读取VTable[-1]值并将该值减去this。现在一切都匹配了!

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

答案 1 :(得分:1)

第一件事:对象不包含vtable,它包含一个指针到vtable。您所说的第一个mov并未加载vtable,而是加载this。第二个mov将指针加载到vtable,该vtable似乎位于对象中的偏移0

第二件事:使用多重继承,您将获得多个vtable,因为从一种类型转换为另一种类型需要this以使二进制布局与转换类型兼容。在这种情况下,您要将EvilTest*投射到TriTest*。这就是add rax,0x10正在做的事情。

答案 2 :(得分: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):
...
...