首页
社区
课程
招聘
[原创]从反汇编的角度学C/C++之虚函数,单继承与多态
2021-10-2 20:14 6218

[原创]从反汇编的角度学C/C++之虚函数,单继承与多态

2021-10-2 20:14
6218

一.虚函数

    虚函数最为C++可以说是最重要的一个特征被广泛应用,那么它的内部机制究竟如何,它和普通函数使用的区别在哪里。定义如下的类

class Base
{
public:
	Base()
	{
		x = 1;
		y = 2;
	}
	void fun()
	{
		printf("fun\n");
	}
	virtual void fun1()
	{
		printf("virtual fun1\n");
	}
	virtual void fun2()
	{
		printf("virtual fun2\n");
	}
	virtual void fun3()
	{
		printf("virtual fun3\n");
	}
private:
	int x;
	int y;
};

    以上类定义中,有3个虚函数和1个普通的成员函数,我们首先看看增加了虚函数以后类变量是否有所变量以及对构造函数是否有影响

class Base
{
public:
	Base()
00BF1000  push        ebp  
00BF1001  mov         ebp,esp  
00BF1003  sub         esp,0CCh  
00BF1009  push        ebx  
00BF100A  push        esi  
00BF100B  push        edi  
00BF100C  push        ecx                  //保存类变量地址
00BF100D  lea         edi,[ebp-0CCh]  
00BF1013  mov         ecx,33h  
00BF1018  mov         eax,0CCCCCCCCh  
00BF101D  rep stos    dword ptr es:[edi]  
00BF101F  pop         ecx                  
00BF1020  mov         dword ptr [this],ecx    //将类变量地址放到局部变量 
00BF1023  mov         ecx,offset _D3472036_test@cpp (0C8200Bh)  
00BF1028  call        __CheckForDebuggerJustMyCode (0BF14E0h)  
00BF102D  mov         eax,dword ptr [this]                              //取出类变量地址赋给eax
00BF1030  mov         dword ptr [eax],offset Base::`vftable' (0C631B4h)     //将0C631B4赋给地址
	{
		x = 1;
00BF1036  mov         eax,dword ptr [this]          //取出类变量地址赋给eax
00BF1039  mov         dword ptr [eax+4],1           //在地址偏移4的地方赋值1
		y = 2;
00BF1040  mov         eax,dword ptr [this]          //取出类变量地址赋给eax
00BF1043  mov         dword ptr [eax+8],2          //在地址偏移8的地方赋值2
	}
00BF104A  mov         eax,dword ptr [this]  
00BF104D  pop         edi  
00BF104E  pop         esi  
00BF104F  pop         ebx  
00BF1050  add         esp,0CCh  
00BF1056  cmp         ebp,esp  
00BF1058  call        _RTC_CheckEsp (0BF14A0h)  
00BF105D  mov         esp,ebp  
00BF105F  pop         ebp  
00BF1060  ret

    由以上反汇编代码可以看出对于有虚函数的类变量,在初始化类成员之前会首先赋给一个地址,这个地址其实就是虚函数表的地址。这个地址在类变量的偏移为0的地方,随后的类成员依次向下排布。此时查看类变量地址对应的内存如下

                    

    可以看到对于有虚函数的类变量地址处首先存储的是我们的虚函数表,然后才是我们的类成员,至于这个虚函数表的内容是啥,为了方便展示这边使用IDA查看,如下图所示

    

    可以看到这个地址对应了3个4地址,而这3个地址其实就是我们所写的三个虚函数。那么对于虚函数的调用与普通的成员函数有什么区别

	Base base;
00481298  lea         ecx,[base]  
0048129B  call        Base::Base (0481000h)  

	base.fun();
004812A0  lea         ecx,[base]  
004812A3  call        Base::fun (0481160h)      //普通成员函数的调用
	base.fun1();
004812A8  lea         ecx,[base]  
004812AB  call        Base::fun1 (0481070h)      //对虚函数的调用

	Base *pBase = &base;
004812B0  lea         eax,[base]  
004812B3  mov         dword ptr [pBase],eax     //取出base地址并赋给pBase

	pBase->fun();
004812B6  mov         ecx,dword ptr [pBase]  
004812B9  call        Base::fun (0481160h)      //普通成员函数的调用
	pBase->fun1();
004812BE  mov         eax,dword ptr [pBase]      //取出pBase的值赋给eax,此时这个值就是base的地址
004812C1  mov         edx,dword ptr [eax]        //取出地址中的第一个元素内容,由上分析知道这个就是虚函数表的地址
004812C3  mov         esi,esp  
004812C5  mov         ecx,dword ptr [pBase]      //取出pBase的值赋给ecx
004812C8  mov         eax,dword ptr [edx]        //虚函数表地址偏移0处的地址所保存的值,此时这个值是我们第一个虚函数的地址赋给eax
004812CA  call        eax                        //调用虚函数

    由上可以看出如果是对象对虚函数或者是对象指针对普通函数的调用都是直接call函数地址,而如果是对象指针对虚函数调用,程序首先会去虚函数表中找到对应的虚函数地址,在对它进行call。

二.继承

    继承作为面向对象的核心概念被程序员广泛使用,那么对象的继承在内存中是如何表现得,修改上述得类定义如下

class Base
{
public:
	Base()
	{
		x = 1;
		y = 2;
	}
private:
	int x;
	int y;
};

class Sub : public Base
{
public:
	Sub()
	{
		this->z = 3;
	}
private:
	int z;
};

    根据如上得定义,类Sub继承Base,首先看看继承得类构造函数的变化如下

	Sub()
00121060  push        ebp  
00121061  mov         ebp,esp  
00121063  sub         esp,0CCh  
00121069  push        ebx  
0012106A  push        esi  
0012106B  push        edi  
0012106C  push        ecx  
0012106D  lea         edi,[ebp-0CCh]  
00121073  mov         ecx,33h  
00121078  mov         eax,0CCCCCCCCh  
0012107D  rep stos    dword ptr es:[edi]  
0012107F  pop         ecx  
00121080  mov         dword ptr [this],ecx  
00121083  mov         ecx,offset _D3472036_test@cpp (01B200Bh)  
00121088  call        __CheckForDebuggerJustMyCode (0121310h)  
0012108D  mov         ecx,dword ptr [this]          //取出类变量地址赋给ecx
00121090  call        Base::Base (0121000h)          //对于父类构造函数的调用
	{
		this->z = 3;
00121095  mov         eax,dword ptr [this]          //取出类变量地址
00121098  mov         dword ptr [eax+8],3           //类变量地址偏移8的地方赋值为3
	}
0012109F  mov         eax,dword ptr [this]  
001210A2  pop         edi  
001210A3  pop         esi  
001210A4  pop         ebx  
001210A5  add         esp,0CCh  
001210AB  cmp         ebp,esp  
001210AD  call        _RTC_CheckEsp (01212D0h)  
001210B2  mov         esp,ebp  
001210B4  pop         ebp  
001210B5  ret

    可以看到,对于子类,首先会调用父类的构造函数,而子类的成员地址,也会往后排列,上述父类构造函数的内容如下:

	Base()
00121000  push        ebp  
00121001  mov         ebp,esp  
00121003  sub         esp,0CCh  
00121009  push        ebx  
0012100A  push        esi  
0012100B  push        edi  
0012100C  push        ecx                  //保存类变量地址到栈中
0012100D  lea         edi,[ebp-0CCh]  
00121013  mov         ecx,33h  
00121018  mov         eax,0CCCCCCCCh  
0012101D  rep stos    dword ptr es:[edi]  
0012101F  pop         ecx                  //把类变量地址恢复到ecx
00121020  mov         dword ptr [this],ecx   //将类变量地址赋给局部变量
00121023  mov         ecx,offset _D3472036_test@cpp (01B200Bh)  
00121028  call        __CheckForDebuggerJustMyCode (0121310h)  
	{
		x = 1;
0012102D  mov         eax,dword ptr [this]   //将类变量地址赋给eax
00121030  mov         dword ptr [eax],1      //地址偏移0处赋值为1
		y = 2;
00121036  mov         eax,dword ptr [this]  
00121039  mov         dword ptr [eax+4],2    //地址偏移4处赋值为2
	}
00121040  mov         eax,dword ptr [this]  
00121043  pop         edi  
00121044  pop         esi  
00121045  pop         ebx  
00121046  add         esp,0CCh  
0012104C  cmp         ebp,esp  
0012104E  call        _RTC_CheckEsp (01212D0h)  
00121053  mov         esp,ebp  
00121055  pop         ebp  
00121056  ret

    由上可以得出结果,在子类构造函数中,首先会调用父类的构造函数对类变量地址空间中的变量进行赋值,其次才是本类的成员赋值。所以继承的子类中的成员变量的排列是在父类成员变量后面,在内存中查看子类变量的地址得到如下结果

                    

    这里有一个概念是继承中可以选择public得公有继承和private得继承继承,区别就是private得继承会把父类得成员变量和函数变成私有的在内存中保存,但是这只是编译器的检查,根据之前发的文章可以知道我们可以很容易的用指针的方式访问这些私有成员。

 三.虚函数与继承

    接下来看看虚函数在继承的子类中是如何保存的,修改上述的定义如下

class Base
{
public:
	Base()
	{
		this->x = 1;
		this->y = 2;
	}
	virtual void fun1()
	{
		printf("Base fun 1\n");
	}
	virtual void fun2()
	{
		printf("Base fun 2\n");
	}
	virtual void fun3()
	{
		printf("Base fun 3\n");
	}
private:
	int x;
	int y;
};

class Sub : public Base
{
public:
	Sub()
	{
		this->z = 3;
	}

	virtual void fun1()
	{
		printf("Sub fun 1\n");
	}
	virtual void fun3()
	{
		printf("Sub fun 3\n");
	}
	virtual void fun4()
	{
		printf("Sub fun 4\n");
	}
private:
	int z;
};

       首先我们依然先查看构造函数的调用情况

Sub()
00CD1070  push        ebp  
00CD1071  mov         ebp,esp  
00CD1073  sub         esp,0CCh  
00CD1079  push        ebx  
00CD107A  push        esi  
00CD107B  push        edi  
00CD107C  push        ecx  
00CD107D  lea         edi,[ebp-0CCh]  
00CD1083  mov         ecx,33h  
00CD1088  mov         eax,0CCCCCCCCh  
00CD108D  rep stos    dword ptr es:[edi]  
00CD108F  pop         ecx  
00CD1090  mov         dword ptr [this],ecx  
00CD1093  mov         ecx,offset _D3472036_test@cpp (0D6200Bh)  
00CD1098  call        __CheckForDebuggerJustMyCode (0CD1620h)  
00CD109D  mov         ecx,dword ptr [this]  
00CD10A0  call        Base::Base (0CD1000h)                              //调用父类构造函数
00CD10A5  mov         eax,dword ptr [this]  
00CD10A8  mov         dword ptr [eax],offset Sub::`vftable' (0D431CCh)   //将虚函数表地址赋给类变量偏移0处的地址
	{
		this->z = 3;
00CD10AE  mov         eax,dword ptr [this]                              
00CD10B1  mov         dword ptr [eax+0Ch],3                             //对类成员进行初始化
	}
00CD10B8  mov         eax,dword ptr [this]  
00CD10BB  pop         edi  
00CD10BC  pop         esi  
00CD10BD  pop         ebx  
00CD10BE  add         esp,0CCh  
00CD10C4  cmp         ebp,esp  
00CD10C6  call        _RTC_CheckEsp (0CD15E0h)  
00CD10CB  mov         esp,ebp  
00CD10CD  pop         ebp  
00CD10CE  ret

