C++逆向分析--多态的本质

这篇我们延续上次的虚函数分析,来研究下多态的本质。

虚函数逆向分析的博客:C++逆向分析--虚函数(多态的前置)-CSDN博客

有了上篇虚函数的知识,我们在正向开发学习的时候知道,多态的发生需要存在继承关系,并且子类重写父类方法,父类需要重写的方法是虚函数。这么几点要求。我一开始接触多态是在学习Java的时候。当时的多态搞的我是一脸懵逼。不是很理解是怎么回事。但是在C++中一切的不理解我们都可以直接去逆向分析剖析他的原理。(Java其实也可以太蠢了不会)我相信C++的学懂了,Java学起来理解起来会更加得心应手。这是大佬告诉我的。

一。单继承无函数覆盖

废话不多说,先来一段demo:

#include<iostream>

using namespace std;

class Base {
public:
	virtual void func1() {
		printf("这是父类的fun1\n");
	}

	virtual void func2() {
		printf("这是父类的func2\n");
	}
};

class Base_Son :public Base {
public:
	virtual void func3() {
		printf("这是子类的func3\n");
	}

	virtual void func4() {
		printf("这是子类的fun4\n");
	}
};


int main() {

	Base_Son s1;

	int i = 0;
	for (i = 0; i < 4; i++) {

		int fun_call= *((int*)*((int*)&s1)+i);

		printf("调用函数地址=%p\n", fun_call);

		_asm {
			call fun_call;
		}

	}

	return 0;
}

我先大致解析下这段代码,主要有两个类,一个父类,一个子类。分别定义了4个虚函数。子类继承父类。

现在我们直接利用虚函数表去调用这四个函数。观察下效果:


有了上篇虚函数的知识,相信这里很容易理解。我就不一一分析了。但是这个demo告诉我们一件事情:当子类继承父类的时候,如果没有和父类同名的函数,那就没有重写父类的函数,并且只要有定义虚函数,那么在虚函数表中就能找到对应的函数地址。否则是不会在虚函数表中有记录。这就是单继承无函数覆盖。

画个图理解下这个过程:


二。单继承有函数覆盖(发生重写)

下面我们重写父类的方法,再次观察虚表的变化(我们只需要在原demo做一丢丢手脚让子类的方方法名和父类同名):

#include<iostream>

using namespace std;

class Base {
public:
	virtual void func1() {
		printf("这是父类的fun1\n");
	}

	virtual void func2() {
		printf("这是父类的func2\n");
	}
};

class Base_Son :public Base {
public:
	virtual void func1() {             //与父类方法同名
		printf("这是子类的func3\n");
	}

	virtual void func2() {             //与父类方法同名
		printf("这是子类的fun4\n");
	}
};


int main() {

	Base_Son s1;

	int i = 0;
	for (i = 0; i < 2; i++) {

		int fun_call= *((int*)*((int*)&s1)+i);

		printf("调用的函数地址=0x%p\n", fun_call);

		_asm {
			call fun_call;
		}

	}

	return 0;
}

现在我们再次运行程序观察效果:


神奇的事情发生了。(爸爸去哪了?)我们仅仅是改了子类的方法名,但是此时父类父类的方法没了,并且替换成了子类的方法。说明子类方法覆盖了父类的方法也就是发生了重写:


三。多继承无函数覆盖

我们知道C++是支持多继承的。也就是一个子类可以继承多个父类。这就是很反人类的地方(一个儿子可以有几个爸爸)。那么底层又是如何实现的呢?demo如下:

#include<iostream>
using namespace std;

class Base {
public:

	virtual void func1() {
		printf("这是父类的func1\n");
	}
	virtual void func2() {
		printf("这是父类的func2\n");
	}
};

class Base_Son{
public:
	virtual void func3() {
		printf("这是子类的func3\n");
	}
	virtual void func4() {
		printf("这是子类的func4\n");
	}
};

class Base_Grandson:public Base,public Base_Son {
public:
	virtual void func5() {
		printf("这是孙子类的func5\n");
	}
	virtual void func6() {
		printf("这是孙子类的func6\n");
	}
};


