写作 by 其实这就是寂寞
文章很长,知识的获取总是不容易的,请耐心阅读,不断尝试。
Chapter 1 数值
内存地址和数值的读写原理和规则
首先需要知道的,电脑的核心:CPU本质上就是一个非常复杂的大规模集成电路的集合体,但无论它的设计原理是多么复杂,它的本质还是电路。电路的特性就表现在于通电和不通电两种不同的开合状态,所以人们就用二进制数来表示电路的状态,0表示不通电,1表示通电。在电脑中无论是命令的指令,还是存贮的数据,它们的本质都是长串的二进制数值的。
这里特别要从二进制说起,如果只是想弄一些简单的数值修改,这里大概了解一下就可以了,但如果想对程序指令或者是逻辑判定上的修改,对二进制数的熟悉和理解则是非常必要的。
二进制数故顾名思义就是逢二进一的数,二进制数每一位上除了1就是0,用二进制数正好可以表示电路的开合状态。但二进数位数多了以后就很难一眼看出其数值的大小,所以就引入了16进制,用16进制数来表示二进制的大小,为什么要用16进制,而不是日常使用的十进制数呢?这里分别用二进制、十进制和16进制来表示0~15这16数:
0000 0001 0010 0011 0100 0101 0110 0111(二进制)
0 1 2 3 4 5 6 7 (十进制)
0 1 2 3 4 5 6 7 (16进制)
1000 1001 1010 1011 1100 1101 1110 1111(二进制)
8 9 10 11 12 13 14 15 (十进制)
8
然后把以上二进制数中不同位为1,其它位都为0的数挑选出来,如下:
二进制 十进 16进制
0001 1 1
0010 2 2
0100 4 4
1000 8 8
然后把上面的数值全部相加,结果正好就是
1111
把上面这些4位二进制数想像成一个4线电路,比如说手柄上的四个按键:[Y]、[X]、[B]、[A]。当单独按下[A]键时,电路的状态是0001,[B]键0010、[X]键0100、[Y]键1000,同时按下[B]+[A]键时为0011,同时按下[Y][X][B][A]时则为1111。
由上面的数表可以看到4位二进制可以表示16种不同的状态,如果用日常使用的10进制则必须用1~2位数才能表示这4位二进制,这样很不方面,所以用A~F这几个字母补足十进数不足的部分,这样用1位16进制数就正好和4位二进制数相互对应,可以完美地互相转换了。
可以看到一个二进制数,当每一位上的数值为1时,从低位到高位,其大小正好是
2^0(2的0次方)、2^1、2^2、2^3、……、2^(N-1)(N=位数)
然后再把所有位数为1的结果相加,比如:
1010这个二进制数,第2位和第4位为1,那么它的大小就是2^(2-1)+2^(4-1),结果就是2+8=10(十进)或者A(16进)。
上面这是二进制数值大小的简单算法,然后是二进制数的表示范围,由上面可以看到4位二进制数的表示范围是0~15或者是0~F这16个数或者是16种状态,而16正好是2^4(2的4次方),所以一个二进制数能够表示多少种状态,就是2^N(N=最高位数)。
一个8位二进制数能够表示的范围就是2^8(2的8次方),也就是256种不同的状态或者数值,用16进制表示就是00~FF(FF=10进的255),这就是现在我们所使用电脑上的最小指令单位,同时也是最小存贮单位:字节(byte)。
8bit(位)二进制数=1byte(字节)2位16进制数,比如00~FF
16bit=2byte=1word,比如00 00~FF FF
32bit=4byte=dword,比如00 00 00 00~FF FF FF FF
所以我们在修改工具上看到的那些16进制数,其本质都是由8位二进制数组成的。另外,
2^10byte=1024Kbyte
2^20byte=1024Mbyte
2^30byte=1024Gbyte
2^40byte=1024Tbyte
1byte(8bit)的数值表示范围,也就是00~FF的表示范围,从上面已经应该很熟悉了,是0~255,这是无符号整数。
对于有符号数,则是用00~
2byte(16bit)数值00 00~FF FF的无符号数值范围0~65535
有符号数,正数:00 00~
4byte(32bit)数值00 00 00 00~FF FF FF FF无符号数值范围0~4294967295
有符号数,正数:00 00
00 00~
负数:80 00 00 00~FF FF FF FF(-2147483648~-1)
内存地址:也就是数据所在内存中的位置。常用的32位系统中,寻址指令的最大范围也是32位。而寻址指令既需要正向寻址,也需要逆向寻址,所以必须用有符号数来表示内存地址。而内存地址本身是物理存在的,不存在负数,所以在32位系统中寻址指令的最大寻址范围是00 00 00 00~
由于寻址指令是顺着内存地址来读写的,所以2byte/4byte数值的读取都是和实际数值反过来的。比如在内存地址0x400000~0x400001这2个字节的位置处存着10进制数10000,10000的16进制数是27 10,其中27为高位,10为低位。但寻址指令是顺着内存地址来访问的,所以在读写时会先访问地址0x400000,然后再访问地址0x400001,为了让指令先访问到数值的低位,将27 10反过来变成10 27,这样指令就会先读写数值的位低,再读写高位了。再比如4byte的10进制数100000,16进制为00 01
以上所列举的无论是数据还是内存地址都只属于整数的范围,另外在电脑中所使用的还有浮点数的表示方法。
浮点数可以表示-∞~+∞范围内的一切实数,其构成原理和运算方法比较复杂,这里就不详述了,有兴趣的可以自己百度一下相关资料。这里只是简单的介绍一下:
和普通整数不同,浮点数由三部分组成,符号位、指数部分和有效数字部分。
浮点数的二进制最高有效位为符号,0表示正数,1表示负数。
指数,也称为阶码,表示数据以2为底的幂。
有效数字,表示数据的有效数字,反映数据的精度。
浮点数由不同精度可分为:
float(单精度)32位(4字节),其中符号1位,指数8位,有效数字23位。
double(双精度)64位(8字节),符号1位,指数11位,有效数字52位。
long double(高双精度)80位(10字节),符号1位,指数15位,有效数字64位。
可以看到浮点数最少需要占用4个字节,即使是像1.00这样简单的数,不过现在的游戏中一般用到的也只有32位单精度浮点数。浮点数在内存中的读写方式和整数一样,顺着内存地址是从低位到高位存放的。比如浮点数1.00用16进制数表示为
===============================================================================
汇编原理
首先需要知道什么汇编,在电脑中存贮了很多各种各样的文件,其中只有很少一部分文件是可以在操作系统中直接执行的,其他文件都只能被这些可执行文件读取或调用。在可执行文件中最常用也是最重要的一种就是扩展名为.exe的文件。
在可执行文件中存贮的是可以直接被处理器识别、运行的指令代码,这些指令代码是在设计处理器时就内置在处理器之中的,不同架构的处理器,其内置的指令集是不同的,个人电脑中最常用的就是x86指令集、SSE指令集等等,我们所使用的CPU都支持这些指令集。
这也是为什么在电脑的操作系统中不能够直接读取运行其他游戏机的游戏,因为游戏机处理器的架构和个人电脑处理器的架构是不同的,其内置的指令集自然也是不相同的,所在在电脑上玩游戏机游戏就必须通过模拟器,模拟器的工作原理就是将游戏主机处理器上的指令转换成能够在个人电脑处理器上运行的指令。
无论是哪一种处理器指令,本质就是一串串8位或8位倍数的二进制机器指令,除非是这些指令是设计者,一般人即使把它们都转换成16进制,也很难直接从这些眼花缭乱的16进制数中看出其所代表的含义的,因为每一条指令之间是没有分隔标志的。这样即使是非常专业的程序员,也无法直接用这些指令集编程来设计调试程序了。
于是最早的编程语言,汇编语言就出现了。汇编语言就是将处理器中内置的指令集,翻译成能够让一般人都看得懂的程序语言,所以不同架构的处理器,其汇编语言是不相同的。
汇编语言的本质就是处理器指令,它非常简单高效,因为每一条处理器指令所做的事情都很单一,而且每一条指令都是直接控制处理器的工作,所以通常用汇编语言编写一些简单的小程序。但正因为每一条指令所做的事情都很单一,要构成结构复杂的大型程序,反而就会变得很麻烦了,直接用汇编程语言定义函数和过程是非常复杂的。于是各种高级编程语言就出现了,比如C++等等,这些编程语言可以很简单的描述一些复杂的逻辑运算,很方便地定义、调用各种函数和过程。但人容易描述和定义的函数、过程,最终还是要编译成处理器能够直接运行的机器指令。高级语言一但编译成处理器指令,这个过程通常是无法逆转的,也就是可执行程序一般是无法还原到C++源代码。
相对于其它高级编程语言,处理器指令是也就是处理器“看得懂”的语言,所以称为机器语言,而将处理器指令翻译成汇编指令,这个就叫做反汇编。因为汇编语言本质上就是人看得懂的处理器指令,所以了解汇编,就要先了解处理器的工作原理。
---------------------------------------------------------------------------------
首先需要了解寄存器,寄存器是CPU的重要组成部分之一,它是处理器中的高速存贮部件,虽然空间很小,但读写速度却远远在内存之上,它通常用来暂存指令运行时的数据和地址。寄存器不同于内存,每个寄存器都是唯一的存在,它只有名称,没有地址,所以寄存器在读写数据时不用像在内存中那样把数值的高低位互换,而是保持数值本来的高低位顺序读写。
通用寄存器分为:
数据寄存器EAX(累加)、EBX(基址)、ECX(计数)、EDX(数据),虽然名称不同,但这四个寄存器是可以相互通用的,即可以存数据,也可以存地址,每个寄存器长度为32位(4字节)。
以EAX为例,EAX(32bit)寄存器又包含AX(16bit)寄存器,AX寄存器中又分为AH(高8位)和AL(低8位),EBX、ECX、EDX的结构和EAX相同,如下所示:(横线为寄存器包含范围)
EAX-----------------------------------------------------
AX----------------------
AH------
11111111 11111111 11111111 11111111(32位二进)
FF FF FF FF(4字节16进)
变址寄存器ESI(源变址)、EDI(目标变址),这两个寄存器通常用来存取地址,每个寄存器长度也是32位(4字节)。其中以ESI为例,ESI(32bit)又包含SI(16bit),EDI的结构与ESI相同,如下所示:
ESI-----------------------------------------------------
SI----------------------
11111111 11111111 11111111 11111111(32位二进)
FF FF FF FF(4字节16进)
指针寄存器EBP(基址指针)、ESP(堆栈指针),这两个寄存器用来存取地址指针,其结构和ESI、EDI相同,EBP(32bit)包含BP(16bit),ESP(32bit)包含SP(16bit)。
控制寄存器EIP(指令指针),这个寄存器专门用来存取指令的地址。不同于以上其它寄存器,EIP不是用传送指令来赋值,而是由处理器根据当前指令地址和指令长度来决定下一条指令所在的地址,或者由跳转指令来决定下一条指令的地址。
标志寄存器FLAGS(32bit),作用是存贮当前指令运行的状态、控制、系统标志,其中主要表示状态标志的位如下:
32 ... 11 10 9 8 7 6 5 4 3 2 1 0(二进制位数)
OF DF SF ZF AF PF CF(每一位的含义)
其中:
CF 进位标志 运算结果最高位进位借位 无=0,有=1
PF 奇偶标志 运算结果低八位一的个数 奇=0,偶=1
AF 辅助标志 运算结果低四位进位借位 无=0,有=1
ZF 零标志 运算结果是否为零 否=0,是=1
SF 正负标志 运算结果是否为负 否=0,是=1
OF 溢出标志 运算结果是否超出范围 否=0,是=1
条件赋值、跳转指令根据FLAGS的结果状态来决定如何赋值或转换程序。另外:
DF 方向标志 在串指令中DF=0时ESI/EDI逐次递增,DF=1时ESI/EDI逐次递减。
--------------------------------------------------------------------
段寄存器
段寄存器分为代码段码寄存器CS,数据段寄存器DS,堆栈段寄存器SS,附加段寄存器ES。
段寄存器是为早期16位CPU而设的,因为16位CPU的通用寄存器也只有16位(AX、BX、CX、DX、SI、DI、BP、SP、IP),而16位寄存器只能处理16位的地址,这样会使得指令的寻址范围变得很有限,而段寄存器的作用就是用来辅助处理器寻址。比如SS为堆栈段地址,处理器可以把SS中的地址当做32位地址的高16位,把SP中的栈地址看做低16位,用这种方法来实现32位地址的访问。不过由于现在的处理器和通用寄存器都已经扩展到了32位,所以段寄存器就变得很少使用了,这里了解一下可以了。
---------------------------------------------------------------------------------
堆栈
堆栈是电脑系统中非常重要的组成部分,堆和栈是两个完全不同的概念,这里主要讲解一下栈。栈是由高级语言在编译成可执行程序时自动定义的一块特殊的连续的内存区域,专门用来存取程序在运行时各种函数和过程的变量和参数。栈虽然也是内存的一部分,但它和存取普通数据的内存区域不同。普通内存区域可以随意指定存取数据的目标地址,而栈的数据则有着特殊的存取规则。
栈的存取规则
比如程序将内存地址0x10000~0x12000这一块区域定义为栈,则低地址0x10000为栈顶,高地址0x12000为栈底。寄存器ESP中存放着栈指针,在栈为空也就是栈中无数据时,ESP指向栈底方向的第一个空栈,也就是0x12000。如下所示:
0x10000 栈顶
0x.....
0x.....
0x.....
0x12000 栈底(空栈) <- ESP
在32位程序中,栈的存取单元也是32位,也就是4字节。
数据在入栈时,会自动存放到ESP寄存器所指向的地址,然后ESP中的指针向栈顶方向移动一个存取单元,也就是4字节。
比如现在要将
0x10000 栈顶
0x.....
0x.....
0x11FF4 空栈 <- ESP(第三数据入栈后) 入/\
0x11FF8 1E <- ESP(第二数据入栈后) 栈|
0x11FFC 14 <- ESP(第一数据入栈后) 方|
0x12000
数据出栈,也就是释放栈空间时,其过程则和入栈时完全相反。比如ESP指针地址为0x11FF4,现在要释放栈中
0x10000 栈顶
0x.....
0x.....
0x11FF4 空栈 <- ESP(出栈前) 出|
0x11FF8 空栈 <- ESP(第一数据出栈后) 栈|
0x11FFC 空栈 <- ESP(第二数据出栈后) 方|
0x12000 空栈 <- ESP(第三数据出栈后) 向V
在程序中通常包含着大量的函数和过程,而每个函数和过程又包含有各种参数和变量,在程序执行的过程中,每时每刻都会有大量的数据在进行着入栈、出栈的操作,栈中的数据每时每刻都在以极高的速度更新变化着。栈的这个特殊性使得栈中的数据是不可以去手动修改它,更不能将数值锁定的。因为只要将栈中的数值锁定,哪怕是只锁定一瞬间,在这一瞬间程序运行运算的所有函数和过程都会以这个锁定的值为变量或参数,这显然会使造成函数的运算错误,如果是过程的地址变量,则会造成程序访问到错误的地址,程序马上就会崩溃。要知道一条处理器指令只占几个(1~10)字节,以现在处理器的运算速度,1秒钟内能够运行的指令数量可是天文数字。另外当栈中已经存满数据时,再次执行入栈指令时,ESP指针就会超出栈顶范围,而栈中无数据时,再次执行出栈指令时,ESP指针就会超出栈底范围。像这样的情况都有可能使得存贮在栈以外的其它有用数据被覆盖掉,不过这是程序的设计者才需要注意的问题。修改的话就不用担心这个问题,因为栈的范围在程序编译时就已经规定好了。
了解栈的工作原理,主要是为了根据汇编指令找出函数参数或变量的来源,然后修改数据的来源,使得函数经运算后达到自己想要的结果。
--------------------------------------------------------------------------------
浮点寄存器
和普通寄存器不同,浮点寄存器是FPU(浮点处理器)的组成部分,主要由8个通用数据寄存器,和几个专用寄存器(状态寄存器、控制寄存器和标志寄存器)组成。
8个浮点数据寄存器,编号FPR0~FPR7,每个寄存器的长度都是80位(10字节),当内存中的浮点数存入浮点数寄存器时,不管原来的精度是多少,都会被自动转换成80位精度,从浮点寄存器中读出数据写回内存的时候则会根据指令转换成指定的精度。
8个浮点数据寄存器被设计成首尾相连的类似栈的形式,它的存取方式也和内存的栈相似,只不过浮点数据栈的首尾是可以相互循环的,不像内存栈会出现超出栈范围的问题。和普通内存栈的操作相反,内存栈是数据先入栈,然后再移动指针,而浮点栈是先移动指针,然后将数据入栈。出栈时也是一样,内存栈是先移动指针再将数据出栈,而浮点栈是数据先出栈再移动指针。浮点栈顶指针为ST(0),指针总是指向最后入栈的数据。如下所示:
FPR0 数据 <- ST(0)
FPR1 数据 <- ST(1)
FPR2 数据 <- ST(2)
FPR3 数据 <- ST(3)
FPR4 数据 <- ST(4)
FPR5 数据 <- ST(5)
FPR6 数据 <- ST(6)
FPR7 数据 <- ST(7)
这是当前寄存器的状态,然后又执行一条由内存写入寄存器的指令(注意内存和寄存器之间的数据传送都是指的内存<->栈顶指针ST(0)所指向的寄存器,寄存器之间的数据传送则是ST(0)<->ST(i)的形式),其结果如下:
FPR0 数据 <- ST(1)
FPR1 数据 <- ST(2)
FPR2 数据 <- ST(3)
FPR3 数据 <- ST(4)
FPR4 数据 <- ST(5)
FPR5 数据 <- ST(6)
FPR6 数据 <- ST(7)
FPR7 新数 <- ST(0)
然后再有数据由内存写入寄存器,则会变为:
FPR0 数据 <- ST(2)
FPR1 数据 <- ST(3)
FPR2 数据 <- ST(4)
FPR3 数据 <- ST(5)
FPR4 数据 <- ST(6)
FPR5 数据 <- ST(7)
FPR6 新数 <- ST(0)
FPR7 次新 <- ST(1)
数据由寄存器写回到内存时,栈顶指针ST(0)的移动方向则是和写入时的方向相反。
浮点标志寄存器
和普通的标志寄存器FLAGS相似,浮点寄存器用来表示浮点数的状态,所以也是用纯二进数来表示(共16位),其中C0~C3这4位标识记录的是运算结果的状态。C0位于寄存器状态值二进数的第9位,作用相当于普通标志寄存器FLAGS中的CF。C1是位于第10位,作用相当于OF;C2位于第11位,作用当前于PF;C3位于第15位,作用相当于ZF,其中C0、C2、C3三个状态位主要用于判定数值的比较结果。由于条件跳转指令都是以通用寄存器中的FLAGS寄存器的状态为准。所以在判定浮点数值比较结果时,一般都会将浮点标志寄存器的16位二进制数传送到AX寄存器,然后再测试AX寄存器中的数值。
---------------------------------------------------------------------------------
SSE寄存器
SSE寄存器由8个128位寄存器XMM0~XMM7组成,这些寄存器专门由SSE/SSE2/SSSE3指令使用,不过CPU必须能支持SSE指令。SSE寄存器主要也是用来存贮和运算浮点数(3D图形运算),不过SSE寄存器的空间更大了,效率更高,每一个寄存器单元可以同时并列存贮和运算2个双精度浮点数或者是4个单精度浮点数。和普通的通用寄存器相同,SSE寄存器可以随意指定,比如将某内存数写入XMM0,或是将XMM2写回内存,或是将XMM1中的数值传送到XMM3,而不必像上面的浮点寄存器那样按指针排队存取。
=================================================================================
以上就是关于内存地址、数值和汇编原理的大致理论知识,也许看完后会觉得汇编好复杂啊,指令什么的还没开始讲,光寄存器、栈什么的就这么多。其实也没有想像的那么难,只要基本了解了这些寄存器和栈的用法,汇编指令本身是非常容易理解的,因为每条汇编指令本身的功能都是非常简单的。游戏修改的主要难点还是查找有效数据所在的地址,只要地址找到了,汇编什么的就不是什么难事了,除非是要改什么非常复杂的判定。
这是一款非常强大功能非常全面的内存修改工具,尤其是它的反汇编和指令追踪功能。
官方网站地址:http://www.cheatengine.org/
目前最新版本为6.1,是6.0的改良版,之前的稳定版本为
首先从官方上下载CE的安装程序,然后安装CE,在安装选项最后会有一个其他软件的推广,选择不要安装就行了。
安装好CE后,第一次运行CE,会提示是否运行教程程序,这个教程是CE作者做的一个小程序,用CE主程序追踪这个教程程序,可以根据教程程序的提示和数值变化,然后用来CE查找追踪,按提示正确修改查找的结果后,教程会自动跳转到下一项。可惜这个教程是英文的,看得懂的可以自己试试。
这里跳过教程,直接运行CE主程序,WIN7系统运行程序时会提示需要管理员权限,点[是]。程序运行后弹出CE主窗口,第一次运行,打开菜单[Edit]中的[Settings],弹出设置窗口。
主要是将[General settings]常规设置栏里面的[Save windows positions]保存窗口位置勾选上,方便之后使用。另外[Update interval ... ms]内存查看窗口的数据更新频率,[Found address list update interval ... ms]地址列表区的数据更新频率,[Freeze interval ... ms]锁定内存数据时的写入频率,这三项的单位都是ms毫秒,1000ms为1秒,数值越小,更新频率越高。其余选项看得懂的可以根据自己的使用习惯设置一下,看不懂的就默认设置不管就行了。
然后可以看到菜单左下方有三个图标,其中第一个被彩色框选中的计算机图标,这个和菜单中的[Process]是同一个功能,就是选择要追踪的目标进程,也就是游戏程序。一般目标进程就是执行游戏的那个.exe文件,不过也有少数例外,比如大航海时代4加强版运行的程序是DK4PK.exe,但这只是个启动程序,实际的游戏进程是DK4PK.ICD。还有魔兽争霸3原版和资料版冰封王座的启动程序分别是Warcraft III.exe和Frozen
Throne.exe,但实际的游戏进程都是那个没有图标的war3.exe。不过这只是很少数的情况,一般来说,运行游戏的那个.exe就是游戏的程序进程,除非在这个进程里面什么都搜索不到,那就要看看是不是游戏目录中的其他文件了。
================================================================================
下面说一下数据的查找,这是修改游戏非常重要的一环,找到了有效的数据才有可能追踪程序指令,才能分析程序指令的作用。
在搜索数据前,我们要限定搜索的内存范围,默认范围为[Usemode]00000000~7FFFFFFF,这里可以将范围改成00400000~7FFFFFFF,因为游戏数据一般都会存到程序指令之后的,而程序指令是从00400000开始,而这之前的地址一般会用于栈或其他用途。可以通过地址范围旁边的[Show]查看内存的使用情况(CE6.1版在内存查看窗口的菜单[View]下的[Memory regions])。缩小搜索范围可以有效的提高搜索效率,当然必须对此非常熟悉,否则范围限定得太小也有可能什么都搜索不到,这个就要看个人经验自己掌握了。
接着可以看到主界面上有[First Scan]([New Scan])首次搜索(重新搜索)和[Next Scan]继续搜索两个按键,然后下面有[Scan type]和[Value type]这样两个选项,一个是搜索方式,一个是数据类型。在首次搜索时,[Scan type]搜索方式会有以下几种选择:
[Exact
Value]具体数值的搜索,也就是每次搜索输入的都是具体的数值。而有些时候我们往往不知道游戏里的数值到底是多少,这个时候就需要使用模糊搜索,模糊搜索的方式有:
[Bigger
than ...]大于输入数值,比如不知道要搜索的数值到底是多大,但可以肯定是正数,那么就输入0,也就是初始搜索内存中大于0的所有数。
[Smaller
than ...]小于输入数值,比如不知道要搜索的数值是多大,但可以肯定不会大于10000,那么就输入10000,这样也可以起到提高搜索效率的作用。
[Value
between ...]不知道数值大小,但可以肯定这个值在某值~某值之间的情况使用。
[Unknown
initial value]完美不知道要搜索的数值会是多大,那么就是这个初始搜索。
初始搜索完成后,然后回到游戏中继续游戏,使我们想要搜索的数值在游戏中发生变化,然后再回到CE界面进行再次搜索,再次搜索又分为以下几种方式:
[Exact
Value]同首次,再次搜索具体大小的数值
[Bigger
than ...]同首次,未知数值小,但可以肯定是大于某值
[Smaller
than ...]同首次,未知数值小,但可以肯定是小于某值
[Value
between ...]同首次,未知数值大小,但可以肯定在某值~某值之间
[Increased
value]未知数值大小,但可以肯定比上次搜索时增大了
[Increased
value by ...]同上,但可以肯定比上次搜索时增大了具体多少或百分比
[Decreased
value]未知数值大小,但可以肯定比上次搜索时减小了
[Decreased
value by ...]同上,但可以肯定比上次搜索时减小了具体多少或百分比
[Changed
value]未知数值大小,但可以肯定和上次搜索的结果发生了变化
[Unchanged
value]未知数值大小,但可以肯定和上次搜索的结果没有变化
[Same as
first scan]未知数值大小,但可以肯定和首次搜索时的结果相等
在模糊搜索时,灵活地交替使用以上几种搜索方式,可以有效地提高搜索效率。
然后是搜索时规定的数值类型[Value type],分为以下几类:
[Binary]二进制数,数值输入方式又分为[bits]二进和[Decimal]十进两种
[Byte]1字节整数,也就是限定搜索的数值范围在00~FF之间
[2 Byte]2字节整数,限定搜索的数值范围在0000~FFFF之间
[4 Byte]4字节整数,限定数值范围在00000000~FFFFFFFF之间
[8 Byte]8字节整数,一般不会用到,以后64位的游戏可能会用到
以上整数搜索时默认为十进数输入,也可以勾选上输入框前的[Hex]变为以16进数输入
[Float]4字节单精度浮点数,这个比较常用
[Double]8字节双精度浮点数,一般不会用到,以后64位的游戏可能会用到
[Text]文字字符搜索,默认为ANSI编码,如果是Unicode码,勾选上[Unicode]
[Array of
Bytes]数值串搜索,默认16进,以字节为单位,输入顺序和内存存贮顺序相同[Custom]自定义,一般不会用到这里主要需要注意整数搜索的区别,比如说要搜索数值01,那么选择[Byte]时搜索的就是01,而如果选择的是[4 Byte],那么搜索的就是01000000。这个要特别注意,如果要搜索的数值在内存中只占用了一个字节,也就是01后面存贮的是其他数值,那么用[4 Byte]就无法搜索到了。
在搜索地址时,一般需要进行好几次搜索才能筛选出最终结果。比如现在要搜索某游戏中金钱的存贮地址,首先在游戏中记下当前金钱的数值,然后在CE中选好搜索类型,金钱是可见数值,也就是具体数值了。比如现在金钱数是1000,那么我们选择[2 Byte](因为2字节整数的大小范围是256~65535或是+256~+32767,1000正好在这个范围之内),然后搜索1000,这个时候CE的结果框中会出现很多地址,显然我们还是无法知道哪个地址存的是金钱。回到游戏中用掉一些金钱买道具,比如说剩下500,然后回到CE界面中,用[Next Scan]再次搜索500,这时候结果框中的地址变少了,但还是无法确定是哪个地址。再回到游戏卖掉一些道具使金钱增加,比如现在是700,然后再回到CE搜索700。经过几次反复搜索,在CE的结果框中应该会只剩下很少的地址了,最好的情况是只剩下一个地址,那么这个地址就肯定是存贮金钱的地址了。但有的时候也会出现无论搜索多少次,都会留下好几个结果,那么就只有把这些结果全部都添加到地址列表中,然后一个个去修改它们,看到游戏中有没有实际变化,修改后游戏中有实际变化的那个地址肯定就是了。
在搜索具体数值时,最后一般不会留下太多结果,一个个试一下就能试出来哪一个是正确地址。但有的时候也可能出现,无论怎么搜都会留下很多个结果,很难一一验证,还有可能出现最后没有搜索结果,一个正确的都没有的情况。这个时候就要考虑一下搜索的思路,或者是换用模糊搜索来查找地址了。比如在有些游戏中,内存中存取的数值和游戏中看到的是不一样的情况,比如太阁5游戏中看到的金钱是1000贯500文,而内存中是10005。魔兽争霸3中的黄金和木头,看到的是1000,内存中则是10000。鬼泣4的华丽得分,游戏中看到的是2000,而内存中则可能是19998.5,也可能是20004.5,比游戏中看到的大10倍,然后4舍5入才是游戏中看到的结果。还有在一些模拟器游戏中,金钱是2000,内存中就直接是16进的20 00,一般的应该是D0 07等等。对于这样游戏中看到的数据和内存中不同的情况,就只能是用模糊搜索来找大小找变化了。再就是搜索时数值类型的选择,在搜索具体数值时,整数就尽量选小范围,比如搜索的数值肯定在0~255,那就选[Byte],选[4
Byte]就有可能找不到。但是如果能够肯定要搜索的数值是占用4字节,比如知道某游戏的金钱最多可以达到999999,那么即使现在只有5块钱,也用[4
Byte]来搜索,这样效率会更高。
然后就是整数和浮点数的区别,比如在鬼武者、KOEI无双系列游戏中的时间、体力和魔力都是以整数形式存取的,在鬼泣3中时间是用整数存取,体力和魔力则是用浮点数存取的,而在鬼泣4中的时间、体力和魔力全部都是以浮点数形式存取的。所以在不知道一个游戏是以什么形式来存取数据,就只能是分别用两种形式都模糊搜索一次了。
总之在搜索数值的内存地址时,有具体数值的直接搜索具体数值最方便了。但如果具体数值搜不到的,就换用模糊搜索。模糊再搜索不到,就考虑会不会是浮点数,整数搜不到的就换浮点搜,浮点搜不到的就换整数搜索。怎么搜都搜不到的,就要考虑一下自己的搜索思路是不是不正确了,比如鬼泣4中的蓄力攻击,是以浮点数来计算按键的时间长短,通常值为0,然后数值随着按键时间增大,放开按键后,数值又变回0,所以这个用模糊搜索增大减少就很容易找到。但是比较特殊的是NERO的X字斩蓄力,这个是在开始蓄力的时候初始60.0(1秒),然后随着按键时间,这个值会不断减小,减到0时则蓄力蓄满。那么这个在事先不知道的情况下,模糊搜索按键时增大就怎么都搜索不到了。这种情况就完全只能靠自己经验的积累和地想像力了,想像如果你的游戏的制作者,你大概会以什么方式来存取和运算这些游戏数据。
最后就是验证搜索的地址结果是否正确有效了,一般在找出最后的结果后,修改数值,游戏中会得到相应的改变,那么这个地址就肯定无误了,但有的时候会碰到很难验证结果是否有效的情况。比如鬼泣4的任务换人,这个是由游神最先修改成功的,要说这个搜索难度大,不如说验证搜索结果的难度更大。在游神公开修改器之前,我只在血宫中修改换人成功,虽说血宫中改换人也没有什么意思,因为血宫本来就可以直接选人,但是通过修改换人成功,知道了DANTE的人物初始值是00,NERO的是01。不过任务的人物初始和血宫的人物初始原理是完全不同的,血宫是通过选人判定,然后根据判定由寄存器入栈,然后由栈赋值,用汇编很容易修改判定的结果。但是任务流程是不能选人的,所以我就想不同关的人物不同,人物初始肯定和关数有关,结果又追踪关数为条件的判定,还是没有结果。后来游神放出游戏器,拿来一看,吐血……确实是和关数有关,但人物的初始值指令不是由关数来判定,而是本身就是关数一起初始的。更吐血的是用指令追踪找到这个值的内存地址,然后把地址添加到地址列表后修改这个值,比如进NERO关卡,初始值为01,然后手动改成00,结果一进任务程序就出错了,晕!……后来想起来游神之前说过要锁定什么时的,然后就把值一直锁定成00,再进NERO任务,果然就变成但丁了,我还想这是什么情况?直接改就不行,锁定就可以……然后再重进一次,结果数值锁定着,程序还是出错了,再吐血……后面连着试了好几遍发现需要以很高的修改频率锁定数值才不会出错。这才知道游神为什么之前一直都没有将修改方法公布出来,这个就算公布出来也会让不少人吐上不少血吧……不过用汇编指令修改就很简单了,追踪修改这个地址的指令,可以找到一共20关的初始指令,然后把初始值01的改成00,把00的改成01就可以了。通过指令追踪还可以找出为什么内存修改会出错的原因,用地址读取追踪,可以看到这个地址的数值在被初始后,会立刻被另一个指令读取,然后根据读取的数值来初值人物的操作控制和状态等数据,然后游戏中另一条判定会根据这个值来判定如何调用这些控制数值。所以在数值初始时,比如初始但丁,程序就立刻在内存中初始了但丁的操作和状态数据,这个时候再把人物值改成NERO,结果进任务后,程序又会按照NERO的数据来读取,所以程序就马上出错了。任务M1无法成功替换成但丁也是这个原因,因为M1会强行初始NERO教学的操作数据,用但丁的判定去调用NERO的数据所以就出错了。同时修改初始和操作的两条判定,也可以简单实现任务换人,不过缺点就是如果在游戏中还原判定的话,游戏也会马上出错,所以还是修改初始赋值,而不是修改初始和操作判定的方法更安全可靠。当然这也是在已经验证了地址确实有效,才能找出来程序出错的原因,如果没有经过确实的验证,只是一改就程序出错,那也无法知道这地址到底是干嘛用的。
一下子把这个说了这么多,看来我到现在还是非常纠结于这个问题啊- =毕竟像修改这种不是会随时发生变化的状态数值却需要锁定才能验证搜索结果地址是否有效的,直接修改就直接出错的情况还是第一次碰到,这个就算是我找到了地址,也没有办法去验证是否正确吧- =这也充分说明了经验积累的重要性啊。嘛,本来搜索内存地址这个就是靠经验+想像+运气,特别是在游戏中看不到具体数值的数据,只能靠经验和想像去推测内存中会以什么方式来运算和存取这些数据,再就是加上一些运气了。
在搜索数据地址的时候,运气也是非常重要的,有了充分的经验和想像再加上运气,经常会意外地找到一些本来不知道怎么找的数据。主要也是因为CE的内存查看窗口中的数值可以即时地反应出内存中数值的变化。比如最开始改出鬼泣3完美防御,一开始根本就不知道改这个要怎么找数据,而是在找RG的三星槽时无意间发现的。虽然不知道RG的三星槽的具体数值,但是通过模糊找增减变化很容易就可以找出来,RG槽每颗星是浮点数3000,满三星是9000,将地址添加到列表后,打开内存查看窗口,转到数值所在的地址,然后一边开着内存查看窗口,一边游戏中游戏,看游戏时内存数值的变化,马上就发现了在三星槽数值的后面紧接着还有一个值,只要在游戏中按下防御键就会变化,不按就不发生变化,非常有规律。于是将这个数值地址也添加到地址列表,然后就可以看到这个值平时是99(也是浮点),当按下防御键后,这个就变成0,然后数值随着按键时间不断增加,于是就将这个值锁定成0,进游戏一试,果然就百分之百是完美防御了。去年鬼泣3改出的很多功能都是这样找到,很多很难想像要怎么找的数据通常和那些很容易找到的相关数据都放在一起,可以一边游戏一边看内存数据的变化是否和游戏的变化非常有规律性,然后将这些数值添加到列表,看这些数据是否真的是根据游戏的操作来有规律的变化,如果是,那么修改或锁定这些数值,看在游戏中会受到什么样的实际影响,来验证这些数据是否就是控制这些游戏状态的。然后不断由找到这些本来不知道该怎么找的数据,就可以不断的积累经验,让其它数据更容易被找出。就好像3代的完美防御、蓄力攻击找出来了,4代也就很容易的找出来,虽然在细节上两个游戏的存取方式并不是完全相同,但是原理和思路是差不多的,鬼武者3的弹一闪也是这个原理,不过判定数值用的是1字节整数。至今为止的所修改出来的功能,因为游戏中的相关处理的相关数据都会放在同一块内存区域,所以像这样一点一滴积累,就可以找出原本很难直接找出的数据。还有根据游戏判定指令,也可以找出一些相关数据的位置。比如鬼泣4中的动作代码,在不知道游戏是如何处理的情况下就很难找出。但是游戏中有些的动作是根据一些很容易找到的数据来判定的,比如说华丽度,在不同的华丽度下,按挑衅键,挑衅动作就不同,所以追踪华丽度值参与的判定指令,就可以找出动作的代码。还有DANTE的拳套蓄力值,魔人状态值,NERO的油门值,都可以判定出不同的招式等等。还有鬼武者3中的空放一闪,要找出被敌人攻击的瞬间的判定,显然是非常困难的。但是找到黑羽织的装备状态,根据是否装备有黑羽织来找到是否出一闪的判定,BOSS战中黑羽织也无法一闪的判定也都在一起。还有空放弹一闪,在按住防键的过程中,可以按出攻击键,也就是可以放出弹一闪的条件判定数值变化也只有一瞬间,直接找到这个数据也是非常困难的。可以根据鬼泣中完美防御的判定原理,先改出百分百且不限时的弹一闪,再根据防住可弹一闪攻击和不可弹一闪攻击时的数值区别,找到判定可放弹一闪条件指令,从而改出可无条件空放。
以上可以说大部分都是经验之谈了,不过这也充分说明了经验积累的重要性,谁也不是一开始就能弄出什么眼瞎的修改,都是一点一滴的积累出来的。很多游戏,在一些处理和判定原理上都是差不多的,虽然在细节上会有一些区别。比如我以前就不知道镜头要怎么改,花生找到镜头的数据地址后,我就通过汇编找到了镜头的参数,最近无聊发现F社空之轨迹系列的镜头控制也是差不多的原理,通过修改可以随意拉远拉近、转动镜头。很多游戏都是这样可以相互借鉴的,特别是在同类型的游戏中,或者是在数据处理和判定的方式上原理相似的。比如鬼泣4的按住跳键走路,其修改思路则是借鉴自FALCOM的空之轨迹。
================================================================================
上面主要是搜索数值查找地址的方法和一些经验之谈,下面开始说怎么用CE的反汇编功能来分析和修改这些已经找到、并且已经验证有效的地址数据。
CE的反汇编功能中,主要分为由数值变化追踪出修改此数值的指令。在地址列表框中已经添加的内存地址中,选中其中要追踪的地址,然后鼠标右键,弹出菜单后选择[Find out what writes to this
address]谁在往这个地址写数值,也就是什么指令在修改这个地址的数值。点[Yes]弹出追踪窗口后,下一次游戏中这个数值再发生变化的时候,追踪窗口就会出现往这个地址写入数值的汇编指令,这条指令一定是往内存地址写入数据的指令,数值的来源可以是寄存器,也可以是直接数值,这是追踪写指令。
然后[Find out what accesses this
address]谁在用这个地址的数值参与运算,这个就是追踪什么指令在根据这个地址的数值来用条件判定了。用这个追踪出的指令型式,通常是直接用这个地址来作条件比较,或者是把此地址的值转送到寄存器,然后由这之后的指令根据寄存器的状态来作之后的判定和处理。注意在用以上这两个功能追踪含指针的内存地址时,会比追踪直接地址时多弹出一个有两个选项的小窗口,这个时候一般都是选第二个。
最后就是根据已经追踪出的指令,在内存查看窗口的反汇编代码区选定想要修改或者追踪的指令,双击是直接修改所选汇编指令。鼠标右键菜单:[Go to address]跳转到指定的地址。
[Replace
with code that does nothing]将此条指令替换成空指令,其程序不运行这条指令,同时这条指令会被添加到指令列表窗(主界面最左下角的[Advanced options],可以在指令列表窗,通过右键[Restore with original code]还原指令)。
[Change
register at this location]改变当前指令的寄存器数值,这个主要是调试用。
[Toggle
breakpoint]当游戏运行到这条指令时,暂停游戏,并显示当前各寄存器的状态,关闭内存查看窗口即可取消暂停。
[Break
and trace instructions]当游戏运行到光标所选的这条指令时,由这条指令开始追踪程序的每一条指令,并在追踪窗口中显示出每一条指令在运行时的各个寄存器状态值。在开始时会提示追踪指令的条数,默认为追踪1000条。这是一个非常重要和有用的功能,分析程序的运算方式和判定原理,基本上就是靠这个功能了。在CE6.0~6.1中,这个功能会以树型结构显示子程序的调用和返回,使得在程序分析或是找到数据的原始来源时变得更加方便。
以上就是所有汇编修改基本需要用到功能位置和说明,至于这些功能怎么用,只能另外专门用实例来讲解了。另外在内存查看窗口的菜单[View]中的[Referenced Strings]为分析当前游戏程序的内存使用结构,对汇编和文件结构比较熟悉的话,这将会是个比较有用的功能。鬼泣4的任务换人用这个功能也可以找到代码的位置,这是后来花生发现的。还有就是在菜单[Search]中的[Find assembly code]查看汇编指令,由当前所选地址开始往后搜索所输入的汇编指令,这也是一个比较有用的功能。一般用得到的也就是这些吧,其它就不一一介绍了,剩下其他那些功能如果会用的话,那水平肯定也不用我来讲了,有兴趣的可以自己慢慢研究了。
Chapter 3 指令
汇编指令有很多,X86指令、X87浮点指令、SSE指令等等,所以这里无法一一列举讲解,只说一下修改游戏时经常会遇到和使用到的指令的用法,如果碰到这里没有列举的指令,可以自己在网上查一下资料,只要了解了汇编指令的工作原理,其他指令应该也是很容易理解的。注意网上的汇编指令资料基本上都是老的16位汇编,就是需要用段寄存器辅助寻址的,而这里所讲的全部都是现在所用的32位汇编指令,寄存器直接支持32位地址,所以在指令的细节上会有一些小小的区别,但是指令的作用都是一样的。
另外这里列举的汇编指令都是PC上用的汇编指令,如果是模拟器游戏,用CE是无法对模拟器游戏进行正确的反汇编的(反汇编的结果并不是真正意思上的主机指令),只能使用模拟器自带的反汇编功能,用过模拟器反汇编的就可以看到,游戏主机上的汇编指令和PC上的是不一样的,因为处理器和存贮单元的结构不同,访问和操作数据的规则自然也不相同。
常用汇编指令
注意:在汇编指令中所有的地址值和数值都是16进制,用符号"[...]"之间的数值来表示内存地址。内存地址处操作数据的长度用BYTE PTR [地址]、WORD PTR [地址]、DWORD PTR [地址]来区别,分别对应表示此地址处的1字节、2字节、4字节数据。
=================================================================================
X86指令
X86指令通指80X86的一系列指令,指令兼容
----------------------------------------------------------------------------
传送指令
MOV 数据传送指令
格式:MOV 目标,源
说明:将源传送到目标,其中目标可以是地址,也可以是寄存器。而源可以是地址,可以是寄存器,也可以是数值。注意:目标不可以是数值,源和目标不可以同时为地址,另外源和目标的数据长度必须相同。
例如:
MOV
EAX,EBX //将寄存器EBX中的数值传送到EAX
MOV CX,DX //将寄存器DX中的数值传送到CX
MOV AL,01 //将数值01传送到AL
MOV
BX,0001 //将数值0001传送到BX
MOV EAX,[
MOV
ECX,[ESI+
MOV [
MOV BYTE
PTR [
//将1字节16进制数01复制到地址0x
MOV WORD
PTR [
//将2字节16进制数2710复制到地址0x
以上两条指令中的BYTE PTR和WORD PTR分别表示1字节和2字节,和原数据的01和2710对应。
再如:
MOV BYTE
PTR [ESI+
MOV WORD
PTR [ESI+
也是如此,源和目标的数据长度必须相同。
------------------------------------
MOVSX 符号填充指令
格式:MOVSX 目标,源
说明:MOVSX和MOV指令相似,也是将源传送给目标,只是源的数据长度要小于目标,不足位用源的符号来填充。
例如:
MOV AL,40 //将数值40传送到AL(8位),40是正数(+64)
MOV BL,80 //将数值80传送到BL(8位),80是负数(-128)
MOVSX
CX,AL //将AL传送到CX(16位),则CX=0040
MOVSX
DX,BL //将BL传送到DX(16位),则DX=FF80
MOVZX 零填充指令
格式:MOVZX 目标,源
说明:MOVZX和MOVSX指令相似,只不过不足位恒定用0来填充。
例如:
MOV
AX,FFFF
MOVZX
AX,01 //其结果AX=0001
------------------------------------
XCHG 数据交换指令
格式:XCHG 目标,源
说明:将目标和源中的数据相互交换,目标和源可以是寄存器,可以是内存,但不可以同时为内存。
例如:
XCHG
EAX,EBX
XCHG
AX,WORD PTR [20401000]
------------------------------------
LEA 有效地址传送指令
格式:LEA 寄存器,内存地址
说明:将有效内存地址数值传送到寄存器。
例如:
LEA ESI,[
可以用如下几条指令来说明LEA和MOV的区别:
MOV
EAX,00400000 //将数值00400000传送到EAX,即EAX=00400000
MOV
ESI,20401000 //将数值20401000传送到ESI,即ESI=20401000
MOV [ESI+
MOV
EBX,[ESI+
LEA
EDI,[ESI+
再例:
MOV
EAX,00000000
MOV
EBX,00000001
LEA
ECX,[EAX+EBX*4] //其结果ECX=00000004,也就是[]内的运算结果。
------------------------------------
PUSH 数据入栈指令
格式:PUSH 源数据
说明:将源数据存入内存栈,地址是ESP指向的栈底方向的空栈,数据入栈后ESP向栈顶方向移动也就是减少4个字节。
例如:
PUSH EAX //将EAX的值存入栈,地址为ESP,数据入栈后ESP=ESP-4
PUSH 01 //将数值01存入栈,地址为ESP,数据入栈后ESP=ESP-4
------------------------------------
POP 数据出栈指令
格式:POP (目标)
说明:将内存栈中的数据移除,操作过程为先将本来指向栈底空栈的ESP向栈底方向移动4个字节,使ESP指向最后一次入栈的数据,然后将此数据从栈中移除,使此处变为空栈。(用目标来接受出栈的数据)
例如:
POP //ESP=ESP+4,然后从栈中移除ESP指向的4字节数据
POP EAX //ESP=ESP+4,然后从栈中移除ESP指向的4字节数据,并用EAX来接收被移除的数据。
注意当栈指令ESP已经指向栈底,也就是栈中数据满时,再使用PUSH指令,ESP就会超出栈顶,将数据写到栈外。当ESP指向栈底,也就是栈中无数据时,再使用POP指令,ESP就会超出栈底,而将栈外数据清除。高级语言在编译成可执行程序时,会提示指令超出栈范围。但程序已被编译后,PUSH和POP指令本身在运行时是不会判断是否超出栈范围的。所以在修改游戏时,是不可能随便在程序中添加或者减少栈指令的,而一般只是修改PUSH指令的源数据。
例如:
PUSH 01
...
MOV
AL,[ESP+4]
MOV BYTE
PTR [20401000],AL
这样一段程序代码,AL的数据来源是栈地址[ESP+4],而ESP总是指向栈底方向的空栈,所以栈地址[ESP+4]中就是空栈下方最后一次PUSH指令入栈的数据。
----------------------------------------------------------------------------
运算指令
ADD 加法指令
格式:ADD 目标,源
说明:用目标数据加上源数据,然后将计算结果传送给目标,并根据运算结果设置FLAGS的各相关标志位。格式要求和MOV指令相同,目标可以是寄存器,可以是内存地址,但不可以是直接数。目标和源不可以同为地址形式。
例如:
MOV
EAX,00000001 //给EAX赋值,使EAX=00000001
ADD
EAX,00000002 //EAX+00000002,结果EAX=00000003
MOV
[20401000],00000004 //将数值00000004存到内存地址20401000处
ADD
EAX,[20401000] //EAX+内存地址20401000处的数值,结果EAX=00000007
ADD
EAX,EAX //结果EAX=0000000E
----------------------------
INC 加一指令
格式:ADD 目标
说明:将目标数据+1,然后将计算结果传送给目标,同时根据运算结果设置FLAGS的各相关标志位,目标不可以是直接数,否则指令没有意义。
例如:
INC
INC BYTE
PTR [20401000]
-----------------------------
SUB 减法指令
格式:SUB 目标,源
说明:结果目标=目标-源,并设置FLAGS相关标志位。
DEC 减一指令
格式:DEC 目标
说明:结果目标=目标-1,并设置FLAGS相关标志位。
MUL 乘法指令
格式:MUL 目标,源
说明:结果目标=目标*源,并设置FLAGS相关标志位,注意目标和源都不能是直接数。
DIV 除法指令
格式:DIV 源
说明:结果EAX=EAX/源,EDX=余数,源不能是直接数,指令不影响FLAGS标志位。
----------------------------------------------------------------------------
逻辑运算指令
AND 逻辑与指令
格式:AND 目标,源
说明:结果目标=目标与源,并设置FLAGS相关标志位。
OR 逻辑或指令
格式:OR 目标,源
说明:结果目标=目标或源,并设置FLAGS相关标志位。
XOR 异或指令
格式:XOR 目标,源
说明:结果目标=目标异或源,并设置FLAGS相关标志位。
NOT 逻辑非指令
格式:NOT 源
说明:结果=源非,源不能是直接数,否则指令无意义,指令不影响FLAGS标志位。
注意以上逻辑指令处理的都是数值二进制位的逻辑关系
AND 目标和源同位上都是1,那么结果为1,否则结果为0
OR 目标和源同位上都是0,那么结果为0,否则结果为1
XOR 目标和源同位上的值相等则结果为0,不相等则结果为1
NOT 结果和源同位的的值正好相反,源位上是1,结果则为0,源位上是0,则结果为1。
其真值表如下:
AND OR XOR NOT
源 0011 0011 0011 0101
目标 0101 0101 0101
结果 0001 0111 0110 1010
AND指令通常用来屏蔽掉数据中的某些位,比如:
MOV EAX,
AND
EAX,0000FFFF
则其结果EAX=
OR指令通常则用来补充数据中的某些位,比如:
MOV
EAX,20401000
OR AL,AC
则其结果EAX=
而XOR指令则用来清空某一寄存器,比如:
XOR
EAX,EAX
因为目标=源,所以异或的结果是数据的每一位都相同,而相同的结果是每一位都变为0。
------------------------------------
条件指令
CMP 比较指令
格式:CMP 目标,源
说明:比较目标和源的大小,CMP指令和SUB作用相似,只是不传送运算结果,而只是根据运算结果来设置FLAGS相关标志位。
这是在游戏程序中很常见的一条指令,常用来判定一些量化的数据。比如在买道具时,当前的金钱比要买的道具的价格少,不好意思,那只能判定为买不起;或者是当前金钱已经达到或者超过了金钱的上限,那么多的钱就只能孝敬CPU了。或者是比较血量,比如血量等于上限,那么就只能看着有好菜不能吃;或者是比较结果血量等于0,那么就只能选择信春哥或者是直接GAME OVER了。
TEST 测试指令
格式:TEST 目标,源
说明:TEST指令和AND作用相似,都是判定目标和源的逻辑与,只不过TEST并不传送运算结果,而只是根据运算结果来设置FLAGS相关标志位。
在很多游戏中,通常用数值的二进制位来表示属性状态,比如是在三国、信野中武将的技能,太阁中人物的卡片,鬼泣3的服装和特典,KOEI无双系列的武器属性,还有一些战棋游戏中习得的魔法或者是能够购买的佣兵种类等等。因为用二进制位来表示状态,一个字节就可以表示8种状态。比如某游戏中,每一个人物都可以习得八种技能,然后假设某一人物已经习得第一、二、五、八项技能,那么用二进制位就可以表示如下:
技能八 技能七 技能六 技能五 技能四 技能三 技能二 技能一
1 0 0 1 0 0 1 1
二进数10010011换算成16进制也就是93,用这一个字节就可以表示当前人物已习得的技能。
假设这一数据存贮在地址0x
MOV
AL,BYTE PTR [
OR
那么这个人物就又习得了第4项技能,因为10010011 OR 00001000的结果是10011011。或者是
MOV
AL,BYTE PTR [
TEST
测试当前人物是否已经得第三项技能,因为100011011 AND 00000100的测试结果是第三位不相等,那么则表示当前人物并没有习得第三项技能。
当然后上面这些只是举一个很简单的小例子,真正游戏中通常都是用含变量的指令来检测,也就是一条指令就可以检测人物的所有技能的习得情况,然后判定当前人物可以使用的技能。
------------------------------------------------------------------------------
直接跳转指令
JMP 直接地址跳转指令
格式:JMP 有效的内存地址
说明:根据内存地址参数,相应修改EIP指令指针,使EIP指向指定的内存地址,这里需要特别解释一下。首先我们已经知道汇编指令只是让我们能够看得懂指令的含义,但它的本质还是处理器可以直接运行的机器指令。JMP指令分为近跳转和远跳转两种,其机器指令分别为:
EB 1E 近跳转 JMP
NEAR 内存地址
E
这两条指令用汇编指令表示都是JMP 内存地址,那么它们有什么区别呢?
首先在EB 1E这条指令中EB表示指令类别(近跳转),1E表示参数(跳转距离),而在
E
短跳转的机器指令参数为1字节,00~
长跳转的指令参数为4字节,00000000~7FFFFFFF表示正向,80000000~FFFFFFFF表示反向。
例如:
00401E5B E
00401E60 XX XX 下一条指令
........
00402000 EB 1E JMP
00402020
00402002 XX XX 下一条指令
00402020 XX XX 跳转的目标指令
可以看到同是汇编指令JMP 00402020,由于指令所在的地址不同,指令的本身也完全不同。
在介绍寄存器的时候我们已经讲到了,EIP寄存器是专门用来存贮当前指令所在的内存地址,所以我们可以看到指令EB1E这里EIP的值应该是00402000。通常情况下,处理器会根据当前EIP的指令地址值+当前指令的长度,来决定下一条指令的位置。也就是如果当前指令是EB,处理器就知道这条指令要占用两个字节,那么EIP+02就是下一条指令的地址;如果当前指令是E9,那么处理就知道这条指令要占五个字节,那么下一条指令的地址就是当前EIP+05。而EB/E9都不是一般的指令,而是跳转指令。处理器是根据当前指令所在的地址+当前指令的长度+跳转的参数,来决定下一条指令所在的位置,所以下一条指令的地址是EIP+02+1E。B
另外,跳转参数是带符号的整数,根据参数的符号不同,指令跳转的方向也不同。
比如:EB 80
我们知道在带符号整数中80是用来表示负数的(-128),所以EB80这是一条反向的跳转指令,其下一条指令的地址应该为当前指令的EIP+02-80。可以由此看出短跳转指令的范围是:
EIP+02-80~EIP+02+
而长跳转指令的范围可以是
EIP+05-80000000~EIP+05+7FFFFFFF
显然长跳转指令可以跳转到32位指令可访问地址范围内的任何位置。
这里还需要补充一下修改跳转指令时需要注意到的问题。比如:
00402000 EB 1E JMP
00402020
00402002 XX XX 下一条指令
00402020 XX XX 跳转的目标指令
我们想要修改0x00402000这里的JMP 00402020这条指令,把它改成不跳转,而是继续运行下一条指令,那么我们就把它改成JMP 00402002,机器指令则会自动相应变成EB 00,如下:
00402000 EB 00 JMP
00402002
00402002 XX XX 下一条指令
但是对于长跳转指令,我们则需要特别注意了。比如:
00401E5B E
00401E60 XX XX 下一条指令
........
00402020 XX XX 跳转的目标指令
我们要把0x401E5B这里改成JMP 00401E60,那么机器指令就会自动变成EB00短跳转指令了。
其结果就变成这样了:
00401E5B EB 00 JMP
00401E60
00401E60 01 00 00
因为处理器会根据当前指令的长度来决定下一条指令的起始地址,五字节指令变成了两字节指令,结果原来指令剩下的后面三个字节被当成了下一条指针,而被剩下的01 00 00这并不是一条处理器指令,所以程序将会在这里无法识别指令而出错。正确的改法应该是:
00401E5B E9 00 00 00 00 JMP FAR 00401E60
00401E60 XX XX 下一条指令
-------------------------------------------------------------
空指令
NOP 空指令
格式:NOP
说明:这是一条指令,但是这条指令什么事情也不做,只是空占一个指令位。
在修改游戏时经常会用NOP指令替换掉不需要或者是不想要执行的指令,比如上面的那条:
00401E5B E
现在我们不需要执行这条指令,那么我们可以空指令NOP把它替换掉,注意空指令只占一个字节,所以我们必须将原指令的每一个字节都用空指令替换掉,否则就会造成余下的字节被当成下一条指令。例如,我们现在将上面那条指令用NOP指令替换,其结果则为:
00401E5B 90 NOP
00401E
00401E5D 90 NOP
00401E5E 90 NOP
00401E
00401E60 XX XX 原来的下一条指令
由上面的示例可以看出,当我们想要用其它指令来替换原来的指令时,最好是用相同长度的指令来替换,如果是用较短的指令来替换原来较长的指令时,那么原来指令多余的字节要用NOP指令全部替换掉,否则程序会无法识别剩余字节的指令。即使是剩余的字节正好也是一条合法的指令,那么这条多出来的指令也还是有可能影响到后面指令的正常运行。在CE中用短指令替换长指令时,会自动提示是否将剩余字节用NOP替换,点[YES]就行了。
由此也可以想到,我们是不可以用一条较长的指令来替换原来一条较短的指令的,因为这样较长的指令多出的字节还会覆盖到下一条指令。这样也是不行的,因为程序经编译以后,每一条指令之间都是逻辑连贯的,无缘无故少运行一条指令,也会将程序的逻辑打乱。除非你可以肯定被覆盖的这条指令是不影响程序的,当然这样的情况很少很少。
CE有专门的插入汇编功能,在内存查看窗口的[Tools]菜单下的[Auto Assemble]项。先将光标停在想替换的指令上,然后在菜单上点选这个功能,CE会自动在内存中找一块未使用的区域,然后用长跳转指令跳到空闲的内存块,将插入的指令写在这里,然后再用长跳转指令跳回到原来的下一条指令处,被之前长跳转指令覆盖掉的指令也会被一并移动到空闲内存块插入代码的最后。嘛,这算是一个专家级的功能吧,了解一下就好了,懂的可以自己试试。
------------------------------------------------------------------------------
子程序调用和返回指令
CALL 子程序调用指令
格式:CALL 有效内存地址
说明:CALL和JMP指令类似,会使程序指令跳转到地址参数指定的位置,只是在跳转之前会先将CALL之后的下一条指令地址存入栈中。所以一条CALL指令也可以把它看作是:
PUSH EIP
JMP 有效内存地址
这样的两条指令。
例如:
CALL
00402020 //调用00402020处的子程序
CALL
DWORD PTR [20401000] //读取地址[20401000]处的4字节地址,然后把结果作为地址,调用结果地址处的子程序。
RET 返回指令
格式:RET (参数)
说明:读取上一条CALL指令运行时入栈的指令地址,使得程序返回到之前CALL指令的下一条指令处,所以RET指令也可以把它看作是POP EIP(用EIP接收上一条CALL指令入栈的EIP值)。
以上是无参数的RET指令,还有带参数的RET指令。比如:
RET 0008
其中参数0008表示栈指针ESP的加算值。比如在某子程序中还有6条PUSH指令和4条POP,也就是说在栈中,由CALL指令入栈的指令地址上面还压着余下两条PUSH指令入栈的数据,那么单一的RET指令读取到的就是最后一条PUSH指令入栈的数据了,这样显然是不正确的。所以用RET 0008来跳过这两条PUSH指令入栈的数据,而正确读取到上一条CALL指令入栈的指令地址。
当然无论是CALL指令还是RET指令,都是由高级语言在编译成可执行文件时自动分配生成的,所以其地址参数也好,反回参数也好,都没有什么可以修改的余地,这里只是对指令做一下了解就行了。在CE的内存汇编指令查看窗口中,可以用光标选定一条CALL指令(地址为直接数的),然后右键菜单点选[Follow]可以跳转到子程序的地址,用[Back]可以返回。当然也可以用[Break and trace instructions]功能来追踪分析CALL的运行去向,比如将光标选定在一条RET指令上,那么就追踪到CALL指令的下一条指令,也就可以找出现在的子程序是从哪里的CALL指令跳转过来的。
------------------------------------------------------------------------------
条件指令
Jxx 条件跳转指令
格式:Jxx 跳转参数(有效的内存地址)
说明:条件跳转指令是根据标志寄位器FLAGS中的相应标志位来决定指令的跳转方式,所以条件跳转指令一般都接在对FLAGS标志位有影响的指令之后。条件跳转指令在当条件满足时,其作用和JMP指令相同,程序会跳转到跳转参数所指定的目标地址;而当条件不满足时,程序则会继续运行紧接的下一条指令。条件跳转指令如下所列:
-----------------------------------------------------
操作码 含义(如果…则跳转) FLAGS判定条件
JA/JNBE 大于/不小于等于 CF=0
and ZF=0
JNA/JBE 不大于/小于等于 CF=1
or ZF=1
JB/JNAE/JC 小于/不大于等于 CF=1
JNB/JAE/JNC 不小于/大于等于 CF=0
JE/JZ 相等/为零 ZF=1
JNE/JNZ 不相等/不为零 ZF=0
JG/JNLE 大于/不小于等于(带符号) ZF=0
and SF=OF
JNG/JLE 不大于/小于等于(带符号) ZF=1
or SF≠OF
JL/JNGE 小于/不大于等于(带符号) SF≠OF
JNL/JGE 不小于/大于等于(带符号) SF=OF
JO 有溢出 OF=1
JNO 无溢出 OF=0
JP/JPE 低8位1的个数为偶数 PF=1
JNP/JPO 低8位1的个数为奇数 PF=0
JS 结果为负 SF=1
JNS 结果为正 SF=0
JCXZ 寄存器CX=0
条件跳转指令和直接跳转一样,也分为短跳转和长跳转两种,短跳转占两字节,指令一字节,参数一字节。长跳转指令占六字节,指令两字节,参数四字节。长跳转替换成不跳转时(参数值为00000000),同样需要写成Jxx FAR [内存地址]的形式,否则会自动被认为是短跳转。
--------------------------------------------------------
条件跳转指令是在修改游戏判定时最常用的修改方法之一,例如:
[内存地址1]处存的是血量的上限,[内存地址2]处存的当前的血量,[栈地址]中存的时当前受伤所扣除的血量,然后有如下处理判定血量的子程序:
指令地址0 CALL
指令地址2 //调用指令地址2处开始的处理血量子程序
指令地址1 ................... //其余程序代码
指令地址2 MOV
EAX,[内存地址1] //血量上限装入EAX
MOV
EBX,[内存地址2] //当前血量装入EBX
MOV
ECX,[栈地址] //受伤血量装入ECX
SUB
EBX,ECX //当前血量减去受伤血量
JBE
指令地址4 //当前血量<=受伤血量,则跳往指令地址4
CMP
EBX,EAX //比较当前血量和血量上限
JAE
指令地址3 //比较结果如果EBX>=EAX则跳往指令地址3
MOV
[内存地址2],EBX //将当前血量存回到当前血量的地址
RET //反回指令地址1
指令地址3 MOV
[内存地址2],EAX //将满血上限存回到当前血量的地址
RET //返回指令地址1
指令地址4 .................... //挂了的程序代码
--------------------------------------------------------
那么我们想把程序改成受伤也不掉血或者不死,可以有很多种改法:
第一:在EBX中也装入血量上限,那么每次减血时都是从满血开始减。
第二:直接把SUB指令中的EBX替换成EAX,效果同第一。
第三:干脆就把SUB这条指令用NOP替换掉,不计算扣血。
第四:将JBE 指令地址4改成JMP 指令地址3,管它是怎么扣血,反正结果是直接满血。
第五:将JAE 指令地址3改成JNE,结果是只要是当前血量不等于血量上限,就直接满血。
第六:直接将MOV [内存地址2],EBX指令中的EBX替换成EAX,结果同上。
由上例可以看到,用汇编指令改游戏是不是很简单?管它中间是什么样的运算或是判定指令,反正最终只要是将自己想要的结果值写回到内存就万事大吉了。至于怎么改,想怎么改都可以,只要别改错指令就行了。当然上面是一个非常简单的例子,只是大致说明一下用指令修改游戏的原理。在实际修改中,不同的游戏,其判定指令的复杂程度也都不加同,修改的方法和思路也更多变。但是只要能够看懂每一条指令的作用,再仔细分析每一条指令的运行结果,想做相应的修改也还是很简单,CE中的指令追踪功能就是专门用来分析程序的。
另外还有一些小技巧,比如说看不懂前面条件比较或者是判定是什么,所以不知道要怎么修改判定后面的条件跳转来实现自己想要的结果。遇到这种情况其实也很简单,比如判定后的跳转指令是JE 某目标地址,可以先将这条指令CTRL+C保存下来,然后改成JMP 目标地址(不管判定结果如何,都直接跳转到目标地址),或者是改成JE 下一条指令地址(也就是不管判定结果如何,都不执行跳转,而是继续运行下一条指令),然后看这两种改法对游戏的实际影响,就知道自己想要的结果到底是要跳转还是不要跳转了。如果修改后对游戏没影响,那么就马上CTRL+V把源指令还原,再找其它判定了。
------------------------------------------------------------------------------
条件设置指令
SETxx 条件设置指令
格式:SETxx 目标
说明:目标通常为8位寄存器(AL/BL/CL/DL)或者是地址单元[内存地址](4字节),根据FLAGS标志位,当条件满足时将目标设置为01,当条件不满足时则将目标设置为00。
SETA/SETNBE 大于/不小于等于 CF=0
或 ZF=0
SETAE/SETNB 大于等于/不小于 CF=0
SETB/SETNAE 小于/不大于等于 CF=1
SETBE/SETNA 小于等于/不大于 CF=1
或 ZF=1
SETE/SETZ 等于/为零 ZF=1
SETNE/SETNZ 不等于/不为零 ZF=0
SETG/SETNLE 大于/不小于等于(带符号) SF=0
或 ZF=0
SETGE/SETNL 大于等于/不小于(带符号) SF=0
SETL/SETNGE 小于/不大于等于(带符号) SF=1
SETLE/SETNG 小于等于/不大于(带符号) SF=1
或 ZF=1
SETC 有进位或借位 CF=1
SETNC 无进位或借位 CF=0
SETO 有溢出 OF=1
SETNO 无溢出 OF=0
SETP/SETPE 低8位1的个数为偶数 PF=1
SETNP/SETPO 低8位1的个数为奇数 PF=0
SETS 结果为负 SF=1
SETNS 结果为正 SF=0
其中在CE中SETNC指令相当于SETNE,一般都会把SETNE写成SETNC。
例如:
CMP
EAX,EBX
SETNC CL //如果EAX不=EBX,那么CL=01,否则CL=00
CMP
EAX,ECX
SETE
[20401000] //如果EAX=ECX,那么0x20401000处=01,否则=00
注意目标为地址时,不需要用 XXXX PTR 来区分数据的操作长度。
同样在修改时,如果不知道前面判定的结果会是什么,那么可以直接把
SETE AL改成MOV AL,01或者是MOV
AL,00这两种指令,来看游戏中的实际影响。
SETE AL指令占3字节,MOV
AL,01指令占2字节,再补上一个NOP指令,正好可以替换。
------------------------------------------------------------------------------
串指令和重复前缀
这里的"串"不单指字符串,也包括连续的数据,串指令只用于内存操作。
串传送指令:MOVSB/MOVSW/MOVSD
说明:将内存地址[ESI]处的数据传送到内存地址[EDI]中,后ESI和EDI的地址值做相应移动
串比较指令:CMPSB/CMPSW/CMPSD
说明:比较内存地址[ESI]和[EDI]处的数据,设置FLAGS,后ESI和EDI的地址值做相应移动
串扫描指令:SCASB/SCASW/SACSD
说明:根据AL/AX/EAX的值查找[EDI]处的数据,设置FLAGS,而后EDI做相应移动
串储存指令:STOSB/STOSW/STOSD
说明:将AL/AX/EAX中的数据传送到内存地址[EDI]处,而后EDI的地址值做相应移动
串载入指令:LODSB/LODSW/LODSD
说明:将内存地址[ESI]处的数据传送到AL/AX/EAX,而后ESI的地址值做相应移动
以上指令中的B、W、D分别指Byte(1字节)、Word(2字节)、DWord(4字节),表示指令操作数据的长度。
地址值做相应移动是指根据指令操作数据的长度,地址值也做相应的移动,其移动方向由标志寄存器FLAGS中DF位的状态值来决定。当DF=0时,ESI/EDI向高地址移动,当DF=1时,ESI/EDI向低地址移动,移动距离根据指令不同,B移动1字节,W移动2字节,D移动4字节。
例如:
MOV
ESI,20401000 //将数值20401000传送到ESI,即ESI=20401000
MOV EDI,
MOV
[ESI],00000001 //将数值00000001传送到内存地址0x20401000处
MOV
[EDI],00000002 //将数值00000002传送到内存地址0x
MOVSD //将内存地址0x20401000处的数值传送到地址0x
其运行后结果为内存地址0x20401000和0x
且标志位DF=0时,ESI=ESI+4=20401004,EDI=EDI+4=204010B0
而标志位DF=1时,ESI=ESI-4=20400FFC,EDI=EDI-4=
----------------------------------
重复前缀
REP ECX
> 0 时
REPE(或REPZ) ECX
> 0 且 ZF = 1 时
REPNE(或REPNZ) ECX
> 0 且 ZF = 0 时
当条件满足时,程序会重复执行后缀的指令,并且每执行一次之后ECX的值都会递减1。注意重复前缀是每次先检查条件,如果条件不满足则不运行后缀的指令。其中REPE/REPNE前缀条件中的"且ZF的状态值"只对后缀指令为CMPS/SCAS时生效。也就是如果当后缀指令是MOVS/STOS/LODS时,前缀REP/REPE/REPNE的三个效果都是一样的。
例如:
MOV
ECX,00000010 //将数值00000010传送到ECX,即ECX=00000010
MOV
ESI,20401000 //将数值20401000传送到ESI,即ESI=20401000
MOV EDI,
REPE
MOVSD //当ECX>0时重复MOVSD指令,直到ECX=0停止,每执行一次ECX-1
其运行结果是把内存地址0x20401000~
再如:
MOV
EAX,00000000
MOV
ECX,00000010
MOV
EDI,20401000
REPE
STOSD
其运行结果则是把内存地址0x20401000~
至于CMPS和SCAS指令,标志位ZF=1时表示结果相等或者结果为0,ZF=0时表示结果不相等或者结果不为0。所以:
REPE
CMPSD则表示在地址[ESI]和[EDI]之间的指定范围(由ECX控制)内查找不相等的内容,因为相等指令就会一直比较(除非ECX=0,也就是完成所设定范围的比较),而遇到不相等时则会停止重复,指令停下时ESI和EDI就正好指向不相等的两个数据内存地址。
REPNE
CMPSD就正好相反,作用是查找相等,因为不相等就会一直查找,相等就会停下,ESI和EDI就正好停在相等处。
REPE
SCASD和REPNE SCASD作用也是差不多,只不过是用来查找EAX和[EDI]之间的区别。
串指令这里只需要理解其作用就行了,修改游戏代码时一般不会使用它们,主要是用来分析游戏程序。比如找到某一有效数值的内存地址,然后用CE追踪什么指令在往这个地址这里写数据,结果发现追踪出来的结果不是一条含地址的MOV传送指令,而是一条给ECX赋值或者运算的指令,而这后面一般就是一条重复串指令(CE追踪到的结果是串指令时,显示的结果是串指令的前一条指令)。那么就可以根据已知的地址,找到和EDI所对应的ESI地址,就可以找到数据的来源了。比如:找到某一数据的存放地址是0x204010EC,然后想通过CE来追踪这个数据的来源,结果追踪出的是重复串传送指令。那么根据ESI/EDI的初始值,就可以找到数据的来源,比如串指令ESI的初始值是20401000,EDI的初始值是
================================================================================
X87浮点指令
浮点指令是专门用来处理内存单元和浮点寄存器之间的数据传送和运算的,同样这里只列举常见和常用的。因为指令操作的原理都是差不多的,只是操作对象变成了浮点寄存器和内存单元之间,所以这里就不像上面那样说得那么详细了。只是要注意一下,浮点指令的地址单元还会用到QWORD PTR [地址],也就是此地址位置的8字节数据(双精度),当然目前大部分游戏中还没有用到双精度的浮点数。
浮点传送指令
FLD 装入实数指令
格式:FLD 源地址单元
说明:将源地址单元中的实数装入浮点栈顶指针ST(0)所指向的浮点寄存器。
例如:FLD DWORD(QWORD) PTR [ESI+
因为装载源是浮点数,所以操作的地址单元最小数据长度必须为DWORD PTR(4字节)。
FILD 装入整数指令
格式:FILD 源地址单元
说明:将源地址单元中的整数变成浮点数形式,然后装入ST(0)。因为装入的源为整数,所以源地址单元的长度可以是WORD/DWORD/QWORD PTR。
FLDZ 将浮点数0.00装入ST(0)
FLD1 将浮点数1.00装入ST(0)
注意以上装入指令在向浮点寄存器存入数据前,其浮点栈顶指针ST(0)会先向栈顶方向移动一个寄存器单元,然后再向ST(0)所指寄存器单元存入数据。这点和X86中的PUSH指令比较相似,只是PUSH指令是先装入数据,然后再移动指针。
-------------------------------------------
FST 保存实数指令
格式:FST 目标地址单元
说明:将浮点栈中ST(0)所指寄存器单元中的浮点数保存到目标地址。因为浮点寄存器中的数据必定是浮点数,所以目标地址的数据操作长度最小必须为DWORD PTR(4字节)。
FSTP 实数出栈指令
格式:FSTP 目标地址单元
说明:先将ST(0)所指寄存器单元中的数据出栈,并用目标地址接收出栈数据,然后栈顶指针ST(0)向栈底方向移动一个寄存器单元。这点和X86中的POP指令比较相似,只是POP指令是先移动指针,然后再将数据出栈。
FIST 保存整数指令
格式:FIST 目标地址单元
说明:将ST(0)所指寄存器单元中的浮点数转换成整数形式,然后保存到目标地址。因为保存的数据是整数,所以目标地址单元的长度可以是WORD/DWORD/QWORD PTR。
FISTP 整数出栈指令
格式:FISTP 目标地址单元
说明:先将ST(0)所指寄存器单元中的数据出栈,再将结果转换成整数形式,并用目标地址接收转换后的出栈数据,然后栈顶指针ST(0)向栈底方向移动一个寄存器单元。
注意在修改游戏时,遇到这种带有栈指针操作的指令,是不可以随便的用NOP指令将原指令替换掉的。因为这样会造成指针的错位,影响到其后的其他指令。比如在早先其它一些鬼泣3修改器中,使用了无限魔力和一击3S等功能,就有可能造成人物服装的显示错误(大衣衣领和大衣背后模型显示不正常,露出屁股的情况),就是简单的用NOP指令替换掉原来向内存写入魔力值的指令,没有考虑到这些指令还附带有栈操作的结果。所以要替换也是把入栈和出栈指令成对替换,或者中间有运算指令的,可以替换掉没有栈操作的运算指令。
---------------------------------------------------------------------------------
浮点运算指令
格式 含义 操作
加法
FADD 加实数 st(0)
<- st(0) + st(1)
FADD 源地址单元 . st(0)
<- st(0) + 源地址单元中的实数
FADD
st(i),st(0) . st(i)
<- st(i) + st(0)
FADDP
st(i),st(0) . st(i)
<- st(i) + st(0),且一次出栈操作
FIADD 源地址单元 加整数 st(0)
<- st(0) + 源地址单元中的整数
减法
FSUB 减实数 st(0)
<- st(0) - st(1)
FSUB 源地址单元 . st(0)
<- st(0) - 源地址单元中的实数
FSUB
st(i),st(0) . st(i)
<- st(i) - st(0)
FSUBP
st(i),st(0) . st(i)
<- st(i) - st(0),且一次出栈操作
FSUBR
st(i),st(0) 用实数减 st(0)
<- st(i) - st(0)
FSUBRP
st(i),st(0) . st(0)
<- st(i) - st(0),且一次出栈操作
FISUB 源地址单元 减整数 st(0)
<- st(0) + 源地址单元中的整数
FISUBR 源地址单元 用整数减 st(0)
<- 源地址单元中的整数 - st(0)
乘法
FMUL 乘实数 st(0)
<- st(0) * st(1)
FMUL 源地址单元 . st(0)
<- st(0) * 源地址单元中的实数
FMUL
st(i) . st(0)
<- st(0) * st(i)
FMUL
st(i),st(0) . st(i)
<- st(i) * st(0)
FMULP
st(i),st(0) . st(i)
<- st(i) * st(0)
FIMUL 源地址单元 乘整数 st(0)
<- st(0) * 源地址单元中的整数
除法
FDIV 除实数 st(0)
<- st(0) / st(1)
FDIV 源地址单元 . st(0)
<- st(0) / 源地址单元中的实数
FDIV
st(i) . st(0)
<- st(0) / st(i)
FDIV
st(i),st(0) . st(i)
<- st(i) / st(0)
FDIVP
st(i),st(0) . st(i)
<- st(i) / st(0),且一次出栈操作
FDIVR
st(i),st(0) 用实数除 st(0)
<- st(i) / st(0)
FDIVRP
st(i),st(0) . st(0)
<- st(i) / st(0),且一次出栈操作
FIDIV 源地址单元 除整数 st(0)
<- st(0) / 源地址单元中的整数
FIDIVR 源地址单元 用整数除 st(0)
<- 源地址单元中的整数 / st(0)
---------------------------------------------------------------------------------
浮点比较指令
格式 含义 操作
FCOM 和实数比较 根据st(0) - st(1)的结果设置浮点标志位
FCOM 源地址单元 . 根据st(0) - 源中实数的结果设置浮点标志位
FCOMP 源地址单元 . 比较st(0) - 源中实数,且一次出栈操作
FICOM 源地址单元 和整数比较 根据st(0) - 源中整数的结果设置浮点标志位
FICOMP 源地址单元 . 比较st(0) - 源中整数,且一次出栈操作
FTST 和零比较 比较st(0) - 0.00
FUCOM
st(i) 指定ST比较 比较st(0)
- st(i)
FUCOMP
st(i) . 比较st(0) - st(i),且一次出栈操作
浮点比较指令对浮点标志的影响:C1表示数据溢出,C0/C2/C3反映比较结果。
浮点标志位 C3 C2 C0
ST(0)
> 源操作数 0 0 0
ST(0)
< 源操作数 0 0 1
ST(0) = 源操作数 1 0 0
无序 1 1 1
其中C0位于状态字第9位,作用相当于FLAGS中的CF位,C2位于状态字第11,相当于PF,C3位于第15位,相当于ZF。
----------------------------------------
浮点标志位传送
FNSTSW AX 保存浮点标志状态字到AX寄存器
FNSTSW 目标地址单元 保存浮点标志状态字到目标地址单元(16位)
说明:因为在浮点指令中并没有根据浮点标志状态来判定的条件跳转指令,所以通常都是用浮点标志传送指令,将状态字传送到AX或地址单元,然后再用普通的判定指令来判定比较结果。
例如:
FLD DWORD
PTR [20401000] //将0x20401000中的实数装入ST(0)
FCOMP
DWORD PTR [
FNSTSW AX //将比较结果的状态字存入AX寄存器
TEST
AH,41 //测试AH和41的逻辑与结果
JNE 目标指令地址 //如果小于等于则跳转,大于则不跳转
这个例子需要特别解释一下原理:为什么JNE是小于等于则跳转,大于则不跳转?
首先我们看到标志位小于的结果是C0=1,等于的结果是C3=1,而大于的结果则是C0/C3同时=0
再看C0位于16位标志状态字的第9位(也就是高8位的第1位),C3位于第15位(也就是高8位的第7位),现在我们把AX中的可能出现的几种结果的状态字列出来:
AX-------------- AX中的状态字范围
AH------AL------ AL中不在判定范围
00000001???????? 结果小于时C0位=1
01000000???????? 结果等于时C3位=1
00000000???????? 结果大于时C0/C3位同时=0
41的二进数为01000001,然后TEST上面三种情况的结果和01000001的逻辑与结果为:
小于时00000001逻辑与01000001的结果是00000001,结果不为0,JNE条件满足,所以跳转。
等于时01000000逻辑与01000001的结果是01000000,结果不为0,JNE条件满足,所以跳转。
大于时00000000逻辑与01000001的结果是00000000,结果为0,JNE条件不满足,所以不跳转。
你一定觉得类似这样的判定好复杂啊,改的时候要怎么改呢?
如果看得懂这些判定的,修改TEST AH,数值是最合乎逻辑的,但其实还有一种更简单的方法。
可以看到在TEST指令中无论前后两个操作数是什么,其逻辑与的结果一定是大于0,或者=0,所以在TEST指令后面用JAE就可以相当于JMP,也就是无论结果如何都跳转到目标指令地址。
而TEST指令的结果无论如何都不可能小于0,所以用JB可以相当于无论结果如何都不跳转。
-------------------------------------------
FNOP 空指令
FNOP和X86中的NOP是相同的使用,只不过FNOP指令要占两个字节。
==================================================================================
SSE指令
SSE指令和X86指令在写法和用法上都差不多,只不过SSE指令是专门处理内存单元和SSE寄存器之间的数据传送和运算。SSE指令通常以"PD"、"PS"、"SD"、"SS"后缀结尾,其中:
第一个字母P表示操作对象中有多个浮点数打包并列操作
第一个字节S表示操作对象中为单个浮点数
第二个字节D表示操作对象为双精度浮点数
第二个字节S表示操作对象为单精度浮点数,所以
PD表示2个双精度浮点数并列操作
PS表示4个单精度浮点数并列操作
SD表示操作1个双精度浮点数
SS表示操作1个单精度浮点数,其中
操作对象为1个操作数时,都是指的寄存器的低位。
-------------------------------------------------------------------
传送指令
MOVSS 单精度传送(32位)
格式:MOVSS 目标,源
说明:目标和源可以是寄存器,可以是地址单元,但目标和源中必须有一项是寄存器,而不可以同为内存单元,源也不可以是直接数值,下同。
例如:
MOVSS
XMM0,[20401000] //将0x20401000处的单精度浮点数传送到XMM0中
MOVSS [
其中:
寄存器 <- 内存单元 结果寄存器低32位=内存单元数据,高96位=0
寄存器 <- 寄存器 结果寄存器低32位=寄存器低32位,高96位不变
内存单元 <- 寄存器 结果内存单元=寄存器低32位
-------------------------------------------------
MOVAPS 对齐数据传送(128位)
格式:MOVAPS 目标,源
说明:
寄存器 <- 内存单元 寄存器128位=内存单元128位
寄存器 <- 寄存器 寄存器128位=寄存器128位
内存单元 <- 寄存器 内存单元128位=寄存器128位
MOVUPS 非对齐数据传送(128位)
格式:MOVUPS 目标,源
说明:
寄存器 <- 内存单元 寄存器128位=内存单元128位
寄存器 <- 寄存器 寄存器128位=寄存器128位
内存单元 <- 寄存器 内存单元128位=寄存器128位
MOVAPS和MOVUPS指令的区别就在于,MOVAPS指令中的内存单元必须是16个字节对齐的,也就是内存地址值的最低位必须为0,比如[20401000]。
而MOVUPS指令对地址变量则不做要求,[
MOVAPS和MOVUPS指令都可以将内存单元处连续的4个单精度浮点数一次性装入同一寄存器。
--------------------------------------------------
MOVHPS 高位数据传送(64位)
格式:MOVHPS 目标,源
说明:操作对象只能是寄存器与内存单元之间
存寄器 <- 内存单元 寄存器高64位=内存单元64位,寄存器低64不变
内存单元 <- 寄存器 内存地址64位=寄存器高64位
MOVLPS 低位数据传送(64位)
格式:MOVLPS 目标,源
说明:操作对象只能是寄存器与内在单元之间
存寄器 <- 内存单元 寄存器低64位=内存单元64位,寄存器高64不变
内存单元 <- 寄存器 内存地址64位=寄存器低64位
MOVHLPS 高位向低位传送(64位)
格式:MOVHLPS 目标,源
说明:操作对象只能是寄存器
目标寄存器 <- 源寄存器 目标低64位=源高64位,目标高64位不变
MOVLHPS 低位向高位传送(64位)
格式:MOVLPS 目标,源
说明:操作对象只能是寄存器
目标寄存器 <- 源寄存器 目标高64位=源低64位,目标低64位不变
---------------------------------------------------
SHUFPS 重新排列指令
格式:SHUFPS 目标寄存器(128位),源寄存器(128位),排列方式(8位)
说明:这里目标和源为同一寄存器,源代表排列前,目标代表排列后。
例如:
MOVUPS
XMM0,[ESI] //从地址ESI开始连续读取4个单精浮点数一并写入XMM0
MOVAPS
XMM2,XMM0 //将XMM0中的4个浮点数写入XMM2
那么结果XMM2=XMM0=x1,y1,z1,w1(表示内存地址由低到高的4个浮点数)
排列方式1字节,展开后为8位二进数,例如:11001001,其中二进位
0~1位为目标0索引,其值01表示将源的第1数赋给目标0
2~3位为目标1索引,其值10表示将源的第2数赋给目标1
4~5位为目标2索引,其值00表示将源的第0数赋给目标2
6~7位为目标3索引,其值11表示将源的第3位赋给目标3
于是执行
SHUFPS
XMM0,XMM0,C9 //C9二进展开为11001001
SHUFPS
XMM2,XMM2,D2 //D2二进展开为11010010
其结果
XMM0=y1,z1,x1,w1
XMM2=z1,x1,y1,w1
再例:
MOVSS
XMM0,[20401000] //读取内存0x20401000处的单精度浮点数写入XMM0低32位。
SHUFPS
XMM0,XMM0,00 //将源XMM0中的低32位单精浮点数重新排列到目标XMM0中的4个单精浮点空间中,即向目标XMM0中装入4个相同的浮点数。
---------------------------------------------------
整数转换传送指令
CVTPI2PS 寄存器,地址单元
将地址单元中的2个32位整数转换成浮点数形式,并将结果存入寄存器的低64位
CVTPS2PI 地址单元,寄存器
将寄存器低64位中的2个浮点数转换成整数形式,并将结果存入地址单元
CVTSI2SS 寄存器,地址单元
将地址单元中的32位整数转换成浮点数形式,并将结果存入寄存器的低32位
CVTSS2SI 地址单元,寄存器
将寄存器低32位中的浮点数转换成整数形式,并将结果存入地址单元
或许会觉得上面几条指令很难区分记忆,其实根据指令的后缀就可以看出每条指令的作用了。比如:CVTPI2PS指令,"PI/PS"中的第一个字母"P"就表示多个浮点数打包操作,第二个字母"I/S"就分别表示整数/单精浮点,而"2"就是"TO"的同音。CVTSS2SI指令中后缀的第一个字节"S"则表示标量(也就是单个浮点数操作),所以CVTSS2SI指令的含义就是"SS TO SI",也就是1个单精浮点转换成整数。
----------------------------------------------------------------------------------
运算指令
因为SSE指令的格式和X86的差不多,所以这里只简单的说明一下指令的作用:
指令的格式都是:运算指令 目标,源
指令操作都是将目标和源的运算结果存入目标中,其中目标和源可以是内存单元,可以是寄存器,但不可同时为内存单元。
加法指令
ADDPD 2次双精度加法,目标高64位 + 源高64位,目标低64位+源低64位
ADDPS 4次单精度加法,将目标和源中的4个单精度浮点数分别对应相加
ADDSD 1次双精度加法,目标低64位 + 源低64位
ADDSS 1次单精度加法,目标低32位 + 源低32位
减法指令
SUBPD 2次双精度减法,目标高64位 - 源高64位,目标低64位-源低64位
SUBPS 4次单精度减法,将目标和源中的4个单精度浮点数分别对应相减
SUBSD 1次双精度减法,目标低64位 - 源低64位
SUBSS 1次单精度减法,目标低32位 - 源低32位
乘法指令
MULPD 2次双精度乘法,目标高64位 * 源高64位,目标低64位-源低64位
MULPS 4次单精度乘法,将目标和源中的4个单精度浮点数分别对应相乘
MULSD 1次双精度乘法,目标低64位 * 源低64位
MULSS 1次单精度乘法,目标低32位 * 源低32位
除法指令
DIVPD 2次双精度除法,目标高64位 / 源高64位,目标低64位-源低64位
DIVPS 4次单精度除法,将目标和源中的4个单精度浮点数分别对应相除
DIVSD 1次双精度除法,目标低64位 / 源低64位
DIVSS 1次单精度除法,目标低32位
/ 源低32位
其中在后缀为PD/PS的运算指令中,如果运算对象为地址单元时,其地址变量必须对齐内存16个字节,也就是内存地址值的最低位必须为0,比如:ADDPD XMM0,[
----------------------------------------------------------------------------------
逻辑指令
SSE逻辑指令和X86逻辑指令作用相同,逻辑运算对象都是目标和源的二进制位。
ANDPD/ANDPS
目标,源 逻辑与运算目标和源中二进制位,结果写入目标
ORPD/ORPS
目标,源 逻辑或运算目标和源中二进制位,结果写入目标
XORPD/XORPS
目标,源 逻辑异或目标和源中二进制位,结果写入目标
ANDNPD/ANDNPS
目标,源 先将目标进行逻辑非运算,再将结果和源逻辑与运算
其中指令后缀"PD"表示操作对象为双精度,"PS"表示操作对象为单精度。
同样的,XORPS XMM0,XMM0,表示将寄存器XMM0中的数据全部清0。
----------------------------------------------------------------------------------
比较指令
CMPSD /
CMPSS
格式:指令 目标,源,比较方式(1字节)
目标必须为寄存器,源可以是寄存器,可以是内存单元,其中比较方式有以下8种:
00 等于
01 小于
02 小于等于
03 无序
04 不等于
05 不小于(大于等于)
06 不小于等于(大于)
07 有序
COMISD /
COMISS / UCOMISD / UCOMISS
格式:指令 目标,源
目标必须为寄存器,源可以是寄存器,可以是内存单元,根据比较结果设置FLAGS。
其中COMISD/COMISS为有序比较,UCOMISD/UCOMISS为无序比较。
由于SSE的比较指令也是影响FLAGS,所以后面的根据条件分歧的判定指令还是使用X86的指令。
----------------------------------------------------------------------------------
Chapter 4 指针
修改游戏一般有两种方法,一种就是直接修改内存中的数据,第二种就是直接对程序指令进行修改了。虽然对游戏指令进行修改会起到更好的效果,但是如果指令是同时处理玩家和电脑的数据时,用修改指令的方法就会使电脑也受到同样的影响。比如在策略类、战棋类游戏中,玩家和电脑用的都是相同的判定指令,如果修改指令,比如说改成所有人物无论本身只能习得的是什么技能,都可以使用全部技能,那么敌人也将能够使用所有的技能。嘛,虽然这样改也可以算是一种玩法,但是如果是处理体力的指令,如果改出受伤不扣血,那么敌人也会变成不会掉血了。那么想改出玩家不掉血,就只能找出只对玩家属性判定的指令。但是如果所有的判定都是敌人玩家通用的话,那么就只能用直接内存数据修改的方法了。
在一些游戏中,某些数据的存放地址总是在发生变化,这样每次修改之前都不得不重新搜索一次数据地址,而锁定数值也会因为地址发生变化而使得锁定失效,甚至还会使得程序出错的情况。假如把这种数据位置会发生变化的地址称为动态地址,把数据位置不会发生变化的地址称为静态地址。那么在修改动态地址的数据时,就不能用直接地址,而必须使用指针地址。在CE的内存查看窗口中,数据地址是静态地址的会用绿色来显示,而数据地址是动态地址的会用黑色来显示。当然包含指针的地址并不一定就是动态地址,如果指针是一个常量,那么地址就是静态的,如果指针是变量,那么地址就是动态的。
什么是指针呢?比如我们找到某数据的地址是[20401004],我们追踪是什么指令在往这个地址写数据,然后找出的指令为:
MOV WORD
PTR [EBP+04],EAX
那么EBP就是地址[20401004]的指针,04为指针的修正值,指针+修正值就是地址[20401004]
可以看出EBP的值也就是指针为20401000。
打开CE的内存查看窗口,在反汇编指令区域找到这条指令,假设在这条指令之前还有这样一条指令:MOV EBP,[00804000]
那么[00804000]就是指针EBP的指针地址,也就是初级指针地址。
在CE的地址列表中添加含指针的地址(勾选上Point),初级指针地址为00804000,地址修正值(Offset)则为04,添加后的地址结果显示为P->20401004,"P->"表示这是一个含指针的地址。
所以如果在[00804000]中存放的地址值是一个常量的话,那么EBP+04的结果也是一个常量。但如果在[00804000]中存放的地址值是一个变量,那么EBP+04也会根据EBP的变化而得到不同的结果。这是只有一级指针的情况,另外指针还有多级的情况。比如:
MOV
EAX,[00604000] //假设在地址[00604000]中存放着数值
MOV
EBX,[EAX+40] //那么[EAX+40]=[
MOV
EBP,[EBX+80] //那么[EBX+80]=[10401080],再假设此存存放指针20408000
MOV WORD
PTR [EBP+04],2710 //那么[EBP+04]=[20408004],这个就是最终地址了
其中
[00604000]为初级指针的地址,EAX为初级指针,40为一次修正值,EAX+40的结果:
[
[10401080]为三级指针的地址,EBP为最终指针,04为三次修正值,EBP+04的结果:
[20408004]为最终结果的地址。
可以看到在上面指令中目标的EAX、EBX、EBP其实就分别是初级、二级、三级指针
源地址单元中的+40/+80/+04则分别是一次、二次、三次修正值。
而[00604000]、[EAX+40]、[EBX+80]、[EBP+04]则分别为:
初级指针的地址、二级指针的地址、三级指针的地址和最终结果的地址,注意初级指针的地址一定是一个直接数值。在CE的地址列表中添加多级指针地址时则是添加:
[初级指针地址]+一次修正值+二次修正值+三次修正值这样的形式。
通常在找到某个数据地址后,然后查找写指令的结果是:
MOV [直接内存地址数],源值
这样的形式,那么就表示此数据的地址是直接地址值,这个地址值是不会发生变化的,因为程序指令本身是不会发生变化的。如果查找写指令的结果是:
MOV [寄存器+修正值],源值
这样的形式,那么则表示寄存器为指针,那么再前一条给寄存器赋值的指令
MOV 寄存器,[内存地址]
中的这个[内存地址]就是指针的地址,这个[内存地址]可能是[直接地址值],也可能是
[寄存器+修正值]这样的形式。通常[直接地址值]这样的形式中直接地址值就是初级指针的地址,而[寄存器+修正值]这样的形式则表示地址还含有指针。
所以当我们查找写指令时,通常可以向这样直接从追踪出的写内存指令向上查找前面的指令,一直找到初级指针地址和每次的修正值。
当然这是在前面这些指令距离写内存指令不远的情况,但是如果前的指令距离很远,就很难这样直接从指令中找出初级指针地址和每次修正值了。比如说追踪某一数据的地址[20408004],然后找出这样一条写内存指令:
MOV WORD
PTR [EBP+04],2710
通常情况我们一般可以直接从这条指令向前查找前面的指令,来找出EBP的数据来源。但有很多时候,EBP可能会在很早很早的时候就被赋值了,然后一直没有变过,那么则表示EBP这个指针会处理很多很多数据。像这样的情况,就很难直接从指令上找出给EBP指针赋值的指针地址了,这个时候就只能在数值搜索的位置来查找。比如还是这条指令:
MOV WORD
PTR [EBP+04],2710
那么EBP就是指针,04就是修正值,找到这条指令后用详细查看当前指令的寄存器状态,记下EBP的当前数值,因为[EBP+04]=[20408004],所以此时EBP=20408000。我们用CE的地址查找功能,查找20408000这个指针的内存地址。因为指针本身也是地址值,地址值一定是占4字节整数,而且一定是16进制数,所以我们在用CE查找地址时,就把查找类型设为4字节,再勾选上HEX,然后在查找框中输入指针:20408000,当然这样查找会找到很多很多个结果地址。
我们这里先暂时假设只有一个正确的结果,也就是存放EBP指针的正确地址,如果结果地址值为绿色显示,那么就表示这个地址就应该是初级指针的地址了。但如果结果的地址为黑色显示,那么则表示结果地址中还包含有其他指针了。
我们假设查找指针20408000找到的地址结果为[10401080],那么这个地址就是存放指针20408000的地址,然后我们把[10401080]这个指针地址也添加到地址列表,然后对[10401080]这个地址进行追踪,看是什么指令在"读取(运算)"这个地址,其结果中有指令:
MOV EBP,[EBX+80]
因为[EBX+80]=[10401080],所以此时EBX=10401000,而EBX就是[10401080]的指针,+80为修正值。然后再用同样的方法搜索存放EBX指针10401000的内存地址,找出的地址结果为:
[
MOV
EBX,[EAX+40]
因为[EAX+40]=[
这个地址就是初级指针的地址了。我们可以把00604000这个地址也添加到地址列表中,然后追踪什么指令在读取,其结果中有指令:
MOV
EAX,[00604000]
可以看到这条指令的内存单元是一个直接数值,不含寄存器变量,那么这个就肯定是初级指针的地址了。
可以看到,找指针找地址的过程就是上面那个程序指令示例的逆推,只是因为通常这些指令并不一定都在一些,可能都离得很远,所以无法直接从指令上找出指针和指针地址,我们就只能用这样一步步逆推的方法来查找了。
当然上面这是举例,为了说明逆推的过程,但在实际操作过程中,每次查找指针的地址结果并不会只一有一个,而会有很多很多的结果,那么我们怎么才能知道哪一个地址结果才是正确的呢?首先,我们查找指针、查找指针地址就是因为我们找到的数据最终地址总是会发生变化。最终地址总是发生变化,是因为其指针变量总是在变化,比如鬼泣4中体力和魔力的内存地址。
假设我们通过模糊搜索,找到了魔力的内存地址,这个地址会在每次切换场景时发生变化。
比如当前魔力的地址为[
MOVSS
[EDI+
那么这个EDI就是指针了,查看EDI的值=
结果我们发现会找到很多个地址结果,要怎么知道哪一个才是正确的地址呢?我们知道这个指针在每次切换场景时会发生变化,所以回到游戏中切换几下场景,会看到CE的查找结果中,数值会发生变化,因为指针的变化是很有规律,不是胡乱变化的,可以看到查找结果的列表中有几个地址的数值变成了
然后把这三个结果都添加到地址列表中,然后一一追踪什么指令在读取(运算)这些地址。
追踪第一个地址,结果出现的指令中有带两个寄存器变量的,有只带一个变量的暂时排除。
追踪第二个地址,没有任何指令出现,排除。
追踪第三个地址,结果出现的指令都是:[寄存器+24]这样的形式,显然这个寄存器就是指针了。我们一一查看这些指令的寄存器状态,马上发现第二条结果指令上面就一条直接数值地址的传送指令。
MOV
EAX,[00E558B8] <-
追踪结果的前一条指令
MOV
EAX,[EAX+24] <- 追踪到的读取指令之一
显然这个[00E558B8]就是初级指针的地址了,然后[EAX+24]中的+24为一级修正值
上面MOVSS [EDI+
[[00E558B8]+24]+
当然,如果没有看到有像MOV EAX,[00E558B8]这样的直接数值地址的传送指令,那么就只能像上面那样再继续逆推了。
注意有个技巧,就是在同一游戏中,相关的数据,它们的地址指针是相同的。比如刚才我们找到的魔力的指针[00E558B8]+24,这个指针再加上魔力指令中的修正值就是魔力的地址。
体力、魔力属于相关的数据,大多情况它们会被放在同一段内存中,所以地址的指针也是相同的,比如体力的写指令是:MOVSS [EDI+000015CC],XMM0
那么体力的指针地址就是:[00E558B8]+24+15CC了。
在鬼泣4DX9中,所有与人物相关数据的地址指针其实都是[00E558B8]+24
在找指针时,只要找到一个数据地址的指针,其它相关的数据基本上都会用的同一指针。
所以我们通常直接把已经找到的指针添加到地址列表中,比如:[00E558B8]+24,
然后将地址的结果用16进制来显示(选中已添加地址项,鼠标右键选[Show as hex...])
那么指针地址[00E558B8]+24的结果显示的就是上面的
注意在追踪指令时,最好是追踪那些只对玩家起作用的指令。比如鬼泣4中的体力,像魔人状态回复体力的指令,这个就是只对玩家有效的,还有M16毒气扣血指令也是只对玩家有效的,而像受伤掉血的指令则的对玩家和敌人都有影响的。在查找指针时我们一般就只选择这些只对玩家有效的指令入手,而不从对敌人和玩家都有效的指令入手。因为即使我们通过对敌我双方的都有效的指令来查找指针,其指针也是即会指向玩家也会指向敌人,那么当我们锁定这样的指针地址中的数值时,显然也会同时锁定了敌人的血量,这样就会使得查找出的指针完全没有意义了,要改不掉血直接改指令就行了。
---------------------------------------------------------------------------------
下面列举一下鬼泣3/4中的常用指针地址
鬼泣3 1.3版
指针地址 指向地址
[00B
[00B6B
[00B6B
[00B
比如
[[00B6B
[[00B6B
----------------------------------
鬼泣4 DX9
指针地址 指向数据
[00E558B8] 时间、红魂之类
[00E558B8]+24 玩家相关状态
[00E558B8]+B0 BOSS相关状态
[00E558B8]+24]+3094 所有敌人相关状态(距离哪个敌人最近,指针就指向哪个敌人)
比如
[00E558B8]+114 红魂数量
[00E558B8]+164 任务时间
[00E558B8]+24]+15CC 玩家当前体力
[00E558B8]+24]+
[[00E558B8]+24]+3094]+1410 敌人类别编号
Chapter 5 编写
SpoilerAL,是一款万用的内存修改器,作者是日本一位叫あつ的达人。
这款内存修改器本身没有搜索功能,它最大的特点就是将已知的内存修改代码转换成可以直接运行的修改器。也就是说只要有已知内存代码,理论上就可以修改任何游戏,非常方便。
SpoilerAL修改器的官方发布页面:
wcs.main.jp/index/software/spal/
SSGファイル一覧:(官方发布的SSG修改文件一览)
wcs.main.jp/index/information/ssg/creator/all.htm
最终版本ver6.1的直接下载链接:
wcs.main.jp/index/software/spal/spal61.lzh
SpoilerAL修改器的安装、使用和卸载:
SpoilerAL修改器没有特别的安装程序,直接用解压软件WinRAR(WIN7可以直接用资源管理器打开)将从官网上下载的spal61.lzh压缩包文件中的所有目录和文件解压到任意硬盘的任意目录下(说是任意,但最好是不带中文汉字的纯英文目录),再将下载的内存修改SSG文件复制到SpoilerAL修改器目录中的SSG子目录下,最后运行SpoilerAL即可。
修改器运行后,会在Windows注册表中写入一些注册信息(修改器的识别目录以及窗口大小位置等),所以彻底从系统中删掉修改器,除了删掉修改器的目录和文件以外,还要有注册表中的[HKEY_CURRENT_USER\Software\Atsu]这一项。可以用注册表编辑器找到这一项删除,或者是用记事本新建一个扩展名为.reg的文件,将以下两行代码复制到其中并保存:
Windows
Registry Editor Version 5.00
[-HKEY_CURRENT_USER\Software\Atsu]
保存之后,在资源管理器中用鼠标点击运行这个扩展名为.reg的文件,即可完全清除修改器在注册表中写入的注册信息。
另外如果SSG目录下的修改文件有所调整或变更之后,造成修改器标题列表中的游戏标题重复识别的话,可以删掉修改器目录下的SpoilerAL.log文件再重新运行一次修改器,或者是用记事本打开SpoilerAL.log文件,将里面的内容全部删掉然后保存,再重新运行一次修改器也可以。
SpoilerAL修改器内存修改代码识别文件的编写方法:
点击修改器目录中HowToSSG目录下的index.html文件即可打开SSG文件编写方法的说明文件,不过因为说明文件是日文的,所以在这里就一些主要常用的修改功能翻译说明一下。
======================================================================
文件构成:
.SSG
SSG(spoiler
scripts goup)为主干识别文件,理论上所有的代码都可以写在这一个文件中,但是如果修改代码过于繁多的话,一般还会使用以下子文件以方便编写整理和修改。
.ADJ
ADJ(adjustment)为地址修正专用子文件。
.CHN
CHN(chain)为多行修改代码专用子文件。
.LST
LST(list)为多行项目代码专用子文件。
.SSC
SSC(spoiler
scripts child)为[replace]代码专用子文件。
.SSF
SSF(spoiler
scripts funnel)为[funnel]代码专用子文件。
.SSR
SSR(spoiler
scripts repeat)为[repeat]代码专用子文件。
.SSL
SSL(spoiler
scripts library)为通用子文件,可以代替以上除SSG以外所有的子文件。
所有文件都必须保存在修改器的SSG子目录下才能被修改器识别,当然SSG子目录下的子目录也是可以被识别的。比如:
x:\SpoilerAL\SSG\game1.ssg
x:\SPoilerAL\SSG\GAME\game2.ssg
都是可以被识别的,但是要注意同一游戏的SSG文件和子文件必须在同一目录中。比如:
x:\SPoilerAL\SSG\GAME\game2.ssg
x:\SPoilerAL\SSG\GAME\game2.ssl
所有的修改文件本质上就是文本文件(扩展名不是.txt而已),都可以直接用记事本编写,注意保存的时候编码只能是默认的ANSI,Unicode和UTF-8编码都无法被修改器识别。
=============================================================================
代码的编写:
修改器的主要识别的是SSG文件,所有代码只有写在SSG文件中才会被修改器执行,另外注意文件中所有的符号都为英文半角符号。
主干代码(必要):
[script]
所有代码写在这个之间才会被修改器识别……
[/script]
主干代码主要用来让修改器识别和区分不同游戏,同一个SSG文件中可以有多个主干代码,也就是同一个SSG文件内可以编写几个游戏的修改代码,不过官方建议单个SSG文件的大小最好不要超过64KB。
比如:
game.ssg
-----------------------
[script]
游戏一的修改代码
[/script]
[script]
游戏二的修改代码
[/script]
[script]
游戏三的修改代码
[/script]
……
-----------------------
识别代码(必要):
[title]游戏的标题
修改器的标题显示选项由此处读取
[creator]修改代码的编写者
[maker]游戏的制作者
以上两行只是修改器显示用,没什么特别的实际意义。
[process]游戏的进程文件名
非常重要的识别代码,此处的文件名必须与内存中运行的游戏进程名一致(大小写可以不分),否则修改器无法找到内存中的游戏进程。
[note]wordwrap
关于游戏、关于修改的各种文字说明
[/note]
wordwrap表示[note][/note]之间的文字说明根据修改器的窗口大小自动换行,不写wordwrap则表示不自动换行。
以上代码构成SSG文件的主要部分,再加上修改代码就成为一个完整的SSG文件。比如
dmc.ssg
-----------------------------
[script]
[title]Devil
May Cry X
[creator]noname
[maker]Capcom
[process]dmcx.exe
[note]wordwrap
鬼泣X的修改器使用说明
[/note]
[subject]红魂数量:calc,0x400000,0,999999,unsigned
[/script]
-----------------------------
以上就构成了一个最简单的完整的SSG文件,[subject]为修改项目,后面会详细讲解。
--------------------------------------------------------------------------------
SSL文件的构成:
SSL为通用子文件,它由分组够成,比如:
test.ssl
-----------------------------
[group]file1
内容一
[/group]
[group]file2
内容二
[/group]
[group]file3
内容三
[/group]
-----------------------------
其中每一组中的内容?都可以用来代替其它子文件,比如以上SSL文件可以分解成
file1.adj
---------------
内容一
---------------
file2.chn
---------------
内容二
---------------
file3.lst
---------------
内容三
---------------
这样三个子文件,也就是在SSL文件中每一个[group]都可以代表一个其它子文件。
=========================================================================
内存地址的写法:
注意修改器读取地址时默认是按10进制读取的,而内存地址一般都是用16进制表示的,所以在书写时16进制数前面必须再加上"0x",这也是为了把地址和一般数值区别开来。比如
4194304 10进制地址
0x400000 16进制地址
上面两个地址值是完全相同的。
------------------------------------------------------------------------
带运算符号的地址:
带运算符号的地址书写时前面要加一个"_",比如
_0x400*50+10
_0x400*(0x32+0x
运算符号:
"+"加、"-"减、"*"乘、"/"除(商)、"%"除(余)
逻辑运算符号(返回运算结果):
"&"逻辑与、"|"逻辑或、"^"逻辑异或、"~"逻辑非
逻辑直值符号(返回真假值,真为1,假为0):
"<"小于、"<="小于等于、">"大于、">="大于等于
"=="等于、"!="不等于、"&&"逻辑与、"||"逻辑或
地址修正运算符号:
_[:0x400000:]
表示从0x400000这个地址这里读取4字节16进制整数,然后把读取的结果值当做地址,这是编写指针地址经常需要用到的。比如:
_[:[:0xb
首先读取地址0xb
----------------------------------------------------------------------------
另外还有
_[_0x400000_]为[adjustment]项目专用,用法含义不明……
_[.0x400000.]为[replace]项目专用,用法含义不明……
$变数名、=>变数名
例如:_ 0x400 => tmp; $tmp+0x10;
_mem,地址,……,……,……等等一些看不懂的变量- =
这些都是变量列表地址专用,比如三国、信野这类的游戏,通常有上百城池、上千武将,每个城池、武将又有十到二十来个状态、属性值,如果用一般的地址写法想要分别修改每一个属性值就得写上上万个地址,这些就是专门用来表示变量地址的写法。比如每个城池、人物的数据量是一定的,所以它们的之间的地址间隔也是一定的。嘛,这个具体怎么用我也没有研究过,有兴趣的可以自己到官网上下载一些旧游戏的SSG文件,比如太阁5、信野12的SSG文件,自己研究一下变量列表地址怎么写,不过这类游戏一般很快有人出修改器,所以研究这个貌似意义也不是很大……
=========================================================================
数值的写法:
修改器读取数值时默认是按16进制读取,所以当数值写的是50时,实际上是10进制的80。
数值之间一个字节的空格是被无视的(后面所有数值写法如此),如下:
2EFFC0E0
2E FF C0
E0
以上这两种写法都是正确的。注意数值在书写时的最小单位是字节,比如数值1必须要写成01,否则就会被识别成10了。
通配符"?":
"?"表示这里的值表示不做处理,比如某一地址处的原值为
01234567
然后往这个地址写入新的数值
89??C??F
其结果则为
-------------------------------------------------------------------------
相对跳跃符"*+":
格式:*+ 使用字节数 跳跃字节数
说明:使用字节数表示其后面接着的几个字节为跳跃字节数
例如:5e *+ 01
结果为向指定地址写入5e,然后跳过
再例:5e *+ 04
则表示向指定地址写入5e,然后跳过
以上两例的实际结果是完全相同的,但是需要特别注意的如果使用字节数指定的是04,那么后的跳跃数一定也要写满4个字节,像
绝对跳跃符"*:"
格式:*:目标地址
例如:
其含义为在指定地址写入数值
二进制展开符"*_"
格式:*_8字节二进制数(每一位上的数值只能是0、1、?)
例如:01 02 *_00000100 08
其结果为向指定地址写入01020408,这个符号的主要作用是将某字节的某一位改写。
比如某地址处的原值是03,然后写入*_????1???,其结果为0B(参看二进制运算)。
条件分歧处理符"*<"
格式:*< 条件字节 :: 条件真处理字节 :: 条件伪处理字节 *>
例如:*< 10 :: 01 :: 00 *>
其含义为如果某地址的值为10时,那么在紧接着后面一个字节处写入01,如果值不为10,那么写入00。
条件置换处理符"*{"
格式:*{ 条件字节
:: 条件真处理字节 :: 条件伪处理字节 *}
例如:*{ 10 :: 01 :: 00 *}
其含义为如果某地址的值为10时,那么将10替换成01,如果为其它值时,则替换为00。
读取置换处理符"$? $$"
格式:$? 地址值
$$ //?号范围1~4,表示读取的字节数
例如:$4 [:0x400000:]+0x500 $$
其含义为读取指针地址[:0x400000:]+0x500这里的4字节数值。
这是个非常有用的功能,特别是对RPG游戏来说。比如RPG游戏中的血量总是随着等级而增长,如果想要锁定血量,就总是要根据等级来改变锁定的血量,这是很麻烦的,这个处理符就可以完美的解决这个问题。
比如地址0x500004这里存贮的是血量的上限,0x500008这里存贮的是当前的血量,那么只要指定往0x500008这个地址这里写入$4 0x500004$$。这样就成了读取0x500004这里的血量上限,然后将结果写到0x500008这个地址这里,然后锁定,这样当前血量就总是等于血量上限了。
反复命令处理符"*["
格式:*[ 反复长度的地址式 :: 反复处理的字节 *]
例如:*[ 0x20 :: FF00 *]
其含义为从当前指定地址开始反复写入数值FF00,总共写满20字节,也就是将FF00连续写10次。如果反复长度不足的情况,其写入值不会超过规定长度,比如:
*[0x08::010203*]其结果则为0102030102030102
屏蔽字节处理符"<_"
格式:<_ 屏蔽字节列 :: 处理字节列
_>
例如:<_ A020 :: FFCC _>
说明:看不懂这是做什么用的,估计也没什么用……
===============================================================================
修改项目的写法:
回到上面列举的一个完整的SSG文件示例:
dmc.ssg
-----------------------------
[script]
[title]Devil
May Cry X
[creator]noname
[maker]Capcom
[process]dmcx.exe
[note]wordwrap
鬼泣X的修改器使用说明
[/note]
[subject]红魂数量:calc,0x400000,0,999999,unsigned
[/script]
-----------------------------
其中除了识别的代码以外,
[subject]红魂数量:calc,0x400000,0,999999,unsigned
这一条即为修改项目的代码,[subject]为项目的开关标识,以下就开始讲解以[subject]开关的修改项目,以及与其相关的一些代码的写法。
-------------------------------------------------------------------------
项目的统一写法:
[subject]项目名:项目类别,选项
其中项目名的写法,可以是英文,也可以是汉字(不一定所有的汉字都能被修改器所显示,这个要自己试)。可以看到在修改器的窗口界面分为左右两个区域,项目名一般也是分左右两边不同显示的。比如:
[subject]左侧项目显示/右侧项目显示:………………
中间用"/"分开左右两边不同的显示内容
然后是屏蔽项目右侧的锁定选项,如果这个项目不需要显示锁定选项时,可以在项目名最后加上"_"则表示这个修改项目不显示锁定选项,比如:
[subject]左侧项目显示/右侧项目显示/_:………………
这样这个项目就不会显示锁定选项,或者是
[subject]修改选项//_:………………
这条项目省略了右侧的显示内容,所以用了两个"/"。
项目目录的写法:
[subject]项目名:dir
这是一条纯粹的显示项目,在这一条代码之后的项目在窗口显示上都会被列在这一目录之下,注意目录的项目名只有左侧显示。
返回目录代码
[back] //反回上一级显示目录
[root] //反回到根显示目录
例如:
[subject]人物:dir
[subject]角色一:dir
[subject]体力:calc,0x400000,0,100
[subject]魔力:calc,0x400004,0,100
[back]
[subject]角色二:dir
[subject]体力:calc,0x400010,0,100
[subject]魔力:calc,0x400014,0,100
[root]
这样在修改器窗口界面上,显示着人物栏分为角色一、角色二两个子栏,然后每一栏下面又分别有体力、魔力两个子项。
-------------------------------------------------------------------------------
calc 计算器输入数值项
格式:
[subject]项目名:calc,地址,最小值,最大值,有无符号,次序
项目名:计算器输入数值项的项目名可以有三段,左侧显示/右侧显示//单位(比如只、个)
地址:详见上面内存地址的写法
最小值:输入数值的最小值限定,如果输入的数值比这个值小则无效。
最大值:同上,最大值的限定。
有无符号:signed为带符号,unsigned为无符号,默认为signed,默认时可省略不写。
次序:big_e表示高位在前,little_e表示低位在前,默认为little_e,默认时可省略不写。内存里面的数值一般都是低位在前的,比如10进数999的16进数为[03E7],内存中则写成[E703],不过有少数一些模拟器游戏的数值是高位在前的,那么就要加上big_e了。
例如:
[subject]数量/剩余//个:calc,0x400000,0,10
f_calc 浮点数输入数值项
格式:
[subject]项目名:f_calc,地址,最小值,最大值,精度,次序
精度:float为4字节单精度,double为8字节双精度,默认为float,默认时可省略。
例:
[subject]单精度:f_calc,0x400000,0.82,2500,float
[subject]双精度:f_calc,0x500000,-3,23.55,double
-------------------------------------------------------------------------------
toggle 勾选开关项
格式:
[subject]项目名:toggle,地址,ON的处理值,OFF的处理值
[subject]项目名:toggle,开始地址,结束地址-ON的处理值,OFF的处理值
例如:
[subject]无限道具:toggle,0x400000,75,76
说明:当选项被勾选时,向地址0x400000写入一次75,当选项上的勾被去掉时,向地址0x400000处写入一次76。这个一般都是用来执行对程序机器指令的修改。
再例:
[subject]存档全开:toggle,0x500000,0x
说明:当选项被勾选时,将地址0x500000~
--------------------------------------------------------------------------------
================================================================================
至于还有一些功能上面没有列举的,可以到自己到修改器的官网上下载一些官网上发布的SSG文件,不过官网上的SSG文件都是很老的游戏了。另外还有猫缶Index的网站,也有一些比较新的游戏的SSG文件可以下载,主页地址是:http://www.necocan.info/
SSG文件的下载页面地址是:http://www.necocan.info/001/fssgfiles.html
直接下载文件的链接是:http://www.necocan.info/001/dat001/1121Dredcat.exe
这是个自动解压程序,指定目录解压后里面有一个[Redcat]子目录,里面有很多游戏的SSG文件,可以将[Redcat]整个目录一起复制到修改器的SpoilerAL\SSG子目录下。注意这些SSG文件都是日文的,所以在运行修改器的时候需要使用Apploc用日文启动。也可以用Apploc用日文启动记事本,然后用记事本打开这些SSG/SSL文件,就可以自己研究一下上面没有列举的功能是如何编写的。