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,所以调用的是子类的虚函数,这就是多态了。

文件下载