刘收获

导航

虚表和虚表指针2.0

  继续从汇编内存层次上对虚表和虚表指针进行分析

  (任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法)

 

0x01 对象调用自身虚函数

 

class CVirtual
{
public:
	virtual int GetNumber()
	{
		return m_nNumber;
		return 0;
	}
	virtual void SetNumber(int nNumber)
	{
		m_nNumber = nNumber;
	}
private:
	int m_nNumber;
};

int main()
{
	//int a = sizeof(CVirtual);
	CVirtual TheVirtual;

	TheVirtual.SetNumber(20);
	printf("%d\n", TheVirtual.GetNumber());
    return 0;
}

  反汇编:

    21: int main()
    22: {
00EC3DA0 55                   push        ebp  
00EC3DA1 8B EC                mov         ebp,esp  
00EC3DA3 81 EC D0 00 00 00    sub         esp,0D0h  
00EC3DA9 53                   push        ebx  
00EC3DAA 56                   push        esi  
00EC3DAB 57                   push        edi  
00EC3DAC 8D BD 30 FF FF FF    lea         edi,[ebp-0D0h]  
00EC3DB2 B9 34 00 00 00       mov         ecx,34h  
00EC3DB7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00EC3DBC F3 AB                rep stos    dword ptr es:[edi]  
    23: 	//int a = sizeof(CVirtual);
    24: 	CVirtual TheVirtual;
00EC3DBE 8D 4D F4             lea         ecx,[TheVirtual]  
00EC3DC1 E8 6E D5 FF FF       call        CVirtual::CVirtual (0EC1334h)  
    25: 
    26: 	TheVirtual.SetNumber(20);
00EC3DC6 6A 14                push        14h  
00EC3DC8 8D 4D F4             lea         ecx,[TheVirtual]  
00EC3DCB E8 0B D4 FF FF       call        CVirtual::SetNumber (0EC11DBh)  
    27: 	printf("%d\n", TheVirtual.GetNumber());
00EC3DD0 8D 4D F4             lea         ecx,[TheVirtual]  
00EC3DD3 E8 BC D4 FF FF       call        CVirtual::GetNumber (0EC1294h)  
00EC3DD8 50                   push        eax  
00EC3DD9 68 3C 6B EC 00       push        offset string "%d\n" (0EC6B3Ch)  
00EC3DDE E8 97 D5 FF FF       call        _printf (0EC137Ah)  
00EC3DE3 83 C4 08             add         esp,8  
    28:     return 0;
00EC3DE6 33 C0                xor         eax,eax  
    29: }

  虚函数SetNumber()反汇编分析:、

    13: 	virtual void SetNumber(int nNumber)
    14: 	{
00EC1770 55                   push        ebp  
00EC1771 8B EC                mov         ebp,esp  
00EC1773 81 EC CC 00 00 00    sub         esp,0CCh  
00EC1779 53                   push        ebx  
00EC177A 56                   push        esi  
00EC177B 57                   push        edi  
00EC177C 51                   push        ecx  
00EC177D 8D BD 34 FF FF FF    lea         edi,[ebp-0CCh]  
00EC1783 B9 33 00 00 00       mov         ecx,33h  
00EC1788 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00EC178D F3 AB                rep stos    dword ptr es:[edi]  
00EC178F 59                   pop         ecx  
00EC1790 89 4D F8             mov         dword ptr [this],ecx  
    15: 		m_nNumber = nNumber;
00EC1793 8B 45 F8             mov         eax,dword ptr [this]  
00EC1796 8B 4D 08             mov         ecx,dword ptr [nNumber]  
00EC1799 89 48 04             mov         dword ptr [eax+4],ecx  
    16: 	}
00EC179C 5F                   pop         edi  
    16: 	}
00EC179D 5E                   pop         esi  
00EC179E 5B                   pop         ebx  
00EC179F 8B E5                mov         esp,ebp  
00EC17A1 5D                   pop         ebp  
00EC17A2 C2 04 00             ret         4  

  可以看到,虚函数与普通函数的实现流程并无差别,并没有看到虚表指针之类的操作。

  也就是说,直接通过对象调用自身的成员虚函数的时候,编译器使用了直接调用函数的方式,没有访问续表指针,来间接获取虚函数地址。

 

 

 

 

