C++ 虚表分析

在C++中,每一个含有虚函数的类都会有一个虚函数表,简称虚表。与之对应的,每一个对象都会有其专属的虚表指针指向这个虚表。

0x00 测试代码

#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;

class A{
public:
int a;
virtual void  print()
{
cout<<"This is class A"<<endl;
}
};


class B : public A{
public:
int b;
virtual void  print()
{
cout<<"This is class B"<<endl;
}
};

int main()

{
A *a=new A;
A *b=new B;
a->print();
b->print();
return 0;
}

0x01 GDB调试

   0x8048791 <main+4>     and    esp, 0xfffffff0
   0x8048794 <main+7>     sub    esp, 0x20
   0x8048797 <main+10>    mov    dword ptr [esp], 8    #参数 8字节
   0x804879e <main+17>    call   0x8048660          #先开辟 8 byte 空间

   0x80487a3 <main+22>    mov    ebx, eax
   0x80487a5 <main+24>    mov    dword ptr [esp], ebx
   0x80487a8 <main+27>    call   A::A() <0x80488aa>    #再调用构造函数

 ► 0x80487ad <main+32>    mov    dword ptr [esp + 0x18], ebx
   0x80487b1 <main+36>    mov    dword ptr [esp], 0xc
   0x80487b8 <main+43>    call   0x8048660

   0x80487bd <main+48>    mov    ebx, eax

执行完 call A::A() 也就是分配好空间,调用完构造函数 我们再看一下返回值eax地址单元的内容:

pwndbg> x/4xw $eax
0x804b008:    0x080489a8    0x00000000    0x00000000    0x00020ff1

这里 0x080489a8 就是a对象的虚表地址(vfptr),0x00000000 是int a变量值(这里构造函数里面未对其赋值,所以为零) ,至于0x00000000 与 0x00020ff1 是top chunk 不用管。

接下来我们再看看 0x080489a8 虚表里面的内容

pwndbg> x/4xw 0x080489a8
0x80489a8 <_ZTV1A+8>:    0x08048852    0x00004231    0x0804a128    0x080489ac

pwndbg> x/5i 0x08048852
   0x8048852 <_ZN1A5printEv>:    push   ebp
   0x8048853 <_ZN1A5printEv+1>:    mov    ebp,esp
   0x8048855 <_ZN1A5printEv+3>:    sub    esp,0x18
   0x8048858 <_ZN1A5printEv+6>:    mov    DWORD PTR [esp+0x4],0x8048970
   0x8048860 <_ZN1A5printEv+14>:    mov    DWORD PTR [esp],0x804a080

很容易看出来了,虚表里第一个地址就是A类里面的print函数

A *a=new A;
a->print();

当执行以上代码的时候实际上是这样一个流程 a - > 0x08048852 - > 0x08048852 来执行print()

===========================================================================================

同理,我们分析一下b对象,我们GDB继续往下跟。

   0x80487b1 <main+36>    mov    dword ptr [esp], 0xc
   0x80487b8 <main+43>    call   0x8048660

   0x80487bd <main+48>    mov    ebx, eax
   0x80487bf <main+50>    mov    dword ptr [esp], ebx
   0x80487c2 <main+53>    call   B::B() <0x80488b8>

 ► 0x80487c7 <main+58>    mov    dword ptr [esp + 0x1c], ebx
   0x80487cb <main+62>    mov    eax, dword ptr [esp + 0x18]
   0x80487cf <main+66>    mov    eax, dword ptr [eax]

箭头指向处 A *b=new B 已经执行完,我们看看回值eax地址单元的内容:

pwndbg> x/4xw $eax
0x804b018:    0x08048998    0x00000000    0x00000000    0x00020fe1

pwndbg> x/4w 0x08048998
0x8048998 <_ZTV1B+8>: 0x0804887e    0x00000000    0x00000000    0x080489c0

pwndbg> x/5i 0x0804887e
   0x804887e <_ZN1B5printEv>:    push   ebp
   0x804887f <_ZN1B5printEv+1>:    mov    ebp,esp
   0x8048881 <_ZN1B5printEv+3>:    sub    esp,0x18
   0x8048884 <_ZN1B5printEv+6>:    mov    DWORD PTR [esp+0x4],0x8048980
   0x804888c <_ZN1B5printEv+14>:    mov    DWORD PTR [esp],0x804a080

同理 b对象的虚表里面有它自己的print函数,地址异于a的,这就是c++多态性的实现。

0x02 总结

这里再总结一下:

如果是调用 A *b = new B; 生成的是子类的对象,在构造时,子类对象的虚指针指向的是子类的虚表,接着由B*到A*的转换并没有改变虚表指针,所以这时候b->print,实际上是p->vfptr->print,它在构造的时候就已经指向了子类的print,所以调用的是子类的虚函数,这就是多态了。

文件下载

Last updated