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

口袋妖怪绿宝石——数据提取与代码分析(5-THUMB汇编指令基础)

2022-08-19 10:54 作者:围巾胖头鱼  | 我要投稿

说在前面:

    前面几期专栏主要讨论的是ROM数据提取的内容,涉及到文本信息、符号表相关的数据详情以及地图信息。数据提取的部分并没有说完,例如还有一类重要的数据——脚本——没有提到,只不过要想理解ROM中的脚本数据,需要先有一些代码分析的基础。

    本期专栏主要介绍ROM中最主要的一种代码类型:THUMB汇编指令。为了讲清楚这个专业词汇的来源,还有许多背景知识需要交待。

代码是什么

    代码是让计算机执行操作的指令。这种命令从理论上来说,只有计算机自己才能“看得懂”,而计算机只能“看懂”0和1,也就是0/1序列。ROM是一种二进制文件,里面到处都是0/1序列,有的是数据,有的是代码,代表代码的那一部分0/1序列正好是计算机可以“看得懂”的指令。这种0/1序列表示的代码被称作机器语言

    顾名思义,机器语言是给机器看的,不是给人看的(并不排除有的大佬能够看懂机器语言)。为了让机器语言方便人们理解,汇编语言诞生了。汇编语言采用了一系列英文单词(术语称为助记符,mnemonic)和特定的格式来描述计算机指令。注意,这只是描述,而不是指令本身,指令本身只能是计算机“看得懂”的机器语言。为了让机器也能看懂汇编语言,需要借助一个“翻译”,“翻译”的专业术语称作汇编(assemble),执行“翻译”操作的“翻译员”就是汇编器(assembler)

    先举一个例子,在绿宝石ROM的环境下,下面两行的第一行是机器语言,第二行是和它等价的汇编语言:

    这里的00 20是字节序列,可以看做是0/1序列在十六进制下的表示,它只是用于节省空间的一种表示方法,并没有改变这是0/1序列的事实(按照二进制的写法,就是00000000 00100000),不要以为这里有个2就不是0/1序列了,2只是二进制0010的十六进制表示。

    第二行是一条汇编代码。在汇编语言中,一行只有一条代码。汇编器做的工作就是将第二行翻译成第一行,这样由人类程序员写出的第二行代码,在汇编器的翻译下,就可以变成能让计算机执行的第一行。

    如果把这个过程反过来,例如ROM里面都是0/1序列这种机器代码,需要某种操作把它转化成人类能够看懂的汇编代码,这种操作就称作反汇编(disassembly)。回到上面那个例子,反汇编的作用是把机器代码“00 20”“翻译”成汇编代码“movs r0, #0”。

    很多模拟器都有反汇编的功能,例如VBA。很久之前那个介绍金手指的专栏究极绿宝石5.3——科普向,什么是金手指(二)里面,是把VBA的反汇编功能当做了变量查看器,其实变量查看器的功能交给“VBA——工具——内存查看器”更合适一些,把反汇编功能用来查看变量有些大材小用了。

    所以,我们从ROM中看到的代码到底是什么代码呢?如果不借助任何工具,直接打开ROM这个二进制文件的话,我们看到的就是机器代码。如果借助模拟器或其他工具的反汇编功能,我们就可以看到汇编代码。还能再进一步吗?

    在机器语言和汇编语言之上,还有一种被称作高级语言的代码类型。绿宝石源代码项目使用的C语言就是一种高级语言,其他类似Java、Python等编程语言也都是高级语言。但是目前还没有任何一个工具能够将ROM中的汇编代码“翻译”成高质量的C代码。

    这里需要指出,汇编代码“翻译”成C代码是存在“翻译质量”这种说法的(机器代码到汇编代码就没有“翻译质量”这个概念,它们是一一对应的关系),最主要的原因就是C语言中的各种常量名、变量名、函数名、宏等等符号的名称,在汇编语言中全消失了。汇编语言中没有任何符号的名称(虽然汇编语言本身可以有label定义的标签名,但是绿宝石ROM中找不到这种东西),有的只是各种各样的地址。绿宝石源代码项目里的C代码,可以看做是原版绿宝石ROM中汇编语言的一个高质量翻译版本,它高质量的一处重要体现在于从符号的名称可以大致猜出该符号的含义。

    在面对绿宝石改版游戏的ROM时,一般是没有用高级语言写成的源代码来分析的(除非是制作组成员),有的只是借助模拟器等工具通过反汇编得到的汇编语言。但是,绿宝石改版毕竟是基于原版绿宝石做出的修改,许多代码和原版绿宝石还保持着高度的相似性,而这部分相似性就让原版绿宝石的源代码项目有了用武之地,让汇编代码的分析速度和质量有了质的飞跃,这就是我们的机会!

