欢迎光临散文网 会员登陆 & 注册

pvzclass是如何实现的?pvzclass源代码初步分析(4)Memory类 & AsmFuntions.h

2021-06-21 22:30 作者:__W1thoutD0ubt  | 我要投稿

本篇将分析PVZ.h中的Memory类,以及AsmFuntions.h & AsmFunctions.cpp) 。

文中会涉及到一些Windows API函数,不过本文不会对它们深层次的实现作深入探讨,请放心。

本文中的“PVZ本体”和“修改器本体”指的是运行中的程序,而不是存在存储器中的文件。

Memory类概览

PVZ.h中的Memory类

如上图所示,Memory类中的所有成员都带有static关键字,即它们都是静态的。

不过为了使用它们,你仍然需要实例化PVZ对象。

除了InjectDll()(.dll文件的注入)的定义在PVZ.cpp中完成,其他方法的定义都在Classes文件夹的Memory.cpp中。

Memory类中的成员可以分为三类:PVZ的属性、读写PVZ内存的方法,以及其他内存相关方法。

不过,因为PVZ属性相关的变量的赋值依赖于Memory类的方法,这些变量的分析放到最后。

读写PVZ内存的方法

ReadMemory、WriteMemory、ReadArray和WriteArray都用到了C++中的template,定义也是直接在PVZ.h中完成。

使用template,意味着我们在实际调用时完全不用担心各种变量类型的转换问题。这种问题放手交给pvzclass就行。

ReadMemory和ReadArray都使用了ReadProcessMemory,它的声明类似于如下的代码:

其中lpBaseAddr表示读取的起始地址,nSize表示读取的内存大小。

虽然返回值是Bool类型,但是实际上它只表示读取是否成功,实际的读取结果存储到修改器本体从lpBuffer开始的内存。

WriteMemory和WriteArray类似,它们使用了WriteProcessMemory,其声明类似于如下的代码:

与ReadProcessMemory类似,lpBaseAddr表示写入的起始地址,nSize表示读取的内存大小。

写入的数据源则是修改器本体从lpBuffer开始的内存。

虽然返回值是Bool类型,但是实际上它只表示写入是否成功。

某种意义上讲,ReadProcessMemory和WriteProcessMemory像是跨程序的memcpy。

ReadPointer的定义是在Memory.cpp中完成的:

套娃函数

要读取指针指向的变量,如果不用ReadPointer,需要多次使用ReadMemory。

ReadPointer可以减少代码量,用起来也非常方便。

实际上PVZ.cpp中GetAll类的方法,大都使用了ReadPointer来代替ReadMemory。

其他方法

剩余的方法也都用到了Windows API函数。

AllAccess使用VirtualProtectEx,可以将PVZ本体中的一段内存转变为可读、可写、可执行的状态。

AllocMemory使用VirtualAllocEx,可以在PVZ本体中申请一段未使用的内存,用来存放数据等内容。

AllocMemory的返回值为申请的内存的起始地址。

CreateThread使用CreateRemoteThread,可以从PVZ本体的某个位置直接开始一段线程的运行。

FreeMemory与AllocMemory相对,使用VirtualFreeEx,可以释放在PVZ本体中申请的内存。

Execute的代码如下:

像是单纯地封装了之前的方法

Execute的作用是在PVZ本体中运行一段在PVZ外写成的程序。

从上面的代码中,我们可以看出Execute的运行步骤:

申请内存、注入代码、运行线程、释放内存、取走返回值。

其中两个WriteMemory的作用,第一个是暂停PVZ本体其他线程的运行,第二个则是恢复其他线程的运行。

InjectDll的代码如下:

与Execute的结构高度相似。

不同的是,InjectDll执行的代码大部分是固定的,汇编代码(即__asm__InjectDll)主要存储在Asmfuntions.h中。

PVZ属性相关

在Memory.cpp中,我们可以看到四个变量的初值……吗?

