文章

滴水-day31-虚函数

struct Persont
{
	int x;
	int y;

	Persont()
	{
		this->x = 1;
		this->y = 2;
	}
	// 普通函数
	void PuTongFunc()
	{
		printf("PuTongFunc()....\\n");
	}

	// 虚函数
	virtual void virtualFunction_1()
	{
		printf("virtualFunction_1().....\\n");
	}
};

void Test_Persont()
{
	Persont persont;
	persont.PuTongFunc();
	persont.virtualFunction_1();
}

当结构托有虚函数的时候如果使用对象的方式调用函数那么传递的参数与普通函数无区别,都会传递一个this指针。

!https://cdn.nlark.com/yuque/0/2024/png/39210623/1727342960189-b6dcff54-49b5-4afe-b7ee-a8ed86e47cac.png

指针调用

!https://cdn.nlark.com/yuque/0/2024/png/39210623/1727343086431-e9ce8644-430b-4c04-bc45-dfc1c9467146.png

当使用指针调用普通函数与对象调用无区别,但是使用指针调用虚函数则有了区别。

分析: 首先取this指针保存的值存放到eax中,this指针保存了结构体首地址。

再取eax地址中保存的值赋值给了edx,从这里得到,此时结构体首地址一定不会是首个变量地址,而是一个保存了其他地址的一个地址,那么这个是怎么来的呢。

既然多了一个地址,那么结构体的大小一定会改变,验证一下是不是与虚函数有关系

!https://cdn.nlark.com/yuque/0/2024/png/39210623/1727343439213-e6ce9938-91ce-4264-9886-c6ec644ce223.png

当注释掉虚函数的时候输出结构体大小发现是8,这与两个int类型的变量一致。

同时当创建对象调用无参构造函数的时候,成员变量也正确的赋值

!https://cdn.nlark.com/yuque/0/2024/png/39210623/1727343511542-6d5be7fd-10f5-4623-b942-b07f09e46e93.png

实现虚函数

!https://cdn.nlark.com/yuque/0/2024/png/39210623/1727343545207-9caf3b89-d325-41b4-9886-8822bd13521f.png

发现结构体大小成了12,多了一个4字节,那么这4个字节是什么呢?存放在哪里呢

在创建对象的时候调试分析

!https://cdn.nlark.com/yuque/0/2024/png/39210623/1727343699097-aa9e2f71-a4b2-46bf-8e9d-a9970f2d1875.png

发现多的4个字节是在所有成员变量的前面,可称为结构体首地址,那么这个地首地址保存的一个值是什么呢?

与虚函数的关系是什么?

那就要接着刚才的分析了:

this指针保存了结构体首地址,取this指针保存的地址的值赋值给了eax,再去eax这个值中的内容(首地址的内容)赋值给了edx接着又取edx这个值保存的内容赋值给了eax后call进了eax

结合层层分析

当使用指针调用虚函数的时候不是直接调用的,而是间接调用,通过一个表去取地址去调用。

通过层层赋值传递发现最终还是调到了虚函数virtualFunction_1();

经过eax,edx,eax的取值,我发现当有虚函数的时候结构体首地址保存了一个地址

通过这个地址再次取值可以得到一个地址,再次取值得到函数的值进行调用。

this--->0x0019FDFC--->e8 7d 42 00--->98 13 41 00----CALL:98 13 41 00

!https://cdn.nlark.com/yuque/0/2024/png/39210623/1727345505126-c9b4d8ab-c21e-498d-8d21-e193e7f1f269.png

单继承无覆盖

// 单继承无函数覆盖(打印Sub对象的虚函数表)
struct Base
{
public:
	virtual void Function_1()
	{
		printf("Base:Function_1...\\n");
	}
	virtual void Function_2()
	{
		printf("Base:Function_2...\\n");
	}
	virtual void Function_3()
	{
		printf("Base:Function_3...\\n");
	}
};

struct Sub:Base
{
public:
	virtual void Function_4()
	{
		printf("Sub:Function_4...\\n");
	}
	virtual void Function_5()
	{
		printf("Sub:Function_5...\\n");
	}
	virtual void Function_6()
	{
		printf("Sub:Function_6...\\n");
	}
};
Sub sub;
Sub* pSub = ⊂
pSub->Function_1();