汇编代码和机器代码怎么转换

    按理说,这种转换的事情交给模拟器的反汇编功能就可以了,之所以这里还需要再说明一下,是因为转换分为两种模式:ARM模式和THUMB模式。

    由于解释清楚这两种模式需要很多计算机体系架构方面的知识,这和本系列专栏的关系不大。因此这里只给出一个结论:在绝大多数情况下,口袋妖怪绿宝石的ROM文件内,使用的代码都是THUMB模式。这样,在VBA的反汇编窗口里应该选什么就不会纠结了:

一般而言都选择THUMB模式

     除了使用模拟器进行汇编代码和机器代码之间的转换,利用在线网址:

    https://armconverter.com/

    也很方便,以上图为例,VBA把080003a4处的数据b5f0翻译成“push {r4-r7,lr}”,可以在网站中验证一下:

在线转换工具

    填进去的时候要写成f0 b5,因为ROM中的数据永远需要用小端序的方式拼接。在右侧的THUMB转换结果中出现了“push {r4,r5,r6,r7,lr}”,这和“push {r4-r7,lr}”是等价的:r4-r7就是r4,r5,r6,r7的一种简化写法。

    另外还可以看到,网站右侧的THUMB下面还有个THUMB Big Endian的输出栏,这里的Big Endian就是大端序,和小端序恰好反过来。如果在左侧输入栏输入b5 f0的话,右侧那两个THUMB和THUMB大端序的结果就会颠倒过来。

THUMB汇编指令的含义

    现在我们的目标明确了:利用反汇编工具获取ROM中的THUMB汇编指令,结合原版绿宝石源代码进行代码分析。但在分析汇编指令之前,首先要搞清楚每条汇编指令都是什么含义。

    汇编指令的文档在这个网址内:

    https://developer.arm.com/documentation/ddi0210/c/Introduction/Instruction-set-summary/Thumb-instruction-summary

    上面的是官方文档,还有一个文档解释得也很详细,这是一个开发GBA游戏的爱好者写的,TA的网站上还有很多和GBA开发相关的知识:

    https://www.coranac.com/tonc/text/asm.htm

    接下来在分析汇编代码的时候,专栏也会随时给出汇编指令的含义,方便读者理解。

    理解汇编指令的含义还需要了解寄存器这个概念。上面提到过,ROM的汇编语言中没有变量名,有的只是作为暂时存放数据的容器:寄存器(在高级语言中,这种存放数据的容器就是变量)。可用的寄存器共有16个,名称从r0到r15,其中r0到r12是通用寄存器,可以进行各种操作;剩下的3个寄存器是专用寄存器,各有各的用途:

  • r13,别名sp,保存当前程序栈的栈顶

  • r14,别名lr,保存通过BL指令调用函数的返回地址

  • r15,别名pc,保存当前要执行的指令所在的地址

    这里提到的几个概念:“程序栈”“调用函数”“返回地址”等等会在后面用到的时候进行解释。现在只需要知道,ROM里的THUMB指令,翻来覆去能操作的,无非就是数字(术语称作立即数)、地址和寄存器而已。

    每条汇编指令的格式基本上都是“指令助记符 操作数”的格式,这两个概念在分析汇编指令时会反复用到。

    和一些高级语言不同,汇编语言里的字母没有大小写的分别,BL指令和bl指令是同一个指令。

