究极绿宝石5.3——科普向,什么是金手指(二)
说在前面:
回顾一下上一期的内容,最核心的是“你所玩的电子游戏,本质都是在操作数据”。电子游戏中 的一切,无论是图片、动画还是逻辑,都可以看做是数字,正常的游戏操作是改数字,使用金手指也是改数字。在结尾的部分,专栏中提出了三个有关“金手指”的问题:
我们想要改的数字在哪里——变量地址查询
我们要改的数字是什么含义,或者说我们想改的东西对应了什么数字——变量与实体的映射表
修改了之后不起作用、甚至存档都损坏了怎么办——修改变量的副作用
第二篇专栏,主要介绍第一个问题:我们想要改的数字在哪里?

变量和它的地址
现在我们用一下稍微专业一点的术语来描述“我们想要改的数字在哪里”这个问题,其实是“我们想要改的变量,它的地址是什么”。变量是编程中的一个术语,含义和数学中的变量类似,通常情况下有一个名称和对应的取值,例如变量a的取值是1,变量b的取值是10。变量这个概念是和常量对应的,常量的取值不能更改,而变量的取值可以被改变,用上一句话的例子,变量a之前取值是1,现在可以把它改成2,之后还可以改成3或者4,一个直观的比喻就是变量名称a是个容器,而里面的内容(取值)是可以任意改变的,再通俗一点就是a是个筐,里面装苹果还是梨都行,也可以什么都不装,只不过我们需要把“装苹果”“装梨”“什么都不装”这三种状态对应到数字,这是下一期专栏的内容。
编写程序时,变量的名称是程序员指定的,TA想把这个变量取什么名字就取什么名字(当然合格的程序员取变量名是要遵循一定规范的,这不在本专栏的讨论范围内)。我们作为游戏玩家,看到的是一个完整的程序界面,而不是编写程序时充满着各种英文单词(术语叫代码)的界面,变量的名称早已无处寻觅。我们想要找到一个变量在哪里,除了查找它的名称之外,还可以通过地址来找。
就像现实世界中,我们要找某个超市、医院、银行时,需要知道它们在哪条街道、门牌号是多少一样,虚拟世界中的变量同样也有个“门牌号”,这就是它的地址。虚拟世界没有现实世界那么复杂,没有纵横交错的街道,所有的变量都“住”在一条“大街”上,而且这条“大街”还只有单侧,不是两边都“住人”的。程序中的所有变量都“住”在这条“大街”上,每个变量都有一个门牌号。为了描述清楚这条“大街”是什么样子,我们需要具体说明一下游戏运行的环境。

VBA模拟器
该系列专栏以Visual Boy Advance (VBA) 模拟器为例,在windows 10操作系统上运行究极绿宝石5.3程序(这并不是说金手指只能在这种环境下使用,仅仅是因为在电脑上操作起来更方便)。VBA模拟器和游戏本体的gba文件都可以在百度贴吧“究极绿宝石吧”的置顶帖“【冬至发布】究极绿宝石V-虹之抹灭者!!”中下载到。
VBA模拟器为究极绿宝石5这个游戏提供了运行的环境,在VBA模拟器中加载好游戏之后,该游戏的所有变量、代码(代表着游戏逻辑)、资源(图片、动画等)就都进入了VBA模拟器的管辖范围。我们想找的变量“住”的那条“大街”,也在VBA模拟器中,具体来说,是在工具-反汇编...中。

打开工具-反汇编...,我们看到的是一堆让人头晕眼花的、夹杂着数字和字母还有特殊符号的东西。这个“反汇编”名词太过学术,我们可以暂时把它理解为“变量地址查看器”。(下图的红框和蓝框是后来添加的,原本的界面没有这两个框)

有的读者打开看到的也可能是下图的样子,蓝色框稍微窄一点:

蓝色框有宽有窄的原因是这个界面左上角默认选择的是“自动”,如果选择的是ARM模式(点击ARM前面那个圆圈),则会显示上半张图那种宽的蓝色框;如果选的是THUMB模式(点击THUMB前面那个圆圈),则会显示下半张图那种窄的蓝色框。自动模式下,会随机选其中一个。
在本专栏中,我们只关注上图中被红框和蓝框圈出来的部分。其中红框圈出来的部分就是变量的地址——门牌号,右边有个滚动条,一路拉下去就形成了一条“大街”,每个变量都有一个地址,按照从小到大的顺序从上到下排列;蓝框圈出来的部分就是变量的取值,按每一行来看,红框内的一个地址对应蓝框内的一个取值,例如在上图中,地址“00000000”处的变量,取值为“ea000006”(这是上半张图,下半张是“0006”)。不了解什么是十六进制的读者们可能会有疑惑:变量的门牌号(地址)和取值应该都是数字才对啊,为什么红框和蓝框里面会有字母出现呢?接下来我们介绍“进制”这个概念。

