C++对象内存布局结构及虚函数调用实现分析(上)
在windows平台,最常见的开发语言是c++语言。大部分游戏逻辑直接采用c++开发;或者采用脚本语言开发游戏逻辑,但是脚本解释引擎本身却采用c或者c++语言开发。如果能了解c++语言,并能把c++语言和其对应的汇编语言实现一一对应起来,那么在阅读游戏汇编代码的时候就能在大脑中把汇编语言反编译成高级语言去理解,这样可以很大程度上加快逆向效率。
C++语言本身就是一门很丰富的学问,而且市面上有很多c++经典著作可以去学习,我也没能力把c++语言的教程写的比那些经典著作更好,所以我这里就不再介绍C++的基础知识。另外汇编语言本身也类似情况,本文也不会去解释什么是eax、eip。本文会假设所有读者都能熟悉c++基本语法,也能知道单句汇编指令是什么意思。写本篇文章的目的只是希望能通过本篇文章的介绍让读者了解高级语言部分特性的汇编层实现,同时除了能知道高级语言的用法之外对高级语言的认识更深刻一些。
一、C++语言中this指针的含义
在给出正确答案前,我们先看看以下C++代码,通过代码说话。
代码解释:objMonster数组里直接保存了对象的内存指针信息,也就是通过new分配的对象内存起始地址。在main函数里直接通过OutputDebugString把各个对象的内存起始地址打印出来。然后在CGameObject::OnUpdate函数内部把this指针也作为1个数值输出出来。然后打开debugview查看输出日志信息,结果如下:
得出结论:通过日志我们可以看到,单数行的dwObjectPtr的值与双数行object的值是完全一模一样的。这个结论也就说明了c++对象的this指针也就是对象内存地址的起始地址。
二、c++对象内存布局结构
接第一小结,我们已经知道了this指针就是内存对象内存起始地址,所以我们要了解c++类对象的内存布局,可以直查看内存保存哪些信息。使用ce的内存浏览功能,可以看到信息如下:
图2.1
而对应该内存时对象实时信息如下:
图2.2
关于CPlayerObject对象定义如下:
图2.3
图2.4
图2.5
图2.6
有了这些足够的信息,我们接下来开始讲解图2.1中内存布局。
2.1函数虚表起始地址
首先从图2.2中我们可以看到我们定义的CPlayerObject对象内存起始地址为0x01D72E38,然后通过图2.1可知,放在地址0x01D72E38处的第一个DWORD值为0x00402250,使用IDA打开EXE文件,跳转到0x00402250处,内容如下:
图2.6
从图2.6中可以看到,0x00402250实际上是保存了一个函数数组列表,里面存放的是该类所有虚函数的函数地址信息。该函数的存放顺序和c++源码中类的虚函数声明顺序一致。
得出结论:类对象在内存中第一个DWORD值就是函数虚表起始地址,以该地址开头的内存中存放了一系列该类的需函数地址。从地址0x00402250开始到0x00402278截止这部分内存保存的表也就是所谓的虚表(虚函数表)。
2.2 this指针的传递与虚函数的调用实现
用IDA查看main函数的实现,汇编代码截图如下:
图2.7
图2.7和图2.5对照着看,图2.5是c++实现源码,图2.7是汇编层实现代码。从汇编代码中我们可以看到:
00401290 mov ecx, ?objMonster@@3PAPAVCGameObject@@A
该行代码执行后,ecx里存放的将是objMonster[0]的指针
00401296 mov eax, [ecx]
改行代码执行后,eax里存放的就是虚表指针了,也就是0x00402250的值
00401298 mov edx, [eax+24h]
该行代码执行后,edx里面保存的也就是[0x00402250 + 0x24] = [0x00402274]的值,对照图2.6可以看到00402274处存放的也就是CPlayerObject::OnDamageHp函数。
从以上逻辑我们可以看出,一般会把对象指针放到ecx里,然后取得ecx的第一个DWORD得到虚函数起始地址,通过虚函数起始地址加上虚函数偏移得到将要调用的虚函数地址,然后调用。
得到结论:this指针一般通过ecx传递,虚函数调实现就是先通过this指针取得虚表地址,通过虚表地址加虚函数偏移取得目标虚函数地址,然后调用。
2.3父类与子类的内存布局
继续利用ida神器,通过菜单view->opensubviews->local types可以打开pdb里保存的各种类信息和结构体信息。也就是类似windebug里dt命令的功能。通过查看CPlayerObject类的信息可以得到如下截图:
图2.8
从图2.8可以发现CPlayerObject对象在内存中是前面保存了一个CGameObject对象,然后后面跟着两个CPlayerObject对象区别于CGameObject对象独有的2个成员变量。在查看CGameObject对象的内存布局信息,可以看到截图如下:
图2.9
对照着图2.1和2.2可看到:
0x01D72E38 [this + 000] = 00402250 vftalber
0x01D72E3C [this + 004] = 00002708 m_nHp
0x01D72E40 [this + 008] = 00004E20 m_nMp
0x01D72E44 [this + 00C] = 00002710 m_nMaxHp
0x01D72E48 [this + 010] = 00004E20 m_nMaxMp
0x01D72E4C [this + 014] = m_szObjectType Player
0x01D72E8C [this + 054] = m_szObjectName IamBOSS
0x01D72E8C [this + 094] = 00000006 m_nDamageIndex
0x01D72E8C [this + 098] = 00000008 m_nPlayerLevel
得出结论:除类的第一个DWORD为虚表函数起始地址外,其它地方存放的都是类的成员变量信息,内存中的成员变量位置和源码中的声明顺序是一一对应的。如果其中有父子类关系则子类的成员变量跟在父类的成员变量之后。