int main() {

		Base_Grandson g1;

		printf("g1的大小为=%d", sizeof(g1));
	return 0;
}

大概解释下这个代码,现在我们创建了三个类,一个父类,一个子类,一个孙子类。我们让孙子类同时继承父类和子类。此时我们猜测下用孙子类创建的对象大小是多大。(没有成员属性的情况下):


大小竟然是8。我们再用之前的方法去调用函数试试:

#include<iostream>
using namespace std;

class Base {
public:

	virtual void func1() {
		printf("这是父类的func1\n");
	}
	virtual void func2() {
		printf("这是父类的func2\n");
	}
};

class Base_Son{
public:
	virtual void func3() {
		printf("这是子类的func3\n");
	}
	virtual void func4() {
		printf("这是子类的func4\n");
	}
};

class Base_Grandson:public Base,public Base_Son {
public:
	virtual void func5() {
		printf("这是孙子类的func5\n");
	}
	virtual void func6() {
		printf("这是孙子类的func6\n");
	}
};


int main() {

		Base_Grandson g1;

	int i = 0;
	for (i = 0; i < 6; i++) {

		int fun_call= *((int*)*((int*)&g1)+i);

		printf("调用的函数地址=0x%p\n", fun_call);

		_asm {
			call fun_call;
		}

	}
		
	return 0;
}

输出结果如下:


我们惊讶的发现只调用了父类和孙子类的方法。这个代码是有bug的,因为虚函数表中只存在了4个函数地址,但是我们却再调用的时候遍历了6个因此程序是有问题的。但是我这样写只是为了验证这个虚函数表中确实只调用了4个方法。那我上面也继承了我的子类呀,为啥子类的方法没了。而且为啥孙子类的对象是8个字节。这就是我们要探究的问题:

先说结论:有多少个直接父类,就有多少张虚函数表。因此,我们的代码中有两个直接父类。所以在孙子类对象中存在两张虚函数表。

那我们在验证下:

#include<iostream>
using namespace std;

class Base {
public:

	virtual void func1() {
		printf("这是父类的func1\n");
	}
	virtual void func2() {
		printf("这是父类的func2\n");
	}
};

class Base_Son{
public:
	virtual void func3() {
		printf("这是子类的func3\n");
	}
	virtual void func4() {
		printf("这是子类的func4\n");
	}
};

class Base_Grandson:public Base,public Base_Son {
public:
	virtual void func5() {
		printf("这是孙子类的func5\n");
	}
	virtual void func6() {
		printf("这是孙子类的func6\n");
	}
};


int main() {

		Base_Grandson g1;

	int i = 0;

	for (i = 0; i < 4; i++) {

		int fun_call= *((int*)*((int*)&g1)+i);

		printf("调用的函数地址=0x%p\n", fun_call);

		_asm {
			call fun_call;
		}

	}

	printf("---------------------------------------------------\n");

	for (i = 0; i < 4; i++) {

		int fun_call2 = *((int*)*((int*)&g1+1) + i); //取第二张虚表

		printf("调用的函数地址=0x%p\n", fun_call2);

		_asm {
			call fun_call2;
		}

	}
		
	return 0;
}

运行结果为:


我们看到第二张虚函数表中,存放的是子类的函数地址。我们总结一下:

在多继承没有发生重写的情况下,第一张虚函数表中放的是第一个继承的父类的函数地址和孙子类的函数地址。而在第二张虚函数表中放的是第二个继承的子类的虚函数地址。也就是说除了第一个直接继承的父类,后面继承的父类的虚函数地址均会在其他表中。(假设有三个直接继承的父类,那么第三个父类的虚函数的地址会在第三张虚函数表中)图示如下:


四。多继承有函数覆盖(发生重写)

#include<iostream>
using namespace std;

class Base {
public:

	virtual void func1() {
		printf("这是父类的func1\n");
	}
	virtual void func2() {
		printf("这是父类的func2\n");
	}
};