4个“空”

这显然不能看出它们的作用。

实际上对它们赋值的主要代码在PVZ类的构造函数中:

(位于PVZ.cpp中)

这样这四个变量的作用就比较明晰了:

processId存储PVZ本体的进程ID,通过Open或其他方法获取;

hProcess存储PVZ本体的句柄,由OpnProcess获取;

mainwindowhandle存储PVZ窗口的句柄,从PVZ本体的内存中读取;

Variable则存储pvzclass申请的一段内存。

hProcess和mainwindowhandle可以作为某些Windows API函数的参数。

pvzclass中也有不少功能是用机器码实现的,这些功能临时占用的空间、部分参数的存储位置、返回值的暂存位置,全都在Variable对应的内存之中。

AsmFunctions

这里吐个槽,AsmFunction.h的拼写一直是错误的"AsmFuntion.h"("Function"没有"c")。下文采用"AsmFunction.h"。

这里的"Asm"指的是"Assembly Language",即“汇编语言”。

AsmFunctions(.h/.cpp)包含的,正是为pvzclass扩充汇编语言的支持的代码。

如果没有汇编语言基础的话,看这一节之前还是先去了解一下相关的内容吧。

当然,这不影响你使用pvzclass的其他绝大部分内容。

AsmFunctions.h包括三部分:定义汇编代码和INVOKE宏、定义封装用宏、声明汇编代码。

我们依次分析。

asm define部分中有一个INUMBER宏,它看上去很奇怪:

如果你熟悉位运算的话,应该能看出来,这是将一个32位整数8位8位地分割,然后将四部分的顺序颠倒过来而已。

这么做是针对机器码处理32位整数的方法。

即使是熟悉汇编语言的人,可能也会对代码名的后缀感到头皮发麻。

这里大概解释一下命名风格:

每个宏的命名由“代码名”和“后缀”两部分构成。

“代码名”对应的是某条指令在汇编语言中的名称。

“后缀”则表示该条指令的参数、参数类型、格式等。

如"DWORD"表示相关参数占4字节(而非默认的1字节),"_EAX"表示参数中含有寄存器eax,等等等等。

这里不详细展开。读者可以利用Cheat Engine等软件自己摸索。

接下来介绍一个比较重要的宏:INVOKE宏。

在汇编语言中,call指令使用相对引用。

也就是说,机器码都为"0xE8 0x64 0x00 0x00 0x00"的代码,在0xE38000时是"call 0xE38007",但在其他位置则是另外的"call"了。

但对于常用汇编语言的创作者而言,绝对引用的call是非常有必要的,因为PVZ的代码不会自己跑到内存的其他地方。

在这种情况下,INVOKE宏应运而生。

INVOKE宏可以视为一个替代CALL的解决方案。它可以作为一个绝对引用的CALL而不会产生其他副作用,避免了计算相对引用的繁杂过程。

在pvzclass中,INVOKE宏也相当常用。

读者也可以尝试在自己的pvzclass自用汇编代码中使用INVOKE宏。

在初始的INVOKE宏下方,还有多个INVOKE宏的变种,但它们都只是在INVOKE宏的基础上添加了参数而已。

定义封装用宏

这里定义的是pvzclass自用的汇编代码中使用的宏。

可以看到,这些宏只是在具体地应用INVOKE宏而已。

这里声明的是pvzclass自用的汇编代码。

在pvzclass中,机器码(包括自汇编代码转化而来的)用byte数组的形式存储。

这些的实装主要在AsmFunctions.cpp中完成。

pvzclass是针对PVZ的项目。

PVZ, PVZ, 怎么能没有P(植物)和Z(僵尸)呢?

下一篇开始分析pvzclass中,有关植物和僵尸的代码。

pvzclass是如何实现的?pvzclass源代码初步分析(4)Memory类 & AsmFuntions.h的评论 (共 条)

分享到微博请遵守国家法律