C++反汇编 剖析虚函数表的实现原理(下)

上节回顾:
C++多态的实现,以及虚函数表的引入,反汇编剖析
基础反汇编知识:
反汇编分析C/C++常见语句的底层实现(入门)

回顾

我们上一节讲到,定义为virtual的函数就称为虚函数,它为继承自它的子类提供了不同的函数调用的方法。

class Animals
{
public:
	void speak()
	{
		cout << "Animal::speak()\n";
	}
	void run()
	{
		cout << "Animal::ran()\n";
	}
};

....省略动物的继承关系

//一个统一的动物行为
void action(Animals* ani)
{
	ani->speak();
	ani->run();
}
int main()
{	
	action(new Dog);
	action(new Cat);
	action(new Pig);
	return 0;
}

在这里插入图片描述

我们把这个过程称之为实现多态的过程。我们利用父类指针指向子类对象,这就是实现多态的本质。

为什么父类指针可以动态的决定他要调用哪个函数? 明明都是一个Animals的指针,为什么在调用函数后却能调用不同的子类的函数?

答案:它使用了一个叫做虚函数表的东西。


当我们把父类实现为virtual的时候,它实际上就产生了一个虚表
还记得我们在上一节讲到的加不加virtual的区别吗?

  • 如果我们不加virtual,则他就不是虚函数: 它产生的汇编是这样的:
    在这里插入图片描述
    已经给你固定死了,尽管你使用的是多态的方法,即父类指针调用子类对象,但是因为你没有声明为virtual虚函数,所以在call函数地址的时候,已经给你写死了,就是直接调用本对象的函数地址

  • 如果你加virtual,他就是虚函数: 它产生的汇编:(完整汇编代码)
    在下面分析的时候也会用到此图:
    在这里插入图片描述
    它为什么加了这么多行东西?

实际上,当你定义了一个virtual虚函数,并且使用父类指针调用子类对象的时候,指针会自动识别你的对象的具体类型是一个Animals指针,但是是一个Dog对象,所以他会生成一个虚表,根据虚表来查找到你具体要调用的函数地址

反汇编剖析虚函数表

虚函数表只要包含虚函数的类就会有一个虚函数表,当这个类是基类时,它的派生类也会有相应的虚函数表。当一个类有多个对象的时候,这些对象共享一个虚函数表。

我们分别赋予父类和子类两个变量,m_age和m_life,我们可以查看他们所处的类所占的字节大小:

class Animals{
public:
	int m_life = 10;
	virtual void speak(){ ... }
	virtual void run(){ ....  }
};
class Dog :public Animals{
public:
	int m_age = 20;
	void speak(){ ... }
	void run(){ ... }
};
int main(){
	Animals* dog = new Dog;
	dog->speak();
	dog->run();
	
	cout<<sizeof(Dog)<<endl;		//结果为12
	return 0;
}

在这里插入图片描述
我们的Dog类继承了父类,它自己有一个m_age的4字节int,父类有一个m_life也占用四个字节他不应该一共只占用8个字节吗? 怎么多出来4个字节,其实,多出来的4个字节,存放此类的虚函数表的地址
8个字节存储两个int,它又产生了4个字节的地址,我们称此地址为虚表的地址,并且此地址位于总共12个字节的最前面4个字节


这就是每个指针变量(dog)的内存结构图:
在这里插入图片描述
我们来一步步解析汇编指令: (完整汇编代码,请看上面,并且我去除了一些不重要的汇编指令)
关键四句:

  //d->speak();  实现汇编代码:

 mov         eax,dword ptr [dog]  	//找到指针变量d的地址所处的内存空间,取出四个字节给到eax存储, eax存储了指针d的地址
 mov         edx,dword ptr [eax] 	//找到eax所存储的地址的内存空间,取出4个字节给到edx存储,edx存储了首4个字节,即虚表的地址
 mov         eax,dword ptr [edx]  //找到虚表的地址所处的内存空间,取出4个字节给到eax存储,eax存储了虚函数表的前4个字节的内存,即Dog::speak 的函数地址
 call        eax  			      // 跳转到函数