绿宝石游戏程序的主函数

    使用过C、Java、Python或者其它编程语言的读者们都很清楚,任何一个程序能够运行起来都必须要有一个被称作主函数的函数(也叫main函数),例如下面这个最简单的C语言Hello World程序:

    这个程序可以不定义任何变量,可以不定义任何其他函数,但是至少要有一个主函数,这也是整个程序开始执行的地方。

    口袋妖怪绿宝石作为一个游戏,也是一个程序,它也要有自己的主函数,这个主函数在符号表内的名称是AgbMain,地址位于080003a4处。我们先来看看源代码项目中的AgbMain函数:

主函数

    可以看到,主函数开头的部分几乎都是函数调用,这对于绿宝石这样一个庞大的游戏程序来说是很正常的。我们真正感兴趣的,是这段代码如何与ROM中的汇编指令对应起来,接下来用VBA的反汇编功能查看080003a4这里的汇编代码:

主函数的汇编代码

    下面我们详细分析一下:

    这是主函数的前三行,做的事只有一件:将部分寄存器的值保存到程序栈中。这里可以提一下程序栈的概念。因为寄存器实在是太少了,每个函数都要用。在函数进行调用的时候,调用函数用到的一些寄存器(里的数值)需要暂时存放起来,这样这些寄存器就可以空出来给接下来的被调用函数使用。这个暂时存放的位置就是程序栈

    push指令执行的操作就是把寄存器(里的数值)存放在程序栈内,与之对应的指令pop做的事正好反过来:把程序栈里的数值拿出来恢复到对应的寄存器内。有一个规律可以帮助记住这两个指令:push指令最常出现在一个函数的开头,pop指令最常出现在一个函数的结尾

    中间第二行的mov指令是最常见的指令,如果把r7和r8都看做变量名的话,mov指令就是一个赋值操作:r7 := r8,将r8内的数值复制到r7内。

    这三行汇编指令在源代码中没有对应,这是汇编代码给函数调用添加的额外指令。

    接下来的两行汇编代码对应到源代码的第92行。源代码的第91行和第93行被称作预编译指令,这种以#开头的指令是不会体现到汇编代码中的。

    源代码第92行是一个函数调用,汇编指令bl就是函数调用功能,它的操作数082E70A8就是被调用函数所在的地址,通过符号表可以查到这个地址对应的函数名正好就是RegisterRamReset。

    那第一行把r0赋值为0xff是做什么用的呢?注意到RegisterRamReset函数有个参数,叫RESET_ALL,通过VS Code的“转到定义”功能很快可以找到RESET_ALL的定义就是0xff,这说明r0是作为参数传递给了函数RegisterRamReset,这里就可以引入传递函数参数的概念了。

    在汇编语言中,函数调用使用的bl指令只有一个操作数,就是被调用函数的地址,如果需要传递参数,需要用到寄存器;如果参数太多,还需要用到程序栈。在口袋妖怪绿宝石的ROM中,函数参数默认使用r0-r3这4个寄存器(当函数参数不超过4个的情况下),这也是为什么AgbMain这个函数在开头的时候没有把r0~r3这4个寄存器放到程序栈里面,因为它们默认就是要让被调用函数使用的,因此不需要暂存起来以便之后再恢复。

    注意汇编代码中函数调用这一行(bl $082E70A8),它使用了4个字节(e6 f2 7c fe)来表示一条指令。网上有的资料会说THUMB模式的代码都是2个字节对应一条指令,只有在ARM模式下才是4个字节对应一条指令。事实上,绿宝石的ROM里采用的THUMB模式是一种扩展的THUMB模式(THUMB-2模式),这种模式也支持使用4字节来编码一条THUMB指令。

    如果把函数调用这一行的4个字节放到在线转换的网站上,会得到一个和VBA反汇编不一样的结果:

函数调用的汇编指令

    网站上给出的结果是bl #0x2e6cfc,为什么和VBA中的bl $082E70A8不一样呢?

    其实它们是一样的,关键在于bl这条指令的操作数是相对地址,相对于这条指令本身所在的地址。这条指令位于080003AC处 ,如果进行这样一个运算:0x080003AC+0x2E6CFC,得到的结果就是0x082E70A8。VBA模拟器自动进行了从相对地址到绝对地址的转换,但是在线网站不知道这条指令的地址是什么,就只能给出它最原始的样子。

    为了让在线网站给出和VBA反汇编相同的结果,需要利用在线网站左下角Offset处的功能,把这条指令的地址填写进去:

填入代码地址后的转换结果

    此时在线网站的转换结果就和VBA反汇编一样了。

    先来理解一下源代码第94行的含义。BG_PLTT本身是个数字,被(vu16 *)进行强制转换后变成了指针,外面再用一个*运算符表示取这个指针的内容。所以这行代码的含义就是在BG_PLTT指向的位置保存RGB_WHITE。

    回到汇编代码,第一行是把r0赋值为0xa0,第二行的lsl是逻辑左移运算,lsl是logic shift left的缩写,和它相对的指令有两个,lsr(logic shift right)逻辑右移和asr(arithmetic shift right)算数右移,等到遇到右移指令的时候再进行说明。

    lsl的操作数有3个,含义是把第二个操作数,左移第三个操作数那么多位,然后赋值给第一个操作数。如果按照C代码来写,就是

    可以用计算器算一下,0xa0左移0x13位的结果是0x5000000,这个数值正好就是BG_PLTT。

    不知道读者有没有疑惑,前两行汇编代码其实就是将0x5000000赋值给r1这个寄存器,为什么不能写成一行:

    要解释清楚这个问题需要了解2个字节的机器代码是怎么“翻译”成汇编代码的。详细的原因专栏就不再解释了,可以理解为2个字节的内容有限,无法编码0x5000000这么大的数字。

    第三行汇编指令,ldr是读取数据的指令,从一个地址处读取4个字节。如果是读取2个字节,指令就是ldrh,如果只读取1个字节,指令就是ldrb,这里h是half word(半字)的简写,b是byte(字节)的简写。把一个数字或者寄存器用中括号括起来的含义是把操作数当做指针来使用,所以这行汇编代码就是把08000468处的4个字节赋值给r2,相当于C语言中的:

    VBA模拟器的反汇编功能自动增加了一条提示,也就是(=$00007fff),这就是08000468处的4个字节,免去了还需要去这个地址再查看一遍的工夫。这也是一个稍微有点“智能”的反汇编器应该具有的功能。可以验证,RGB_WHITE的值就是0x7fff。

    和函数调用指令bl类似,如果有感兴趣的读者把这条指令对应的2个字节放到在线网站上去转换,会发现它用的也是相对地址。也就是说,08000468这个地址也是VBA模拟器自动转换的结果。

    再下一行,add指令是加法运算,等价于C语言中的

    最后一行,strh指令,它是str指令中的一个,str和ldr指令相对,ldr是读取,str是保存,后面可以添加h或者b这种后缀表示读取/保存的字节数,什么都不加就是4个字节。strh就是保存2字节,这行代码相当于C语言中的:

    用中括号括起来的部分——[r1, #0x0] 表示以一个寄存器(的数值)为基本地址(基址),向后偏移一定数量的字节(偏移量)后形成的地址,这里的偏移量是0,所以和[r1]是一样的。

    之前没接触过汇编语言的读者可能又有了一个疑惑。前面提到的mov指令相当于C语言中的赋值,那为什么还需要ldr和str这两条指令?换句话说,这两条指令:

 和这两条指令:

是不是等价的?

    这就要说到汇编语言中的寄存器和C语言中变量的区别了。在C语言中,只要定义好了一个变量(必要时还需要分配好它的空间),把它放在赋值运算的左侧或者右侧都是可以的。但是在汇编语言中,像mov或者数学运算这种指令只能对寄存器直接操作,而不能对内存直接操作。这里所说的内存就是用中括号括起来、用数字表示指针指向的那些区域。

    所以,后面的两条mov指令本身就是不合法的。

    整个ROM是内存的一部分,回顾一下ROM这个名字的来源(Read-Only Memory),在汇编语言中对ROM里的数据只能读取(ldr指令),而不能写入(str指令),这就是它被称作只读存储器的原因。

    有了上面一些指令的知识基础,再分析接下来的代码就不难了,因此下面只分析还没有提到过的汇编指令。

    这里出现了两个新的指令:比较指令cmp和条件跳转指令beq。顾名思义,这里就是比较r0和0x1的大小,如果相等(eq是英文equal的简称),就跳转(b是英文branch(分支)的简称,在THUMB汇编指令中,所有的跳转都被称作“分支”,和其他汇编语言中用j来简称jump有所区别)。这种以b开头的指令可以称作分支指令家族,包含了条件跳转指令和无条件跳转指令。

    也就是说,如果r0和0x1是相等的,就会跳过下面两行代码,直接到08000414这里继续执行。和C代码对比就知道,这里的r0就是gFlashMemeryPresent,0x1就是TRUE,如果相等,条件语句if里面的代码就不会执行。汇编语言只能通过各种以b开头的指令来实现跳转。

    除了beq,分支指令家族还有许多其他的成员,就像除了等号之外,还有许多比较大小关系的运算符那样:有不等号,就有bne;有小于号,就有blt和blo;有小于等于号,就有ble和bls……其中比较大小还分为有符号数的比较大小,和无符号数的比较大小,这些都等到专栏用到的时候再作说明。

    上面提到的函数调用指令bl也是分支指令家族的成员,其实仔细想想就能明白,函数调用无非也就是程序跳转到另一个地方执行,函数调用也是一种“跳转”。

    在源代码AgbMain函数的第122行,出现了:

    并且在这个for循环代码块内没有出现break或者return这种跳出循环的代码,也就是说,代码在这里陷入了死循环。

    这正是一个游戏程序应该具有的特点。试想一下,如果游戏程序也能运行结束的话,那么此时的玩家就再也无法进行游戏中的任何操作了,这只能是玩家结束游戏的时候才会做的事。而结束游戏只需要关闭模拟器就可以了,这是在“硬件上”强行终止了这个死循环的程序。在游戏还在运行的时候,无论玩家进行什么操作,游戏程序本身都要给出响应,这就注定了游戏程序要以一种死循环的方式不断等待玩家的输入。

    这条汇编代码执行了死循环的逻辑:

    这种后面没有任何后缀的b指令就是无条件跳转指令。它不需要经过任何判断就会跳转到指定的位置。可以看到,这里跳转到的位置是在当前代码所在位置的前面。前面提到过,这些用到地址的指令采用的都是相对地址,之所以显示出来0800042a是因为VBA模拟器做了自动转换。前面我们看到“向后跳转”的相对地址是什么样子的,现在再来看这种“向前跳转”的相对地址:

向前跳转的指令

    0800042A处的两个字节是b4 e7,在线网站转换的结果是b #0xffffff6c。了解有符号数的读者们可能会马上明白,这里的0xffffff6c其实是个负数,负数的相对地址才会导致“向前跳转”的效果。

到底分析出来了什么

    分析了这么久,到底分析出来什么了?有的读者会说,我还是不知道AgbMain函数到底要做什么,调用的什么RegisterRamReset函数又是InitGpuRegManager函数根本不知道是什么意思啊!

    的确,对于主函数而言,它调用的函数一定是非常上层的函数,无数的细节隐藏在被调用函数中、甚至是被调用函数的调用函数中、一层一层深入下去……要解释清楚每个函数都是做什么的,估计可以出一本书了。

    上面的分析过程,只是想让读者们体会一下C语言的源代码程序和汇编语言的相似性,这样在面对没有源代码的改版ROM时,可以迅速找到改版ROM里的代码和原版代码的对应关系,毕竟用C语言编写的源代码项目是非常宝贵的资源,要尽可能充分地利用。

    可以说,分析改版的ROM,就是先找到改版ROM和原版ROM的相似之处,然后再分析改版ROM的不同之处

改版ROM的主程序在哪儿

    原版ROM由于有符号表这个方便的工具,找到AgbMain函数的位置是非常方便的。但是改版的ROM很有可能改变了AgbMain函数的位置。如果能找到主函数的位置,无疑是在ROM这个二进制文件的“大海”中找到了一个“定海神针”。

    由于主函数是游戏程序运行的第一个函数,找到它的难度会大大降低,在这里有必要说明一下,模拟器在把ROM加载进来之后,是怎么找到主函数在哪儿的。

    之前曾经有不止一期的专栏提到过,ROM加载到模拟器之后,位于模拟器内存地址0x08000000处,绿宝石ROM的特点在于,08000000处一定是一个汇编代码的跳转指令,这就是模拟器运行的第一条汇编指令(它不属于任何一个函数,因此和游戏程序运行的第一个函数是主函数并不矛盾)。

    前面提到过,ROM中绝大多数的汇编指令都是THUMB模式的,那么那些“极少数”的ARM模式指令呢?其实运行主函数之前的这些汇编指令就是那些“极少数”的ARM模式指令,可以看看VBA的反汇编里面08000000处的“第一条指令”:

第一条指令

        第一条指令是b $08000204,注意只有选择了ARM模式才能看到VBA翻译出来的这条指令。无论是什么样的改版ROM,在08000000处一定是一条ARM模式的跳转指令,它跳转到哪里了呢?

    来到08000204,还是需要使用ARM模式查看:

跳转后的结果

    这里也有若干行ARM汇编指令,它没有函数调用常见的push和pop指令,因此这也不算是个函数。里面的汇编指令是ARM模式的,和THUMB模式指令有些区别,但一些关键指令的含义没有变化,比如mov和ldr。这里不再一行一行去分析每条指令的含义,只是说明一下,如果把打开绿宝石ROM游戏类比到给电脑开机的话,这些指令类似于把一台电脑打开后、但是在桌面显示出来之前,计算机运行的代码。

    一般来说,改版的ROM不会对这部分代码有很大的修改,只是有些地址可能会变化。上面截图中的最后一行是个跳转指令,但它跳转到的是这段代码的开头,也没有跳出这个范围。剩下的就只有一条跳转指令:位于08000230的bx指令。

    bx也属于分支指令家族,它和bl有些类似,是专门为了调用函数设计的指令,但是它还有一个额外的功能,就是调用这条指令会导致汇编指令的模式发生切换,现在是ARM模式,等运行完bx这条指令后,就切换到THUMB模式了。bx跳转到r1保存的数值处,向上看可以找到r1从一个地址处获得4个字节的数值080003a5,这就是主函数的位置。

还有一个重要的提示

      有的反汇编器给出的反汇编结果可能和VBA模拟器稍微有些区别,最主要的区别在于有些指令的后面会加上一个s,比如mov变成movs,lsl变成lsls等,这种加s的指令常见于数据转移指令(mov系列)、数学运算指令(加减乘除、移位等),而不会出现在分支指令家族、内存操作指令(ldr, str, push, pop等)、比较指令(cmp等)

    加不加s的指令本质上没有区别,如果有的读者看到这种末尾带一个s的指令,默认可以把它去掉(当然需要分清楚这个s究竟是不是添加上的,比如跳转指令bls,这里的s不能去掉,因为ls表示的是“小于等于”的意思,上面也说到了跳转指令中不会出现这种额外添加的s)。

    本期专栏涉及到一些比较专业的知识,对于之前没有了解过的读者们可能有着不小的阅读难度。为了对绿宝石ROM有更深刻的理解,或者功利一点,为了找到更厉害的金手指,了解这些知识是有必要的。当然,如果是由于作者的表达水平有限,给读者带来了困扰甚至是误导,请在评论区指出,读者提出的好的意见和建议是促使作者提升表达能力的重要基础。

    绿宝石源代码里的函数成千上万,逐一分析是不现实的。为了能在分析代码这个枯燥的过程中还能感受到哪怕一点点的乐趣,接下来的几期专栏会紧密结合金手指的探索过程,尽量和游戏本身的内容靠拢。否则忙碌了半天,到头来连自己为什么忙碌可能都忘记了(虽然在这个过程中能力得到了不知不觉的提高)。

    再次感谢众位读者的支持!

口袋妖怪绿宝石——数据提取与代码分析(5-THUMB汇编指令基础)的评论 (共 条)

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