今日重新回顾了一下以前使用过的ce修改器,在学过C语言之后感觉概念通透了许多。
为什么要用指针
CE寻找到的一些地址往往是一个动态地址,它是动态生成的(例如malloc函数),每次重启游戏后,它都会随之改变,我们不可能每次打开游戏时都重新搜索一波。
当我们想要找一个通用的地址,让我们每次重新启动游戏后仍然可以准确定位到我们想要的位置的时候。就要考虑到这个地址是如何被动态生成的了。
指针
我们都知道修改植物大战僵尸中的阳光时,要先找到本局游戏阳光的地址,然后一级一级查找是谁改变了这个地址,一直到找到基地址之后,就可以基地址+偏移+偏移从而达到一个通用的找到阳光地址的方法了。
但是在游戏编写者的眼中,它是什么样的呢?
看这样一段代码
#include<stdio.h>
#include<malloc.h>
struct BiTNode { //定义二叉树结构体
char dat;
struct BiTNode *lChild;
struct BiTNode *rChild;
};
void CreatTree(BiTNode **T) { //得到*T的地址
char c;
scanf_s("%c", &c);
if (c == ' ') {
*T = NULL;
}
else {
(*T) = (BiTNode*)malloc(sizeof(BiTNode)); //*T指向的区域更改为新开辟的区域(创建的BiTNode区域被作为一个指针赋给*T)
(*T)->dat = c; //赋值给*T指向的bitnode区域中的dat元素
CreatTree(&(*T)->lChild); //将*T指向的区域的元素lchild的地址作为入口参数传给下一个**T
CreatTree(&(*T)->rChild);
}
}
void PreOderTraverse(BiTNode *T, int level) {
if (T) {
printf("第%d层,%c\n", level,T->dat);
PreOderTraverse(T->lChild, level + 1);
PreOderTraverse(T->rChild, level + 1);
}
}
int main()
{
BiTNode *T = NULL; //指针T指向NULL
CreatTree(&T); //取指针T的地址传给函数内的**p 实现**p->&p
PreOderTraverse(T, 0);
}
这是一个关于二叉树生成,遍历的代码,我们重点观察里面指针的部分
在main函数内我们定义了一个BiTNode *T,它是一个指针,由于它在main函数内就已经被定义,所以它的地址接近0x00400000(exe文件的基地址)。实际上也就是我们ce中最终找到的基地址。(基地址一般接近于0x00400000)。通过这个基址/指针,我们可以得到它所指向的位置(目前为NULL)。
接下来观察CreatTree这个函数。它的入口参数为指针T的地址,之后的操作就是我们即将说到的多重指针。
多重指针
在CreatTree函数内,(函数的入口参数为&T,即指针T的地址,也就是我们所说的基址)
有一句
(*T) = (BiTNode*)malloc(sizeof(BiTNode)); //*T指向的区域更改为新开辟的区域(创建的BiTNode区域
这句话的意思就是为我们传入的指针*T,随机分配一片没有被使用的内存空间,并且将*T 所指的区域(原先为NULL), 更改为新创建的区域的起始位置。
此时此刻就达成了我们所谓的二重指针,每一次开辟的内存空间可能不一样,也就对应着我们重启游戏后原先的地址已经不再存储我们想要的变量。而如果我们不想每次都重新搜寻地址的话,就必须找到我们的基址,一层一层的顺着指针找到我们想要的地址。
偏移
我们大多数需要修改的数据都存放在一个结构体内。而一个结构体存放数据都是在地址上相邻的。
例如我们需要访问二叉树根节点的左节点的data值。
就需要先用指向根节点的指针找到根节点的结构体,再访问该结构体内的lchild指针,到达*lchild 所指向的结构体,这时,我们就可以访问目前结构体(根节点的左子树)的data值了。
反应在ce上,我们所做的就是:
基地址->根节点的结构体起始地址
根节点的结构体+偏移 ->结构体内的lchild指针元素 (基址+偏移1)
结构体内的lchild指针元素->根节点的左子树的结构体起始地址
根节点的左子树+偏移 -> 根节点的左子树结构体的data元素 (基址+偏移1+偏移2)
可以理解成偏移就是取结构体内的不同元素。