class Base_Son{
public:
	virtual void func3() {
		printf("这是子类的func3\n");
	}
	virtual void func4() {
		printf("这是子类的func4\n");
	}
};

class Base_Grandson:public Base,public Base_Son {
public:
	virtual void func1() {      //发生重写,重写父类的方法
		printf("这是孙子类的func5\n");
	}
	virtual void func2() {      //发生重写,重写父类的方法
		printf("这是孙子类的func6\n");
	}
};


int main() {

		Base_Grandson g1;

	int i = 0;

	for (i = 0; i < 2; i++) {

		int fun_call= *((int*)*((int*)&g1)+i);

		printf("调用的函数地址=0x%p\n", fun_call);

		_asm {
			call fun_call;
		}

	}

	printf("---------------------------------------------------\n");

	for (i = 0; i < 4; i++) {

		int fun_call2 = *((int*)*((int*)&g1+1) + i); //取第二张虚表

		printf("调用的函数地址=0x%p\n", fun_call2);

		_asm {
			call fun_call2;
		}

	}
		
	return 0;
}

这里我们只做了一点手脚,让孙子类重写父类的方法观察结果:


我们发现父类的方法被覆盖掉了,也就是一旦子类重写父类,父类的方法将不会被调用,也就没必要在虚函数表中留下函数地址。我们在试试重写子类的方法改改我们的demo:

#include<iostream>
using namespace std;

class Base {
public:

	virtual void func1() {
		printf("这是父类的func1\n");
	}
	virtual void func2() {
		printf("这是父类的func2\n");
	}
};

class Base_Son{
public:
	virtual void func3() {    //发生重写,重写父类的方法
		printf("这是子类的func3\n");
	}
	virtual void func4() {    //发生重写,重写父类的方法
		printf("这是子类的func4\n");
	}
};

class Base_Grandson:public Base,public Base_Son {
public:
	virtual void func3() {      
		printf("这是孙子类的func5\n");
	}
	virtual void func4() {      
		printf("这是孙子类的func6\n");
	}
};


int main() {

		Base_Grandson g1;

	int i = 0;

	for (i = 0; i < 2; i++) {

		int fun_call= *((int*)*((int*)&g1)+i);

		printf("调用的函数地址=0x%p\n", fun_call);

		_asm {
			call fun_call;
		}

	}

	printf("---------------------------------------------------\n");

	for (i = 0; i < 4; i++) {

		int fun_call2 = *((int*)*((int*)&g1+1) + i); //取第二张虚表

		printf("调用的函数地址=0x%p\n", fun_call2);

		_asm {
			call fun_call2;
		}

	}
		
	return 0;
}

这里我们让孙子类重写子类的方法,运行结果如下:


我们发现覆盖的是第二张虚函数表的地址。那么由上两个实验我们得出一个结论:

重写谁的方法将会覆盖谁的虚函数表的地址。


五。多重继承无函数覆盖

#include<iostream>
using namespace std;

class Base {
public:

	virtual void func1() {
		printf("这是父类的func1\n");
	}
	virtual void func2() {
		printf("这是父类的func2\n");
	}
};

class Base_Son :public Base{
public:
	virtual void func3() {    
		printf("这是子类的func3\n");
	}
	virtual void func4() {    
		printf("这是子类的func4\n");
	}
};

class Base_Grandson : public Base_Son {
public:
	virtual void func5() {
		printf("这是孙子类的func5\n");
	}
	virtual void func6() {
		printf("这是孙子类的func6\n");
	}
};

int main() {

	Base_Grandson g1;

	int i = 0;

	for (i = 0; i < 6; i++) {

		int fun_call= *((int*)*((int*)&g1)+i);

		printf("调用的函数地址=0x%p\n", fun_call);

		_asm {
			call fun_call;
		}

	}

	return 0;
}

多重继承就是儿子继承爸爸,孙子继承儿子。就是下一代继承上一代的优良传统。那么他的布局是什么呢?运行结果:


多重继承又会变成只有一张虚表,而且虚表的顺序是按照辈分排列的。也就是最开始的父类会在首位,其次才是子类。(比较符合人类特性)画图布局如下:


六。多重继承有函数覆盖(发生重写)

我们下面改一下demo重写父类的方法:


我们看到会把对应的父类的方法位置给覆盖掉。这就是6种情况的剖析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/345485.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

创意飞轮

创业初期最难的是确定好的创业方向&#xff0c;创意飞轮就是一种用来寻找、判断创业想法的心智模型。原文: The Ideation Flywheel 想出好的创业点子是很困难的&#xff0c;伟大的想法和完全糟糕的想法往往在一开始很难区分&#xff0c;糟糕的想法也许看起来会很诱人&#xff0…

深入探讨Python JSON库:兼容性与高级应用【第34篇—python:JSON库】

文章目录 深入探讨Python中的JSON处理技术引言官方json库进阶用法多说一句 第三方json库DemjsonOrjson Demjson库详解Orjson库的高级选项Python JSON库的兼容性考虑JSON类型映射兼容性处理解码时使用object_hook处理中文字符编码 多版本兼容 结语 深入探讨Python中的JSON处理技…

1.8 万 Star!这款 Nginx 可视化配置工具太强了

NginxConfig简介 Nginx Config 是一个强大的 Nginx 配置文件生成器&#xff0c;号称配置 Nginx 服务器所需的唯一工具。 正因为 Nginx 功能强大&#xff0c;所以针对其各个功能的配置项会显得特别多&#xff0c;对于我们来说要记住那么多配置是一件十分头疼的事&#xff0c;甚…

Springboot+vue的校园疫情防控系统(有报告),Javaee项目,springboot vue前后端分离项目。

演示视频&#xff1a; Springbootvue的校园疫情防控系统&#xff08;有报告&#xff09;&#xff0c;Javaee项目&#xff0c;springboot vue前后端分离项目。 项目介绍&#xff1a; 本文设计了一个基于Springbootvue的前后端分离的校园疫情防控系统&#xff0c;采用M&#xff…

网络原理-初识(1)

目录 网络发展史 独立模式 网络互连 局域网LAN 广域网WAN 网络通信基础 IP地址 概念 格式 端口 概念 格式 认识协议 概念 作用 五元组 网络发展史 独立模式 独立模式:计算机之间相互独立; 网络互连 随着时代的发展,越来越需要计算机之间相互通信,共享软件和数…

【计算机网络】UDP协议与TCP协议

文章目录 一、端口号1.什么是端口号2.端口号范围划分3.认识知名端口号(Well-Know Port Number)4.netstat5.pidof 二、UDP协议1.UDP协议端格式2.UDP的特点3.面向数据报4.UDP的缓冲区5.UDP使用注意事项6.基于UDP的应用层协议 三、TCP协议1.TCP协议段格式1.1理解封装解包和分用1.2…

代码随想录算法训练营第14天 | 二叉树的前序、中序、后序遍历(递归+迭代法)

二叉树的理论基础&#xff1a;&#xff08;二叉树的种类&#xff0c;存储方式&#xff0c;遍历方式 以及二叉树的定义&#xff09; https://programmercarl.com/%E4%BA%8C%E5%8F%89%E6%A0%91%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html 二叉树的递归遍历 Leetcode对应的三道习…

使用骨传导耳机对人体有没有伤害?一文读懂骨传导耳机有什么危害?

不能说骨传导耳机对人体没有一点伤害&#xff0c;只能说骨传导耳机可以最大程度的减少对人体的伤害&#xff0c;首先就是骨传导耳机不用入耳&#xff0c;可以减少耳道内细菌的滋生&#xff0c;避免中耳炎等耳部疾病&#xff1b;其次就是骨传导的声音传播方式是通过骨骼直接进入…

Spring Boot 学习之——@SpringBootApplication注解(自动注解原理)

SpringBootApplication注解 springboot是基于spring的新型的轻量级框架&#xff0c;最厉害的地方当属**自动配置。**那我们就可以根据启动流程和相关原理来看看&#xff0c;如何实现传奇的自动配置 SpringBootApplication//标注在某个类上&#xff0c;表示这个类是SpringBoot…

