CD64.【C++ Dev】多态(3): 反汇编剖析单继承下的虚函数表

发布于:2025-08-12 ⋅ 阅读:(12) ⋅ 点赞:(0)

目录

1.知识回顾

2.配置VS项目

关闭"仅我的代码调试"

禁用安全检查

关闭__RTC_CheckEsp

2.Debug+x86下编译以下代码

3.反汇编分析

整个程序的结构

main函数完整代码

VS动态调试

附:机器码转汇编指令查询

讲解

虚函数表是编译期间写好的,运行时只要设置obj对象的虚函数表的指针

Derived的构造函数

Base的构造函数

★多态调用

Derived的析构函数

4.总结关键点

5.附件: 博主第一次分析时的草稿


1.知识回顾

参见CD63.【C++ Dev】多态(2): 剖析虚函数表的前置知识文章复习

2.配置VS项目

为了方便反汇编分析,需要去除冗余的代码

关闭"仅我的代码调试"

项目 → 属性 → C/C++ → 常规 “支持仅我的代码调试” → 否

工具 → 选项 → 调试 → 常规 → 取消勾选"启用仅我的代码"

禁用安全检查

项目 → 属性 → 配置属性 → C/C++ → 代码生成 → 安全检查改成禁用(/GS-)

关闭__RTC_CheckEsp

项目 → 属性 → 配置属性 → C/C++ → 代码生成→基本运行时检查

2.Debug+x86下编译以下代码

#include <iostream>
using namespace std;
#define  TEST __asm{test ecx,ecx}
class Base
{
public:
	virtual void func1()
	{
		cout << " Base: virtual void func1()" << endl;
	}
	virtual void func2()
	{
		cout << " Base: virtual void func2()" << endl;
	}
	virtual ~Base()
	{
		cout << "~Base()" << endl;
	}
};

class Derived :public Base
{
public:
	virtual void func1()
	{
		cout << " Derived: virtual void func1()" << endl;
	}
	virtual void func2()
	{
		cout << " Derived: virtual void func2()" << endl;
	}
	virtual ~Derived()
	{
		cout << " ~Derived() " << endl;
		delete[] data;
	}
protected:
	char* data = new char[10];
};

int main()
{
	TEST
	Derived obj;
	TEST
	Derived& ref = obj;
	TEST
	Derived* ptr = &obj;
	TEST
	ref.func1();
	TEST
	ptr->func1();
	TEST
	return 0;
	TEST
}

注:代码中使用内联汇编指令test ecx,ecx是为了方便调试观察各个语句对应的反汇编指令,而且test ecx,ecx只改变状态寄存器的值,对本程序的运行没有影响

(test指令的参考资料:https://www.felixcloutier.com/x86/test)

3.反汇编分析

整个程序的结构

main函数完整代码

按照添加的test ecx,ecx对指令分块:

VS动态调试

附:机器码转汇编指令查询

下面讲解会用到

89 4D F0  mov    dword ptr [ebp-0x10],ecx

89 4D FC mov    dword ptr [ebp-0x4],ecx

8B 45 F0 mov    eax,dword ptr [ebp-0x10]

在线汇编和反汇编网站:https://defuse.ca/online-x86-assembler.htm#disassembly2

讲解

虚函数表是编译期间写好的,运行时只要设置obj对象的虚函数表的指针

无论运行多少次,_vfptr的值都不会变,可以看到几次调试的结果的寄存器的值都在变化,但是_vfptr的值都是0x004d8b9c

下面只讲关键部分,从第一个test ecx,ecx开始调试

 lea ecx,[obj] #ecx存储obj的地址

VS调用约定1:this指针由ecx传入

call后转到jmp指令,jmp指令执行后才真正进入Derived的构造函数

Derived的构造函数

进入Derived的构造函数后:

之前在CD58.【C++ Dev】继承(2)文章讲过:父类和子类的构造函数是各司其职的,即子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员

调用父类的构造函数需要传this指针,由ecx寄存器传参,然后保存到临时变量[this]中:

解析89 4D F0机器码后得知: [ebp-0x10]处临时存储obj的地址:

call后转到jmp指令,jmp指令执行后才真正进入Base的构造函数

Base的构造函数

进入Base的构造函数后,会在obj对象中初始化虚函数表指针vfptr

解析89 4D FC机器码后得知: [ebp-0x4]临时存储obj的地址:

VS使用eax当指针来向obj对象写入Base的虚函数表指针的值:

写入后:

之后从Base的构造函数返回:

返回到Derived的构造函数:

解析8B 45 F0机器码后得知: [ebp-0x10]临时存储obj的地址:

VS使用eax当指针来向obj对象写入Derived的虚函数表指针的值:

写入后:

之后向栈中push 0x10,准备调用operator new[]分配空间,这里省略operator new[]函数的分析

从operator new[]回到Derived的构造函数后:

调用方清栈:

设置eax指向obj对象:

[ebp-54h]作为中转,存储

写入ecx前:

写入ecx后:

写入[eax+4]前:

写入[eax+4]后:

注:mov dword ptr fs:[0],ecx的作用不在本文的讨论范围内

恢复各个寄存器,准备回到main函数:

回到main函数后:

设置ref值:

设置ptr值:

★多态调用

★eax帮助edx取得虚函数表的指针,ecx存储this指针,eax取得虚函数的入口,对于ptr->func1()调用也是一样的,这里略去func1()的内部分析

-->引用在本质上是通过指针实现的

VS调用约定2:edx存储虚函数表的指针,eax通过edx取得虚函数的指针之后通过call eax实现多态调用

Derived的析构函数

进入Derived的析构函数:

关键:调用父类的析构中重置了虚函数表指针vfptr的值:

原因:之前在CD58.【C++ Dev】继承(2)文章讲过先子析构后父析构

在析构过程中,对象会从子类退化为父类(先子析构后父析构)
如果不重置vfptr,当父类析构函数中调用虚函数时,vfptr仍指向子类虚表,因为子类的成员已被析构,所以继续调用子类的虚函数会访问无效内存
所以重置vptr后,可以确保调用父类的虚函数版本,避免访问已释放的子类资源

继续分析:

之前在CD58.【C++ Dev】继承(2)文章讲过:子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员,即子对象先析构,父对象后析构

所以会在Derived的析构函数的最后调用Base的析构函数

4.总结关键点

1.debug模式下,ecx存储this指针

2.debug模式下,edx存储虚函数表的指针,eax通过edx取得虚函数的指针之后通过call eax实现多态调用

3.构造函数初始化成员变量和虚函数表的指针,注:虚函数表的指针是在编译期间写好的,运行时直接填入对象中

4.子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员

5.子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员,即子对象先析构,接着重置虚函数表的指针,然后父对象后析构

6.如果函数是非虚函数,那么编译器会根据指针的类型找到该函数,即指针是哪个类的类型就调用哪个类的函数;如果函数是虚函数,并且派生类有同名的函数隐藏它,那么编译器会根据指针的指向找到该函数,即指针指向的对象属于哪个类就调用哪个类的函数,这就是多态

5.附件: 博主第一次分析时的草稿