由於水平有限,分析的過程和文章可能會存在漏洞已經錯誤的地方,歡迎大家對有疑問的位置提出問題,共同討論,一起成長 :)
一、啟動過程
當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入門推薦)