thinkphp5向数据表插入数据并且获得id

$id db(数据表名)->insertGetId([status>1]); 直接...打印$id就是这条插入的数据的id了

数据采集与预处理01: 项目1 数据采集与预处理准备

数据采集与预处理01&#xff1a; 项目1 数据采集与预处理准备 任务1 认识数据采集技术&#xff0c;熟悉数据采集平台 数据采集&#xff1a;足够的数据量是企业大数据战略建设的基础&#xff0c;因此数据采集成为大数据分析的前站。数据采集是大数据价值挖掘中重要的一环&#…

python-分享篇-养老金数据统计

代码 import matplotlib.pyplot as plt import numpy as np # 为柱状图添加标注 def label(bars):for bar in bars:height bar.get_height()plt.text(bar.get_x()bar.get_width()/2.- 0.2, 1.03*height, %s % int(height))plt.rcParams[font.sans-serif] [SimHei] # 显示中文…

【数据结构】 顺序表的基本操作 (C语言版)

一、顺序表 1、顺序表的定义&#xff1a; 线性表的顺序存储结构&#xff0c;即将表中的结点按逻辑顺序依次存放在一组地址连续的存储单元里。这种存储方式使得在逻辑结构上相邻的数据元素在物理存储上也是相邻的&#xff0c;可以通过数据元素的物理存储位置来反映其逻辑关系。…

基于Springboot的大学生心理健康管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的大学生心理健康管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体…

【吃灰开发板复活】DIY全志V3s随身终端屏幕适配,LVGL以及各种外设驱动移植教程

在上周的文章中介绍了一款因作者想要学习Linux而动手DIY的终端设备V3S-PI&#xff0c; 《梦回2004&#xff01;我用全志V3s做了个成本100元&#xff0c;功能媲美MP4的随身终端》&#xff1a;梦回2004&#xff01;我用全志V3s做了个成本100元&#xff0c;功能媲美MP4的随身终端…

微信小程序如何获取当前日期时间

Hello大家好&#xff01;我是咕噜铁蛋&#xff0c;获取当前日期时间是小程序中经常会用到的一个功能。因此&#xff0c;在本文中&#xff0c;我通过科技手段给大家收集整理了下&#xff0c;今天我将向大家介绍如何在微信小程序中获取当前日期时间的方法&#xff0c;并分享一些实…

HubSpot在线客户互动:建立强大数字连接的关键一步

HubSpot在线客户互动为企业带来了多方面的具体业务优势&#xff0c;其中一些关键点包括&#xff1a; 提高销售转化率&#xff1a; 通过实时在线聊天、个性化推荐等互动方式&#xff0c;HubSpot使企业能够更主动地接触潜在客户&#xff0c;解答其疑问&#xff0c;提供定制化的…

RabbitMQ——高级篇

目录 一、MQ的常见问题 二、消息可靠性问题 生产者消息确认 消息持久化 消费者消息确认 失败重试机制 三、死信交换机 简介死信交换机 TTL超时机制 延迟队列 四、惰性队列 消息堆积问题 惰性队列 一、MQ的常见问题 消息可靠性问题&#xff1a;如何确保发送的…

解决 Git:ssh: connect to host github.com port 22: Connection timed out 问题的三种方案

1、问题描述&#xff1a; 其一、整体提示为&#xff1a; ssh: connect to host github.com port 22: Connection timed out fatal: Could not read from remote repository. 中文为&#xff1a; ssh&#xff1a;连接到主机 github.com 端口 22&#xff1a;连接超时 fatal&a…

【设计模式】腾讯面经:原型模式怎么理解?

什么是原型模式&#xff1f; 设计模式是编程世界的基石&#xff0c;其中原型模式无疑是一种常用而又高效的创建对象的手段。那么&#xff0c;什么是原型模式呢&#xff1f;又该如何去实现它&#xff1f; 在软件工程中&#xff0c;原型模式是一种创建型设计模式。我们可以这样…