进制
“进制”是一个数学上的概念,如果从网上搜索的话有一大堆的资料可以让读者们了解什么是进制。这里为了本专栏的完整性,从个人理解的角度试图让没有接触过“进制”这个概念的读者迅速入门。已经了解进制概念的读者跳过也罢。
我们在日常生活中见到的数字,例如考试中的85分、微信红包里的12块钱、人口统计中的14亿(写开了是1400000000)等等,都是“十进制数字”。这里的“十进制”代表“逢十进一”,也就是说0到9都是一位数,从10开始就变成两位数了,碰到了“十”,数字的位数就会增加一位,我们也称这些数字的“进制”是“十”。从符号的角度来看,阿拉伯数字中其实只有十个符号:0123456789,“10”这个数字本身并没有用一个特殊的符号来代替,而是用“1”和“0”两个符号前后拼接来表示,因此不严格来讲,也可以说只用十个符号的数字就是十进制数字。
十进制数字深入人心,因此在习惯上,十的倍数总会让人看做是“整的数”,比如付钱时如果能凑足10块或者100块,都会说“凑个整”。其实“十”这个数字在数学上没有任何特殊之处,人们之所以采用十进制纯粹是因为人类都长了十根手指(可能有争议,但是至少包括作者在内的很多人都认可这个原因)。古时候人们常用手指计数,当两只手数不过来的时候,每伸出来十根手指,就拿一个小石头或者小树枝来代替这伸出来的十根手指,然后手指再都缩回去重新计数,这样数完之后,看看地上有多少个石头或树枝,再加上伸出来的手指数量,就知道结果了。从这个过程可以看出来,假如人类只长了八根手指,那么很有可能人类的数字系统就会变成八进制的;假如人类长了十二根手指,那么很有可能人类的数字系统就会变成十二进制的。
日常生活中也有其他进制的例子,比如时间和角度按60进制(逢六十进一),一天中的小时按24进制(逢二十四进一),星期按7进制(逢七进一)等等,但是在表示这些数字的时候,我们仍然是按十进制数字的形式表示的,比如说21:19这个时间,我们只会按照“二十一点十九分”来理解,而不会把它看做是别的时间,但如果换个进制就不同了。
最简单无脑的进制就是一进制,纯粹就是看数字的位数来判断这个数字是多少。一进制中只有一个符号,我们可以用“0”,也可以用“+”,或者用“=”,都无所谓,比如“+++++”就代表五,“===”就代表三,“0000000000000000”就代表十六,等等。有个笑话说的是一个小孩学了汉字怎么写“一二三”之后,就以为自己什么数字都会写了,结果让他写个“万”字,写半天都写不出来,这就是一进制的典型应用。如果要写出“亿”这种大数,一进制得浪费多少墨水?但这并不妨碍一进制是最朴素、最原始的进制,看起来再傻,也是一个合法的进制。
下一个就是二进制了,相比于一进制而言有了质的飞跃。在计算机领域中,再怎么高估二进制的作用也不为过,道理和人有十根手指类似:早期计算机的“手”——二极管,只有两根“手指”,就是“导通”和“截止”,一根二极管只能表示两个数,自然就形成了二进制。二进制有两个符号,可以用“+”和“-”,也可以用“*”和“/”,也可以用“0”和“1”,用什么符号本身无所谓,只要能让人理解就好。只是很多介绍二进制的资料,上来就说“二进制的‘1010’就是十进制的‘10’”这种话,很容易让人犯晕,因为前面“1010”中的“1”和“0”,跟后面“10”中的“1”和“0”,虽然长相一样,但含义却完全不同,这时我们只能把“1”和“0”看做是符号,暂时忽略它们的数字属性。
二进制中只有两个符号,而且最常使用的符号就是“0”和“1”,因此采用这种符号的二进制数里面是不会出现“2”、“3”之类的符号的。二进制的特点是“逢二进一”,每当碰到一个二进制单独符号计数计不过来的情况(就是碰到“二”了),就会多出一位数字来,和古代人类一双手数不过来、拿个小石头代表“十”是类似的含义。这样我们从小到大去数二进制的数,第零个是“0”,第一个是“1”,第二个数呢?我们无法再用一位数字表示了,因为“只有两根手指”的我们数不过来了,所以第二个数变成了“10”,这个数字不是“十”,再强调一遍这里的“0”和“1”只是个符号,把它换成“+”和“-”也是一样的,也就是说“+-”这个东西也可以表示二进制中的第二个数,总不会有人认为“+-”就是日常生活中我们用到的那个“十”吧?
接下来,第三个数是“11”,第四个数呢?倒数第一位要“逢二进一”,进位之后,倒数第二位也要“逢二进一”,因此第四个数是“100”,这个数可不是“一百”。像这样依次向后数,就是二进制的世界中对全部数字的描述。
为了避免混淆,二进制的数字往往在最后加上一个下标,例如“1010(2)”表示二进制的“1010”,也就是十进制里的“十”,写成“10(10)”,右下角的小括号内数字表示进制。有了这种区分方式,我们就可以写“1010(2)=10(10)”,含义就是二进制的“1010”等于十进制的“10”。
我们怎么看得出来二进制的“1010”是十进制的几呢?这可以通过观察十进制数来推测:
在十进制下:
或者我们写成
也就是说,从符号的角度上来说,我们把数字“一千三百七十二”写成“1372”,把四个符号从左往右写,在数学上的意义是将这四个符号加权求和,其中权重和两个因素有关:该符号所在的位置,和它所用的进制。符号所在的位置,为了和指数上标对应起来,我们把最右边的位称为第0位(它的权重是),也称为最低位,倒数第二位称为第1位(它的权重是
),以此类推,最左边的是第3位(它的权重是
),也称为最高位。1372(10)这个数是十进制的4位数,权重里面,所有的底数都是10,因为这是个十进制的数字,“逢十进一”说的就是这个意思。
类比到二进制中来:
加权求和、最低位、最高位的概念没有变,只不过权重的底数变成了2,把上面公式的右侧算出来就知道这个数是十进制的10。有了这个办法,就可以进行二进制数向十进制转换的操作了。至于十进制怎么转换成二进制,原理在网上或书上的资料都有,在介绍完十六进制后,会给一个好用的工具。
理解了二进制,就可以看懂一个有关二进制的谜语了:
这个世界上只有10种人,一种是懂二进制的人,一种是不懂二进制的人。
把这句话里的10写成10(2)相信大家就都能看懂了。
再往后的三进制、四进制用处就不是很大了,我们只介绍另一个最常用的进制:十六进制。十六进制是“逢十六进一”,从符号的角度来看,十六进制中一共有16个符号,最通用的是用0~9来表示前十个数字,从10开始,用英文字母a~f来表示(大小写都可以,但需要统一,不能大小写混用,否则就不是16个符号了)。
具体而言,就是说
a(16)=10(10),b(16)=11(10),……,f(16)=15(10)
十六进制之所以常用,是因为1位十六进制数和4位二进制数是等价的(原因就是)。因此用十六进制数表示数字会更紧凑,用8位十六进制数就可以表示32位二进制数。我们在究极绿宝石中用到的所有金手指,以及在变量地址查看器里看到的所有变量的地址和取值,都是用十六进制表示的数字。所以在变量地址查看器中,看到有字母就不会感到奇怪了。
理解进制需要转变一些固有观念,一些早在意识深处认定“理所当然”的事情。再讲一个故事,换位思考一下可能会对理解进制有所帮助:
你在和一个外星人交流各自的数学。你对外星人说人类采用10进制,外星人说很巧,它们用的也是10进制。随着你们交流的深入,你们都发现对方用的根本不是10进制,你认为外星人用的明明是16进制,而外星人认为你用的明明是A进制。
有了二进制和十六进制,就可以介绍计算机领域中字节这个概念了,字节是长度单位,一个字节表示8个二进制位,也就是2个十六进制位。在变量地址中,字节是最基本的单位。
这一节的最后给一个好用的、在各个常用进制之间转换的工具:win10系统自带的计算器:

左上角有一个三个横杠的小图标,在其中可以选择“程序员”类型的计算器,进入计算器界面后,左边有4个进制:HEX表示十六进制,DEC表示十进制,OCT表示八进制(一般用不到),BIN表示二进制。这四个进制可以选中一个,然后输入数字,则其他进制下该数字会自动表示到每个进制的后面,如图中所示,选中的是DEC十进制,输入10的时候,十六进制HEX显示的是A,二进制BIN显示的是1010。这种类型的计算器,或者从网上找到的进制转换器,在金手指的应用中可谓必备工具。

变量地址查看器
言归正传,介绍了大半篇的进制、最高最低位的概念,为的是读者能更深刻地理解金手指的含义。
回到上面这幅变量地址查看器的图片,了解了进制之后,读者对里面的数字应该就不会陌生了:


左边红框表示的是变量地址,第一行为00000000(16),第二行怎么就跳到了00000004(16)(上半图ARM模式)或者00000002(16)(下半图THUMB模式),而不是00000001(16)呢?原因是变量是有长度的,下面我们以上半图为例。我们看一下蓝色框的第一行,这就是位置在00000000(16)的这个变量的取值,这是一个8位的十六进制数,需要占据4个字节,从00000000(16)开始,4个字节之后正好就是00000004(16),也就是说,左边红框里的数字,单位是字节,每个变量占据4个字节的长度,然后依次排列下去。
那变量究竟是占4字节的长度,还是2字节的长度呢?这一点其实无关紧要,只需要知道变量长度的最基本单位也是字节(也就是说没有长度是半个字节的变量),2字节也好,4字节也好,都是连续的一段数字,怎么看都可以,只有赋予其实际意义的时候才需要把它们连起来看,在现在没有实际意义的情况下,完全可以看做一个字节是一个变量,两个连续的单字节变量又可以组成一个双字节变量,两个连续的双字节变量可以组成一个四字节变量,等等。
请注意,由于地址是十六进制数,因此第三行00000008(16)的下一个地址不是00000012,而是0000000c(16),这里避免惯性思维,认为8+4=12就把12放进去,12在十六进制中是c,不是个两位数,真正的00000012(16)在十进制中其实是18。
大家可以尝试把右边的滚动条稍微向上拉一下,可以看到最后一个地址是fffffffc(16)(上半图,下半图的话是fffffffe(16)),也就是说,变量全部的地址,取值范围总共占据了从00000000(16)~ffffffff(16)的空间,填满了整个8位十六进制数。
还有一个重要的地方需要说明:上面两张图表示的变量取值情况其实是完全一样的,也就是说:
和
是等价的,仔细观察不难发现规律,8位十六进制数ea000006被拆成了两部分,分别放在了00000000和00000002两个位置,不过为什么是ea00放在后面,0006放在前面呢?好像有点不符合直观感觉。这就要回顾最高位和最低位这两个概念了,这种变量的拆分方式是高位放在高地址,低位放在低地址(术语叫小端序),ea00是ea000006的高4位,因此放在较高(也就是较大)的地址00000002处,0006是ea000006的低4位,因此放在较低(也就是较小)的地址00000000处。之后有的金手指代码会用到这个规律进行合并。
所以,我们也可以说,ARM模式是变量取值按8位十六进制数显示的模式,THUMB模式是变量取值按4位十六进制数显示的模式。
在反汇编(也就是变量地址查看器)的界面内,上边有一个输入框,在其中输入8位16进制数然后点击后面的“转到”按钮,可以直接看到该地址处的变量取值。
因此,我们在使用金手指之前,学会了一项重要的技能:从游戏中读取变量的取值。如果说金手指是对数据进行了修改(在计算机的术语中,称为写操作),那么通过变量地址查看器看到变量的取值,就是计算机术语中的读操作。有许多用了金手指导致存档损坏(也被称为坏档)的读者正是因为不了解使用金手指之前这些变量的取值情况,在使用之后不能将它们恢复到正常的样子,从而导致的坏档。
知道了变量在哪里,好像意义并没有那么大,这些变量都是一些16进制数,说白了也就是一个表示位置的数字,谁看得懂它们是什么意思啊?这就是下期专栏要介绍的内容——变量与实体的映射表。将一个个抽象的数字与游戏中有具体含义的实体联系起来,才是程序化腐朽为神奇之处。在这里,先给大家一个彩蛋,见识一下读取变量取值的威力。