typedef void(*FuncTion)(void);
FuncTion fPn;
for (int i = 0; i < 6; i++)
{
	fPn = (FuncTion) * (((int*)*(int*)&sub) + i);
	// 虚函数地址
	cout << hex << uppercase << *(int*)(((int*)*(int*)&sub) + i) << endl;
	fPn();
}

虚函数继承,与普通函数无区别,通过函数指针调用也可以。

单继承有覆盖

// 单继承有函数覆盖(打印Sub对象的虚函数表)
struct Base1
{
public:
	virtual void Function_1()
	{
		printf("Base:Function_1...\\n");
	}
	virtual void Function_2()
	{
		printf("Base:Function_2...\\n");
	}
	virtual void Function_3()
	{
		printf("Base:Function_3...\\n");
	}
};
struct Sub1 :Base1
{
public:
	virtual void Function_1()
	{
		printf("Sub:Function_1...\\n");
	}
	virtual void Function_2()
	{
		printf("Sub:Function_2...\\n");
	}
	virtual void Function_6()
	{
		printf("Sub:Function_6...\\n");
	}
};

当子类有函数与父类一致的时候,一律按照子类的函数执行。

通过this指针获取到首地址存储的值后跟进这个值发现

!https://cdn.nlark.com/yuque/0/2024/png/39210623/1727348179697-56e17d09-bec8-4359-a863-ec1185613f85.png

只有4个地址,执行结果

!https://cdn.nlark.com/yuque/0/2024/png/39210623/1727348402246-54e5f7cc-d480-4577-98c7-0e3666537dc4.png

通过对象调用执行结果

!https://cdn.nlark.com/yuque/0/2024/png/39210623/1727348416552-38879ae6-bb97-4cbe-84e1-628fb97a003e.png

Sub1对象

0x427f08→0x4110a0,0x41153c,0x41119a,0x411320

Sub对象

0x427e44—>

0x411280,0x4114ce,0x4110af,0x411249,0x41115e,0x4115cd

虚函数地址替换

		Sub sub;
	Sub* pSub = &sub;
	pSub->Function_1();

	typedef void(*FuncTion)(void);
	FuncTion fPn;

	// 尝试修改虚函数地址调用其他函数
	// 获取目标函数地址
	DWORD FuncAddress = (DWORD)&Hello;

	// 第一个虚函数地址
	// 获取虚函数表指针
	void** vtable = *(void***)pSub;
	cout << hex << uppercase << vtable[0] << endl;

	// 使用void更好
	//int** address = (int**)*(int*)&sub;

	// 修改内存保护属性
	DWORD oldProtect;
	VirtualProtect(vtable, sizeof(void*), PAGE_EXECUTE_READWRITE, &oldProtect);

	// 修改值
	vtable[0] = (void*)&Hello;

	// 恢复内存保护属性
	VirtualProtect(vtable, sizeof(void*), oldProtect, &oldProtect);
	// 调用函数
	fPn = (FuncTion)vtable[0];
	fPn();

通过函数指针和函数地址修改虚函数地址表的属性把自定义函数地址写入虚函数地址表中,在通过函数指针进行调用,在后续的调用中也都调用了自定义函数而不是原有虚函数。

虚函数基础

  • 结构体含虚函数时,对象调用与普通函数无区别,都传递this指针

  • 指针调用虚函数时,通过间接方式调用,使用虚函数表

  • 含虚函数的结构体大小增加4字节,用于存储虚函数表指针

虚函数调用机制

  • 虚函数调用过程:this指针 → 结构体首地址 → 虚函数表 → 函数地址

  • 虚函数表位于结构体成员变量之前

继承中的虚函数

  • 单继承无覆盖:子类继承父类虚函数,并添加自己的虚函数

  • 单继承有覆盖:子类覆盖父类同名虚函数,执行子类版本

  • 继承后虚函数表结构会相应变化,反映继承和覆盖关系

License:  CC BY 4.0