首页 > 编程语言 >C++逆向分析——多态和虚表

C++逆向分析——多态和虚表

时间:2023-08-02 18:34:55浏览次数:31  
标签:虚表 函数 int Age 多态 C++ 地址 Print

虚表

上一章了解了多态,那么我们来了解一下多态在C++中是如何实现的。

了解本质,那就通过反汇编代码去看就行了,首先我们看下非多态的情况下的反汇编代码:

C++逆向分析——多态和虚表_虚表

然后再来看下多态情况下的反汇编代码:

C++逆向分析——多态和虚表_虚表_02

很明显这里多态的情况下会根据edx间接调用,而非多态则会直接调用。

那么我们来看下间接调用的流程是什么:

  1. ebp+8地址对应的值给到eax(ebp+8 也就是函数的参数 → 当前参数指针【父类指针】)
  2. eax地址对应的值给到edx(eax相当于当前对象的第一个成员)
  3. 调用edx地址对应的值,也就是子类对象的Print函数

但是这里很奇怪,第一个成员为什么就能是Print函数呢?跟我们之前理解的4个字节的参数完全不一样。

 

我在vs2022实验下:

#include <stdio.h>

class Person {
private:
	int Age;
	int Sex;
public:
	Person() {
		Age = 0;
		Sex = 0;
		printf("default person ctor invoked.\n");
	}

	Person(int Age, int Sex) {
		this->Age = Age;
		this->Sex = Sex;
	}

	virtual void Print() {
		printf("age = %d sex = %d\n", Age, Sex);
	}
};


class Teacher :public Person {
private:
	int Level;
public:
	Teacher() {
	}

	Teacher(int Age, int Sex, int Level) :Person(Age, Sex) {
		this->Level = Level;
	}

	virtual void Print() {
		Person::Print();
		printf("level = %d \n", this->Level);
	}
};



void main() {
	Teacher t(35, 0, 3);
	
	//t.Print();

	Person* p = &t;
	p->Print();	
	
	return;
}

 先说输出:

age = 35 sex = 0
level = 3
通过父类指针调用到了子类的函数。反汇编看到的:

C++逆向分析——多态和虚表_抽象类_03

 

那么编译器到底是做了什么工作,才能根据我们传入的对象来进行间接调用的呢?这是因为虚表

只要有虚函数,不论多少个,对象的数据宽度就会比其原来多出4个字节,这四个字节我们称之为虚表。

C++逆向分析——多态和虚表_抽象类_04

C++逆向分析——多态和虚表_子类_05

那么虚表在哪呢?可以通过VC6来寻找虚标,先创建对象然后下断点运行查看,如下图中,可以很清晰的看见对象t除了继承Person父类的Age、Sex以及本身的Level成员外,还有一个__vfptr,上面有一个地址就是0x00422024,那这个地址就是虚表,这个表里面存储的就是函数的地址:

C++逆向分析——多态和虚表_子类_06

我们可以调出内存窗口查看一下:

C++逆向分析——多态和虚表_虚表_07

这个存储的地址就是0x00401037,这时候切到反汇编代码就然后Ctrl+G输入跟进这个地址:

C++逆向分析——多态和虚表_子类_08

那这个地址就是Teacher的成员函数Print的地址。

虚表的结构:虚表中存储的都是函数地址,每个地址占用4个字节,有几个虚函数,则就有几个地址。

子类没有重写时,虚表中则只有父类自己的成员函数地址,反之,当子类重写虚函数时候,虚表中则存在父类自己的成员函数地址与子类重写的成员函数地址。

 

对于我上面的vs2022里的代码,看到的情况如下:

C++逆向分析——多态和虚表_子类_09

 

上述地址在反汇编里看到的就是:

C++逆向分析——多态和虚表_抽象类_10

最重要的是,看下有了虚函数表以后的内存布局:

如下,可以看到__vfptr就是放在this指针(也就是&t)的位置了!

 

名称


类型


__vfptr