彩蛋:黄半仙送的蛋里面是什么?
终于回到游戏了!在究极绿宝石5中,通关神奥联盟后,神奥地区随意镇的随意遗迹洞口会打开,在遗迹的上方站着一个黄半仙,他会送你一个蛋,里面可以孵出各种神奇的宝可梦,不仅是神兽幻兽,甚至连极巨化的宝可梦都有(对战一次后失效)。如果孵蛋需要的步数很多很多,有经验的玩家就知道蛋里面一定是好东西。

在学会了本期专栏的内容后,配合下个专栏的内容,我们就可以做到在蛋孵出来之前,提前得知蛋里面是什么精灵!
操作方法:领取到蛋之后,把它放到队伍的首位。

打开变量地址查看器(工具-反汇编),在“转到”按钮之前的框里面,输入0202450C,点击“转到”,会显示地址是0202450C的变量,其取值是多少。每个人的取值不一样,下图是作者临时测试的一个结果:

这里显示的位于0202450c处的变量,取值是02b0,如果我们恰好知道这个数字对应的精灵是三首恶龙,那么我们就相当于在蛋孵出来之前,提前知道了蛋内的精灵是什么。果不其然,孵出来的确实是三首恶龙。

是不是有种未卜先知的感觉?
两个最关键的地方:为什么0202450C处的变量代表了蛋里面是什么精灵?为什么02b0表示的是三首恶龙?这一切都要等下一期——变量与实体的映射表——来介绍了。
这两个神秘数字一现身,金手指也就呼之欲出了,如果我们能修改这个位置处的变量取值,是不是蛋里面就会孵出来别的精灵呢?先别着急,等接下来的几期专栏慢慢介绍~
如果大家还感兴趣的话,作者会继续更新,谢谢众位读者的支持!