首页
论坛
专栏
课程

[调试逆向] [系统底层] [其他内容] [原创]多重继承?抽象类?C++的内存布局并不复杂

2019-10-16 21:37 696

[调试逆向] [系统底层] [其他内容] [原创]多重继承?抽象类?C++的内存布局并不复杂

2019-10-16 21:37
696

多重继承

先来看看多重继承吧
class cFa {
public:
	cFa() {};
	virtual ~cFa() {  };
};


class cMo{
public:
	cMo() {};
	virtual ~cMo() { };
};

class cChild : public cFa, public cMo {
public:
	cChild() {};
	virtual ~cChild() { };
};


void main()
{
	cChild cCh;
}
先来看构造函数
......
001917DD  pop         ecx  
001917FC  mov         ecx,dword ptr [this]  ;取出this指针
001917FF  call        cFa::cFa (01913B1h)  ;调用cFa构造函数
00191804  mov         dword ptr [ebp-4],0  ;异常计数 可以忽略
0019180B  mov         ecx,dword ptr [this]  ;获取this指针
0019180E  add         ecx,4  ;this指针偏移
00191811  call        cMo::cMo (0191302h) ;调用cMo的构造函数  
00191816  mov         eax,dword ptr [this]  ;获取this指针
00191819  mov         dword ptr [eax],offset cChild::`vftable' (0198B4Ch)  ;设置虚函数表
0019181F  mov         eax,dword ptr [this]  ;获取this指针
00191822  mov         dword ptr [eax+4],offset cChild::`vftable' (0198B58h)  ;设置第二个虚函数表
00191829  mov         dword ptr [ebp-4],0FFFFFFFFh  
00191830  mov         eax,dword ptr [this]  ;返回this指针
......

在上面的代码,我们看出构造函数的调用顺序是根据,从左到右的继承顺序来依次调用父类函数的

在调用cFa的构造函数时,直接传递了this指针,也就是cCh的地址,而在调用cMo的时候,传递的是this指针加4个字节的地址,也就是跳过了cFa所占的空间。


那么也就是说,父类对象在子类的内存布局的顺序和构造函数的调用顺序是一样的,那么现在cCh对象的内存结构如下图:

来看看这两个父类的构造函数:
CFa:
......
001918AD  mov         eax,dword ptr [this]  
001918B0  mov         dword ptr [eax],offset cFa::`vftable' (0198B34h)  
001918B6  mov         eax,dword ptr [this]  
......


cMo:
......
0019190D  mov         eax,dword ptr [this]  
00191910  mov         dword ptr [eax],offset cMo::`vftable' (0198B40h)  
00191916  mov         eax,dword ptr [this]  
......

过滤掉了一些无用的代码,构造函数和非多重继承无任何区别,只是将传进来的对象的虚表换成自己的虚表。


有营养的部分来了:

两个构造函数结束后,出现了两个虚表赋值,为什么是两个虚表?

因为有两个父类,当调用父类函数的时候,需要通过偏移取得父类对象,传递指针,并调用函数。


这两个虚表在本例中意义不大,因为子类没有覆盖父类的虚函数,如果有覆盖的情况出现,这两个虚表中会保存子类覆盖的虚函数,和父类未覆盖的虚函数。那么调用就很简单了,虚表偏移。


析构函数就不上代码了,和构造函数的顺序恰巧相反。

如果在main函数中添加一行如下代码会发生什么?

cMo *pMo = (cMo*)&cCh; 
编译器会找到cCh的地址,根据cCh父类中的cMo对象的偏移位置,获取到cCh父类中的cMo的地址,并返回赋值给pMo。

抽象类

上代码

class CAbstractBase {
public:
	virtual void Show() = 0;
};

class CAbstractChild : public CAbstractBase{
public:
	virtual void Show() { printf("show"); };
};

void main()
{
	CAbstractChild cAb;
	cAb.Show();
}
不说废话,直接进入子类的构造函数
;CAbstractChild 构造函数
......
00A1198F  pop         ecx  
00A11990  mov         dword ptr [this],ecx  
00A11993  mov         ecx,dword ptr [this]  
00A11996  call        CAbstractBase::CAbstractBase (0A11474h)  
00A1199B  mov         eax,dword ptr [this]  ;获取this指针
00A1199E  mov         dword ptr [eax],offset CAbstractChild::`vftable' (0A18B40h)  ;虚函数表初始化
00A119A4  mov         eax,dword ptr [this] ;返回this指针
......

;CAbstractBase 构造函数
......
00A118BF  pop         ecx  
00A118C0  mov         dword ptr [this],ecx  
00A118C3  mov         eax,dword ptr [this]  ;获取this指针
00A118C6  mov         dword ptr [eax],offset CAbstractBase::`vftable' (0A18B34h)  ;虚函数表初始化
00A118CC  mov         eax,dword ptr [this]  ;返回this指针
......

看起来似乎和普通的继承没什么不同?

我们来看看CAbstractBase中的虚函数表(0A18B34h)指向的Show函数吧。




CAbstractBase的Show函数,没有实现代码,所以没有首地址,编译器防止误调用纯虚函数,将虚表中保存的纯虚函数的首地址项替换成了__purecall 函数,这个函数的作用就是结束程序,并返回错误码0x19。


其余与单类继承并无差别。


总结:

这里包括上篇文章的内容总结 《我们聊聊继承吧,从继承的角度出发再来聊聊多态》 。

单类继承:

  • 在类对象占用的内存空间,只保留一份虚表指针,也就只有一个虚表
  • 虚表中各项保存了类中各虚函数的首地址
  • 构造函数先构造父类,在构造自身
  • 析构函数先析构自身,在析构父类

多重继承:

  • 在类对象所占用的内存空间中,根据继承父类的个数保存对应的虚表指针
  • 根据所保存的虚表指针的个数,对应产生相应个数的虚表
  • 转换父类指针时,需要调整到对象的首地址
  • 构造时需要调用多个父类构造函数
  • 构造时先构造继承列表中第一个父类,然后依次调用到最后一个继承的父类构造函数。
  • 析构与构造顺序相反
  • 当对象作为成员时,整个类对象的内存结构和多重继承很相似。当类中无虚函数时,整个类对象内存结构和多重继承完全一样,可按实际情况进行还原,当父类或成员对象存在虚函数,通过观察虚表指针的位置和构造函数、析构函数中填写虚表指针的数量及目标地址,来还原继承或成员关系。





[公告]安全测试和项目外包请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最后于 2019-10-17 18:01 被Hasic编辑 ,原因:
最新回复 (2)
killpy 2 2019-10-17 14:42
2
0
__purecall  f返回错误 啥意思 没看懂
Hasic 2019-10-17 18:01
3
0
killpy __purecall f返回错误 啥意思 没看懂
CAbstractBase的Show函数,没有实现代码,所以没有首地址,编译器防止误调用纯虚函数,将虚表中保存的纯虚函数的首地址项替换成了__purecall 函数,这个函数的作用就是结束程序,并返回错误码0x19。
游客
登录 | 注册 方可回帖
返回