0x00b57b58 {Conso.exe!void(* Teacher::`vftable'[2])()} {0x00b51064 {Conso.exe!Teacher::Print(void)}}

void * *

 

C++逆向分析——多态和虚表_抽象类_11

 

 

 对于虚表的内存布局,见这个文章总结:C++中的虚函数表实现机制——对于虚表的内存布局讲解得非常好

 

接下來,我们看看虚函数的覆盖是什么时候做的???

#include "stdio.h"

class CSum {
public:
	virtual int Add(int a, int b) {
		return (a + b);
	}

	virtual int Sub(int a, int b) {
		return(a - b);
	}
};

int x = 0xff;

class DSum : public CSum {
	virtual int Add(int a, int b) {		
		return (a + b + x);
	}
};

void main() {
	CSum* pCSum = new DSum;
	pCSum->Add(1, 2);
	pCSum->Sub(1, 2);

	CSum* pCSum2 = new DSum;
	pCSum2->Add(1, 2);

	delete pCSum;
	pCSum = NULL;

	delete pCSum2;
	pCSum2 = NULL;
}

 运行的时候,调试看看pCsum和pCsum2里面的虚表地址是不是同一个?答案是肯定的!如下所示:

C++逆向分析——多态和虚表_虚表_12

 

我原本以为子类重载了虚函数add以后,vfptr里面函数add的覆盖是在构造函数的代码里做的,实际上我想错了,vs2022里的做法是在编译时就将vfptr直接指向了一常量区域,如下所示,该常量区域里面的东西在编译生成二进制的时候就已经确定了到底是CSum还是DSum对应的虚表!==》核心就一句:vs2022将vfptr指向了一个常数!

 

C++逆向分析——多态和虚表_虚表_13

 

因此,如下代码,将pCSum修改为new CSum以后,看到的情况是pCsum和pCsum2里面的vfptr指向的常量不一样了!很容易理解:就是编译器在为CSum和DSum生成了两个虚表常量,然后在类的构造函数里将该常量赋值给vfptr了。

C++逆向分析——多态和虚表_虚表_14

纯虚函数

 

之前学习过虚函数,也提到了纯虚函数,虽然纯虚函数语法很简单的,但是其比较难理解,所以在有一定的面向对象的基础时候再来学习会比较容易理解一些。

纯虚函数语法:

  1. 将成员函数声明为 virtual
  2. 该函数没有函数体,最后跟=0

class Base {


public:


 virtual int Plus() = 0;

}

语法不过多的阐述,之前也有写过;接下来我们要了解一个新的概念:抽象类。

抽象类有这几种特征:

  1. 含有纯虚函数的类,称之为抽象类
  2. 抽象类也可以包含普通的函数;
  3. 抽象类不能实例化(创建对象)。

那么问题来了,抽象类有什么意义呢?我们可以把抽象类看作是对子类的一种约束,或者认为其(抽象类)就是定义一种标准。

比如:淘宝,有很多店铺,虽然每个店铺卖的东西都不一样,但是他们同样都可以下单、评论、购物车,也就是说他们都遵守了这种标准规则;也就是说你可以把淘宝当作一个抽象类,其有很多成员:购物车、评论、商品展示区...但是他都没有定义,而是交给开淘宝店的人(子类)去根据标准规则定义。

C++逆向分析——多态和虚表_虚表_15

而如果不按照这种标准呢来,那么假如要统计所有的数据就会非常麻烦,不便于管理。

标签:虚表,函数,int,Age,多态,C++,地址,Print
From: https://blog.51cto.com/u_11908275/6941554

相关文章

  • C++逆向分析——继承与封装
    面向对象程序设计之继承与封装之前已经学习过继承和封装了,但是要在实际开发中使用,光学语法和原理是不够的,在设计层面我们需要做一些优化。如下代码是继承的例子:#include<stdio.h>classPerson{public:intAge;intSex;voidWork(){printf("Person:Work()"......
  • C++逆向分析——引用
    voidmain(){intx=1;int&ref=x;ref=2;printf("%d\n",ref);return;}反汇编代码:intx=1;00724A5FC745F401000000movdwordptr[x],1int&ref=x;00724A668D45F4lea......
  • C++逆向分析——类成员的访问控制
    类成员的访问控制课外→好的编程习惯:定义与实现分开写,提升代码可读性。如下代码,Student这个类的所有成员我们都可以调用,但是我们不想让被人调用Print1这个方法该怎么?structStudent{intage;intsex;voidPrint1(){printf("FuncPrint1");}voidPrint(){......
  • C++逆向分析——new和delete new[]和delete[]
    在堆中创建对象我们可以在什么地方创建对象?全局变量区,在函数外面在栈中创建对象,也就是函数内在堆中创建对象注意:之前一直提到的堆栈实际上是两个概念->堆、栈,我们之前所讲的就是栈,从本章开始要严格区分。在C语言中,我们可以通过一个函数去申请一块内存,就是malloc(N);申请的这一块内存......
  • C++逆向分析——继承
    继承structPerson{intage;intsex;};structTeacher{intage;intsex;intlevel;intclassId;};如上代码中可以看见,Teacher类与Person类都存在着相同的2个成员age和sex,那么这就相当于重复编写了,我们可以通过继承的方式避免这样重复的编写(当前类名称:要......
  • C++逆向分析——this指针
    this指针概述C++是对C的拓展,C原有的语法C++都支持,并在此基础上拓展了一些语法:封装、继承、多态、模板等等。C++拓展新的语法是为了让使用更加方便、高效,这样就需要编译器多做了很多事情,接下来我们就需要一一学习这些概念。封装之前我们学习过结构体这个概念,那么结构体可以做参数传......
  • 【C++数据结构】启航,打开新世界的大门!
    @TOC一、学习数据结构的原因学习数据结构对于计算机科学和软件开发非常重要,它提供了处理和组织数据的有效方法和技术。以下是几个学习数据结构的重要原因:提高问题解决能力:数据结构教会了我们如何选择和使用适当的数据结构来解决问题。了解各种数据结构的特性和性能可以帮助我们分......
  • protobuf在c++中的使用
    一、安装sudoaptinstalllibprotobuf-devprotobuf-compiler二、编辑proto文件,生成代码文件proto语法分为“proto2”和”proto3“两个版本,指定方法是在proto文件中第一行写入:syntax="proto3";,这样指定使用proto3版本的语法,如果不指定,默认是使用proto2的语法。两个语法的......
  • C/C++ 数据结构五大核心算法之分治法
    分治法——见名思义,即分而治之,从而得到我们想要的最终结果。分治法的思想是将一个规模为N的问题分解为k个较小的子问题,这些子问题遵循的处理方式就是互相独立且与原问题相同。两部分组成:分(divide):递归解决较小的问题治(conquer):然后从子问题的解构建原问题的解三个步骤:1、......
  • 多态性
    多态性引入:传统的方法(形参为不同类,就要新建不同的方法)代码复用性不高,不利于代码维护多(多种)态(状态):方法或对象具有多种形态多态的具体体现:方法的多态方法的重载和重写都体现了多态对象的多态对象的多态一个对象的编译类型和运行类型可以不一致,编译类型在定义对象时就......