c++ - 反汇编多重继承中的虚拟方法。 vtable 是如何工作的?

标签 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() 被调用时,我看到以下错误(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

第一个 mov 将 vtable 移动到 rax 中,下一个取消引用它(现在我们在 vtable 中)

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

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

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

This was answered by rodrigo here

如何反汇编虚方法?
如果我知道这一点,我可以自己回答前面的问题。 disas EvilTest::gB 给我:

Cannot reference virtual member function "gB"

call 之前设置断点,运行 info reg raxdisassing 给我:

(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 子对象。您可以通过尝试在 EvilTest 中使用 this->a 来了解情况:您将得到一个不明确的引用错误。

首先请注意,每个 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 的串联。

关于c++ - 反汇编多重继承中的虚拟方法。 vtable 是如何工作的?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23529649/

相关文章:

c++ - 如何在 OpenCV 函数中访问多维矩阵的子矩阵?

c++ - 单元测试引用关键部分类

assembly - 如何在汇编宏中以立即值作为参数编写 riscv CSR?

C++ 类型对 `vtable for Producer' 的 undefined reference

c++ - C++中每个类都有虚函数表吗

c++ - Makefile 并不总是重建

c++ - 将成员函数指针转换为字符串

汇编 64 位 NASM

assembly - 在汇编器中编写 x86_64 linux 内核模块

c++ - 多态性如何涉及多重继承?