0x02 析构函数对虚表指针的操作

class CVirtual
{
public:
	virtual int GetNumber()
	{
		return m_nNumber;
		return 0;
	}
	virtual void SetNumber(int nNumber)
	{
		m_nNumber = nNumber;
	}
	~CVirtual()
	{

	}
private:
	int m_nNumber;
};

int main()
{
	//int a = sizeof(CVirtual);
	CVirtual TheVirtual;

	//TheVirtual.SetNumber(20);
	//printf("%d\n", TheVirtual.GetNumber());
    return 0;
}

  反汇编:

   17: 	~CVirtual()
    18: 	{
00A51730 55                   push        ebp  
00A51731 8B EC                mov         ebp,esp  
00A51733 81 EC CC 00 00 00    sub         esp,0CCh  
00A51739 53                   push        ebx  
00A5173A 56                   push        esi  
00A5173B 57                   push        edi  
00A5173C 51                   push        ecx  
00A5173D 8D BD 34 FF FF FF    lea         edi,[ebp-0CCh]  
00A51743 B9 33 00 00 00       mov         ecx,33h  
00A51748 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00A5174D F3 AB                rep stos    dword ptr es:[edi]  
00A5174F 59                   pop         ecx  
00A51750 89 4D F8             mov         dword ptr [this],ecx  
00A51753 8B 45 F8             mov         eax,dword ptr [this]  
00A51756 C7 00 34 6B A5 00    mov         dword ptr [eax],offset CVirtual::`vftable' (0A56B34h)  
    19: 
    20: 	}

  从析构函数中摘取关键的汇编指令:

00A5174F 59                   pop         ecx  
00A51750 89 4D F8             mov         dword ptr [this],ecx  
00A51753 8B 45 F8             mov         eax,dword ptr [this]  

  可以看出这和我写的上一篇博客:虚表与虚表指针中构造函数的操作是一模一样的,pop ecx还原this指针的值到ecx中,然后通过ecx赋值给this指针,再由eax得到this指针。

  最后一步:

00A51756 C7 00 34 6B A5 00    mov         dword ptr [eax],offset CVirtual::`vftable' (0A56B34h)  

  将当前类的续表首地址赋值到虚表指针中。

 

  通过分析构造函数和析构函数的流程可知:

  两者对虚标的操作过程几乎一致,都是将虚表指针设置为当前对象所属类的虚表首地址。看起来相同,其实差别很大。

  构造函数当中的虚表指针初始化,是将虚表指针初始化为正确的虚函数表基地址;而析构函数写入虚表指针,是将原对象的虚表指针重新赋值,其指针可能指向了另外一个虚表。

 

  下面有一份继承代码的反汇编来验证析构函数中对象虚表指针的重新赋值(验证结果为:父类指针指向子类的对象,并调用子类中的同名虚函数时,虚表指针会指向子类的虚函数表,再调用子类的函数,对象析构时,父类析构函数会将当前的虚表指针重新赋值为父类的虚表指针。)

  代码流程:

  

#include<iostream>  
using namespace std;

class A
{
public:
	void 刘大大()
	{
		printf("1\n");
	}
	virtual void 刘小()
	{
		printf("2\n");
	}
	~A()
	{

	}
};
class B : public A
{
public:
	void 刘大大()
	{
		printf("3\n");
	}
	void 刘小()
	{
		printf("4\n");
	}
};
int main(void)
{
	B b;

	A *p = &b;

	p->刘小();

	return 0;
}

  准备调用刘小()函数前,对象中的虚表指针指向B类的虚表:

 

  结束后B的析构函数中callA的析构函数:

 

  A的析构函数重新赋值虚表指针后,虚表指针指向了A的虚表:

 

posted on 2017-08-26 00:23  沉疴  阅读(283)  评论(0编辑  收藏  举报