    唯一的变化就是在调用完父类构造函数后,程序首先对类变量地址为0的地方赋值了一个虚函数表的地址,接下来看看父类构造函数的内容

	Base()
00CD1000  push        ebp  
00CD1001  mov         ebp,esp  
00CD1003  sub         esp,0CCh  
00CD1009  push        ebx  
00CD100A  push        esi  
00CD100B  push        edi  
00CD100C  push        ecx  
00CD100D  lea         edi,[ebp-0CCh]  
00CD1013  mov         ecx,33h  
00CD1018  mov         eax,0CCCCCCCCh  
00CD101D  rep stos    dword ptr es:[edi]  
00CD101F  pop         ecx  
00CD1020  mov         dword ptr [this],ecx  
00CD1023  mov         ecx,offset _D3472036_test@cpp (0D6200Bh)  
00CD1028  call        __CheckForDebuggerJustMyCode (0CD1620h)  
00CD102D  mov         eax,dword ptr [this]  
00CD1030  mov         dword ptr [eax],offset Base::`vftable' (0D431B4h)  //将虚函数表地址赋给类变量偏移0处的地址
	{
		x = 1;
00CD1036  mov         eax,dword ptr [this]  
00CD1039  mov         dword ptr [eax+4],1  
		y = 2;
00CD1040  mov         eax,dword ptr [this]  
00CD1043  mov         dword ptr [eax+8],2                                 //为类成员变量赋值
	}
00CD104A  mov         eax,dword ptr [this]  
00CD104D  pop         edi  
00CD104E  pop         esi  
00CD104F  pop         ebx  
00CD1050  add         esp,0CCh  
00CD1056  cmp         ebp,esp  
00CD1058  call        _RTC_CheckEsp (0CD15E0h)  
00CD105D  mov         esp,ebp  
00CD105F  pop         ebp  
00CD1060  ret

    可以看到父类构造函数的内容和最开始讲解虚函数时候的内容是一样的,此时在用IDA查看两张虚函数表的内容如下

    虚函数表里面存着的是各个虚函数的地址,经过查看以后对着写函数进行重命名如下

    可以看到虚函数Base_fun2由于子类没用重载他,所以也在子类的虚函数表里面,而fun1, fun3由于被覆盖了,所以虚函数表存储的是Sub_fun1和Sub_fun3。由此可以得出结论,父类的虚函数会如果没有被子类重载,那么子类的虚函数表中就会有父类的虚函数,如果被重载了,虚函数表中就会存储子类的虚函数。

四.多态

   根据上面的内容可以总结出以下三点结论

  1. 当用指针调用虚函数的时候,程序会从虚函数表中找到相应的函数进行调用。

  2. 子类调用构造函数的时候,先调用父类的构造函数,父类的构造函数会先用自己的虚函数表覆盖在最开始的类变量地址中,接着子类在将自己的虚函数表覆盖在开始的类变量地址中,然后在对变量成员进行初始化。

  3. 如果子类中有对父类的虚函数进行重载,那么子类的虚函数表中存储的这个虚函数就是子类重载后的虚函数。

    跟据上面的三点结论,就应该可以很好理解多态这一概念了,修改类定义如下

class Base
{
public:
	virtual void fun1()
	{
		printf("Base fun 1\n");
	}
};

class Sub1 : public Base
{
public:
	virtual void fun1()
	{
		printf("Sub1 fun 1\n");
	}
};

class Sub2 : public Base
{
public:
	virtual void fun1()
	{
		printf("Sub2 fun 1\n");
	}
};

    此时main函数中的代码如下

int main()
{
	Sub1 sub1;
	Sub2 sub2;
	Base *pBase;

	pBase = (Base *)&sub1;
	pBase->fun1();
	pBase = (Base *)&sub2;
	pBase->fun1();

	return 0;
}

    对应的反汇编如下

	Sub1 sub1;
000812B8  lea         ecx,[sub1]  
	Sub1 sub1;
000812BB  call        Sub1::Sub1 (081040h)      //调用Sub1构造函数
	Sub2 sub2;
000812C0  lea         ecx,[sub2]  
000812C3  call        Sub2::Sub2 (081090h)      //调用Sub2构造函数
	Base *pBase;

	pBase = (Base *)&sub1;
000812C8  lea         eax,[sub1]  
000812CB  mov         dword ptr [pBase],eax  //将sub1的地址给到pBase
	pBase->fun1();
000812CE  mov         eax,dword ptr [pBase]  //取出pBase的值赋给eax,此时eax的值就是sub1的地址
000812D1  mov         edx,dword ptr [eax]    //地址最开始处的数据就是虚函数表的地址
000812D3  mov         esi,esp  
000812D5  mov         ecx,dword ptr [pBase]  //将pBase中sub1的地址赋给ecx
000812D8  mov         eax,dword ptr [edx]    //虚函数表地址第一个函数的地址
000812DA  call        eax                    //调用这个函数
000812DC  cmp         esi,esp  
000812DE  call        _RTC_CheckEsp (081560h)  
	pBase = (Base *)&sub2;
000812E3  lea         eax,[sub2]  
000812E6  mov         dword ptr [pBase],eax  
	pBase->fun1();
000812E9  mov         eax,dword ptr [pBase]  
000812EC  mov         edx,dword ptr [eax]  
000812EE  mov         esi,esp  
000812F0  mov         ecx,dword ptr [pBase]  
000812F3  mov         eax,dword ptr [edx]  
000812F5  call        eax                    //与上面同样的操作

    首先可以看到,当类对象有虚函数的时候,即使我们没用构造函数,编译器也会为我们生成构造函数。

    其次可以看到通过指针调用的时候,两次都是从虚函数表中找到函数进行调用,也就是说此时虚函数表中对应位置的函数是谁,他就调用谁。那么究竟是谁就要看看这两个构造函数里面的内容。

Sub1::Sub1:
00F11040  push        ebp  
00F11041  mov         ebp,esp  
00F11043  sub         esp,0CCh  
00F11049  push        ebx  
00F1104A  push        esi  
00F1104B  push        edi  
00F1104C  push        ecx  
00F1104D  lea         edi,[ebp-0CCh]  
00F11053  mov         ecx,33h  
00F11058  mov         eax,0CCCCCCCCh  
00F1105D  rep stos    dword ptr es:[edi]  
00F1105F  pop         ecx  
00F11060  mov         dword ptr [this],ecx  
00F11063  mov         ecx,dword ptr [this]  
00F11066  call        Base::Base (0F11000h)  
00F1106B  mov         eax,dword ptr [this]  
00F1106E  mov         dword ptr [eax],offset Sub1::`vftable' (0F831C8h)  //sub1虚函数表地址
00F11074  mov         eax,dword ptr [this]  
00F11077  pop         edi  
00F11078  pop         esi  
00F11079  pop         ebx  
00F1107A  add         esp,0CCh  
00F11080  cmp         ebp,esp  
00F11082  call        _RTC_CheckEsp (0F11560h)  
00F11087  mov         esp,ebp  
00F11089  pop         ebp  
00F1108A  ret

Sub2::Sub2:
00F11090  push        ebp  
00F11091  mov         ebp,esp  
00F11093  sub         esp,0CCh  
00F11099  push        ebx  
00F1109A  push        esi  
00F1109B  push        edi  
00F1109C  push        ecx  
00F1109D  lea         edi,[ebp-0CCh]  
00F110A3  mov         ecx,33h  
00F110A8  mov         eax,0CCCCCCCCh  
00F110AD  rep stos    dword ptr es:[edi]  
00F110AF  pop         ecx  
00F110B0  mov         dword ptr [this],ecx  
00F110B3  mov         ecx,dword ptr [this]  
00F110B6  call        Base::Base (0F11000h)  
00F110BB  mov         eax,dword ptr [this]  
00F110BE  mov         dword ptr [eax],offset Sub2::`vftable' (0F831DCh)  //sub2虚函数表地址
00F110C4  mov         eax,dword ptr [this]  
00F110C7  pop         edi  
00F110C8  pop         esi  
00F110C9  pop         ebx  
00F110CA  add         esp,0CCh  
00F110D0  cmp         ebp,esp  
00F110D2  call        _RTC_CheckEsp (0F11560h)  
00F110D7  mov         esp,ebp  
00F110D9  pop         ebp  
00F110DA  ret

    可以看到这两个子类变量在调用完父类构造函数以后分别用不同的虚函数表地址覆盖在类变量最开始的4字节。在IDA中查看这两个地址


    可以看到这两个虚函数表地址中的虚函数是两个不同的函数,而这两个函数其实就是我们两个子类中重载的虚函数,由此程序便实现了多态。最终的运行结果如图


[培训]内核驱动高级班,冲击BAT一流互联网大厂工 作,每周日13:00-18:00直播授课

最后于 2021-10-20 11:13 被1900编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回