由于水平有限,分析的过程和文章可能会存在漏洞已经错误的地方,欢迎大家对有疑问的位置提出问题,共同讨论,一起成长 :)
一、启动过程
当gameboy通电后,机子会从内存地址为0的地方开始运行一段长度为256字节的程序,这段程序是固化在gameboy内部的ROM(只读存储器)上的。
这段程序的作用是,把卡带中从104H到133H地址的任天堂的LOGO读取出来并显示在屏幕的最上方。这个LOGO会滚动到屏幕中间,然后会播放两个提示音。然后会把从这个地址段读出来的数据和内部数据进行比较,如果比较失败,则停止运行。如果比较通过,则把地址从134H到14DH的数据逐个相加,再把相加的结果加25,如果最后得到的结果的最低有效位不为0,则停止运行。否则,内部程序运行结束,机子会从卡带地址100H处开始执行实际的游戏指令,同时设置寄存器为以下值:
AF=$01B0 BC=$0013 DE=$00D8 HL=$014D Stack Pointer=$FFFE [$FF05] = $00 ; TIMA [$FF06] = $00 ; TMA [$FF07] = $00 ; TAC [$FF10] = $80 ; NR10 [$FF11] = $BF ; NR11 [$FF12] = $F3 ; NR12 [$FF14] = $BF ; NR14 [$FF16] = $3F ; NR21 [$FF17] = $00 ; NR22 [$FF19] = $BF ; NR24 [$FF1A] = $7F ; NR30 [$FF1B] = $FF ; NR31 [$FF1C] = $9F ; NR32 [$FF1E] = $BF ; NR33 [$FF20] = $FF ; NR41 [$FF21] = $00 ; NR42 [$FF22] = $00 ; NR43 [$FF23] = $BF ; NR30 [$FF24] = $77 ; NR50 [$FF25] = $F3 ; NR51 [$FF26] = $F1-GB, $F0-SGB ; NR52 [$FF40] = $91 ; LCDC [$FF42] = $00 ; SCY [$FF43] = $00 ; SCX [$FF45] = $00 ; LYC [$FF47] = $FC ; BGP [$FF48] = $FF ; OBP0 [$FF49] = $FF ; OBP1 [$FF4A] = $00 ; WY [$FF4B] = $00 ; WX [$FFFF] = $00 ; IE
二、Rom文件的反汇编
好了,现在知道了ROM的文件结构(参考上一篇文章),知道了gameboy从ROM的什么地方开始执行指令,接下来就要对ROM进行反汇编,看看ROM的实际内容是什么东西。
先说一下反汇编的思路:首先按字节从ROM文件中读取单个字节的数据,然后根据gameboy的CPU指令手册,把读出来的数据翻译成相应的CPU指令输出成文本,这样就能看到ROM的内容了。
有了思路之后我们就开始着手实现吧,嘿嘿嘿,首先需要一个gameboy的CPU指令手册,可以从网上找到相应的资料。这个是我找到的CPU的参考资料:http://www.myquest.nl/z80undocumented/z80cpu_um.pdf
从资料中可以知道,gameboy用的CPU是8位的,从程序的角度讲,就是CPU一次能从内存中读取8个位,刚好一个字节的数据到内部,所以我们要逐个字节分析,接下来这个是gameboy的CPU所用到的指令:http://pastraiser.com/cpu/gameboy/gameboy_opcodes.html
从表格中我们可以知道每条指令的长度,以及每条指令对应的二进制数,而且我们看可以看到有些指令是用一个字节表示,有些指令是用两个字节表示,而且两个字节的指令都是以十六进制数CB开头的。注意:一条完整的指令应该包括指令的操作符和操作数,这里说的一个字节、两个字节只是用来表示指令的操作符,不包含操作数,所以完整的指令长度还应该包含标识指令操作数的长度。我们可以先获取到指令的操作符,然后再根据地址中给出的表格来查询出指令的操作数以及完整指令的指令长度。
对于两个字节长度的操作符,CPU会先读第一个字节,如果第一个字节是CB的话,则再读下一个字节,这样根据第二个字节读到的值,就可以通过查表来确定该两个字节长度的操作符是什么了。
那么CPU怎么知道它读出来的就一定是操作符,而不是操作数或别的什么东西呢。
首先,CPU一定是从读取第一个指令的操作符开始的,然后通过翻译该操作符,再获取到这条指令使用到的操作数。接着执行该条指令,执行完后再去读取下一个指令。所以这就要求CPU在翻译指令的时候,最先读取出来的一定是指令的操作符,这样才能确定该条指令的长度,以及实用那些操作数等这些指令的附加信息。
所以,我们在模拟gameboy的CPU进行翻译指令的时候(也就是通常所说的译码),首先读取进来的一定是CPU的指令的操作符,这时如果读取的值是CB,则我们就可以知道当前处理的是一个双字节长度的操作符,接着我们需要读取下一个地址的字节数据,然后通过这次读取出来的数据来确定当前世纪的操作符是什么。
好了,思路理清楚了,就做一个小工具来验证一下吧,结合上一篇对ROM文件格式的分析,做一个查看gameboy rom数据的小工具,分析的第二次机器人大战G的rom(我的gameboy的第一个游戏啊~~),先来个截图:
第一个红框框住的是从ROM里面读取出来的游戏的名称,第二个框框住的是初步反汇编出来的代码,可以看到里面每条指令的操作数还不是真正的操作数,暂时只是个操作数的占位符,随着学习的深入,会进一步对这些数据进行分析。在进入本次的代码分析之前,先说下我在学习过程中遇到的两个个问题:
就是在反汇编ROM的过程中,发现有些字节无法进行反汇编,就是在CPU的指令表中无法找到该字节数据对应的指令,在网上查找资料后,有的朋友说是因为ROM除了包含指令和数据外,还包含有图片、声音等信息,也可能有部分的ROM数据会被加密过,所以有时候简单的反汇编并不能完全的把ROM的所有内容都展现出来。
第二个问题,请看截图中的一黑一蓝的两个区域,在这两个区域的左上角有一个红色和黑色的小矩形,矩形的内容是从ROM读出来的任天堂的LOGO数据,可是显示出来之后怎么看都不像一个LOGO。
望有高手能够帮忙点拨一下,J
好,下面来看下我自己写的代码,献丑了:
private void AnalyzieFileContent(byte[] aFileContent) { //get 512Kbyte content to analy byte[] mEntry = new byte[4]; byte[] mAnalyAssembly = new byte[512 * 1000]; Array.Copy(aFileContent,0x100, mEntry,0, 4); StringBuilder mSB = new StringBuilder(); mSB.Append("Entry:"); Instruction mCurrentInstruction = InstructionConfig.SinglOpCodeConfig[mEntry[1]]; mSB.Append(mCurrentInstruction.Symble); byte[] mAddress = new byte[2]{mEntry[2],mEntry[3]}; mSB.Append(" " + Convert.ToString(BitConverter.ToUInt16(mAddress, 0), 16)+"\r\n"); Array.Copy(aFileContent, BitConverter.ToUInt16(mAddress, 0), mAnalyAssembly, 0, 512 * 1000); for (int i = BitConverter.ToUInt16(mAddress, 0); i < aFileContent.Length; ) { byte mFirstByte = aFileContent[i]; byte mSecondByte; if (i + 1 < aFileContent.Length) { mSecondByte = aFileContent[i + 1]; if (mFirstByte == 0xCB) { //i++; mCurrentInstruction = InstructionConfig.MultipleOpCodeConfig[BitConverter.ToUInt16(new byte[] { mSecondByte, mFirstByte }, 0)]; } else { if (InstructionConfig.SinglOpCodeConfig.Keys.Contains(mFirstByte)) mCurrentInstruction = InstructionConfig.SinglOpCodeConfig[mFirstByte]; else { i++; mSB.Append("Error code map:" + mFirstByte +" at address " +i ); mSB.Append("\r\n"); } } } //mCurrentInstruction = InstructionConfig.SinglOpCodeConfig[mFirstByte]; i += mCurrentInstruction.Length; mSB.Append(mCurrentInstruction.OpCode.ToString()); mSB.Append(":"); mSB.Append(mCurrentInstruction.Symble); mSB.Append( " "); mSB.Append(mCurrentInstruction.Operand); mSB.Append("\r\n"); } txtAssembly.Text = mSB.ToString(); }
由本文的第一部分可知,系统从Rom的0x100处开始运行,而这里始终都是00开头,后跟一个JP指令,JP指令后面跟着的就是跳转的地址,在此需要注意,因为gameboy使用的是小字节序(little endian),就是对于一个长度大于一个字节的内容,改内容的低字节位会存入内容的低地址中,高字节位的数据会存入内存的高地址中,举例来说,JP的操作数是两个字节的,如文本中读取到的操作数是8A 08 ,把它还原为真正的操作数应该为08 8A,所以,ROM会跳转到08 8A地址开始执行指令。
好了,本次就先到这吧,附上本问中用到的小工具的代码。(话说园子现在不让在文章里面直接加附件了吗?)
注意:在本文中图像内容的显示使用的是XNA来实现的,对于XNA的内容我也是刚开始学习,嘿嘿,这里粘上园子里前辈的文章以供参考:
http://www.cnblogs.com/clayman/category/191000.html
http://blog.csdn.net/soilwork/article/category/207496 (同样是上面的这位前辈在CSDN上的博客,XNA入门推荐)