这是我们的d->speak的实现原理:

  • mov eax,dword ptr [dog]
    首先:找到指针变量dog(Animals*类型)的地址所在的内存空间,取出四个字节给到eax寄存器,eax寄存器就存储了Dog对象的地址。 此对象包括12个字节,前四个字节为虚表的地址

eax存放的内容:(就是图示的整个结构,即Dog类的对象的内存空间的内容)
在这里插入图片描述

  • mov edx,dword ptr [eax]
    在eax所处的内存空间中取出四个字节给到edx寄存器,此处,eax就得到了图示四个字节存储空间里的内容,内容就是虚表的地址,所以edx就获取了此地址

edx存储的内容:虚表的地址,edx: 0x00B89B64h
在这里插入图片描述

  • mov eax,dword ptr [edx]
    在edx所处的内存空间中取出4个字节给到eax寄存器,edx此时已经存储了虚表的地址了,虚表里面前4个字节是第一个函数的地址后4个字节是第二个函数地址(一共8个,因为有两个虚函数),所以取出的是前四个字节的内容,即第一个虚函数的地址,即eax里存储第一个虚函数的地址。

eax存储的内容:此时就是第一个虚函数speak的地址:eax:0x00B814E7
在这里插入图片描述

  • call eax
    eax经过上面的操作,已经存储了第一个虚函数的地址,call eax,代表call到寄存器所存储的内容,即call此地址,跳转到此虚函数

我们来看一下run函数的执行过程:

	dog->run();

 mov         eax,dword ptr [dog]  
 mov         edx,dword ptr [eax]  
 mov         eax,dword ptr [edx+4]  	//此时第二个函数的地址
 call        eax    

也是,首先找到对象的地址,再找到虚函数表的地址,再找到第二个虚函数的地址,即 [edx+4] 就存储了第二个虚函数的地址,把此地址给到eax存储,再由call跳转到此函数


验证虚函数地址的正确性

如何证明虚表里存储的就是虚函数的地址??
你怎么证明虚表的所对应的存储地址的那8个字节就是两个虚函数的地址呢?

解析:
在这里插入图片描述
根据dog的地址找到其内存

dog对象的内存空间
在这里插入图片描述

一共占用12个字节一个虚表4字节,两个int8字节),并且前四个字节就是虚表的地址

假设这就是虚表的地址:所以我们得到了虚表的地址:0x00169b6c (小端存储)

根据此虚表地址,我们进入其内存空间:
在这里插入图片描述
虚函数表的地址所在的内存空间对应8个字节, 分别表示表示两个虚函数的地址。

前四个:第一个虚函数的地址: 0x001615fa
后四个:第二个虚函数的地址: 0x001615cd

我们已经得到了我们认为的虚函数的准确地址,现在我们进入反汇编来看看call跳转的地址是不是就是我们找到的这两个地址。


我们进入反汇编:
经过上述分析可以得知:

第一个虚函数:speak:

....
....
0x00166D7D  mov         eax,dword ptr [edx]    //eax存储第一个虚函数地址
0x00166D7F  call        eax  					//进入此函数

经过上述分析得到,eax 存储第一个虚函数的地址,此时我们监视eax:
在这里插入图片描述
可以得知:eax的值就是 0x001615fa,即这就是第一个虚函数的地址


继续进入run的反汇编:
在这里插入图片描述
eax的值就是0x001615cd,即这就是第二个虚函数的地址。
在这里插入图片描述
call地址后,我们就能进入 0x001615cd,即第二个虚函数的地址:Dog的run函数。


总结:

  • 至此,我们在反汇编中看到的eax存储的地址和在dog对象在内存中存储的两个地址一摸一样的,所以,我们可以得知,dog对象的前四个对象的地址就是虚表的地址,同时在虚表的内容中存储的八个字节就是两个虚函数的地址(dword ptr)。
posted @ 2022-09-26 22:08  hugeYlh  阅读(56)  评论(0编辑  收藏  举报  来源