精華區beta Emulator 關於我們 聯絡資訊
【雜言】 我有點後悔寫這系列文章了....比我預想的還難寫(抱頭 先說明一下,因為我不可能從計算機概論或寫編譯器談起, 所以這系列的經驗談是假定讀者有資工背景或具基本程式能力。 反組譯這邊則最好有修過組合語言或系統程式這類課程,具備基本概念即可。 一般來說,資工大三~大四學生應該就具備充足知識了.....。 之前三篇中提到的工具我並沒有解釋如何使用,因為那是自己花時間摸一下就會用的。 但這篇的反組譯模擬器我會稍微提一下用法,因為這邊有些"經驗上"的用法。 【正文】 ~PART I. Rom、Ram、VRam~ 跟中文化有關係的SFC記憶體是Rom、Ram跟VRam。 Rom應該都知道了,就是包含程式與資料的遊戲"軟體"部分 (實際是存放在卡匣硬體上,這點就跟存放在光碟中的軟體一樣)。 Ram則是用來放置遊戲記錄與暫存資料的地方, SFC會用7E:0000~7F:FFFF這個特殊SFC位址來代表Ram裡的資料位置。 所以SFC的金手指碼"基本上"都是7E、7F開頭,就是為了處理Ram裡的數值。 我們也隨時可以用snes9x v1.43.ep9r8將遊戲目前Ram的內容dump出來。 http://0rz.tw/cT4l6 (第一個紅框應該在Dump Ram,不是Dump Palette,我畫錯了) VRam則是存放顯示在螢幕上的內容。 根據遊戲設計不同,最多會有4個圖層與一些sprites....。 (如果是用Snes9X的玩家,在遊戲中按數字鍵1~5就能看出圖層來) VRam在每個圖層上要顯示的東西都會分成兩部分: (1)8x8為單位的圖塊(tile): 依模式不同每個點有可能是2bits~4bits(4色~16色)顯示。 基本上這些就像一塊塊可重複使用的拼圖。 (2)每個位置要使用的圖塊編號: 螢幕上(嚴格講是某圖層或sprites)由左到右、由上到下依序使用的圖塊編號。 相當於是指明螢幕上(嚴格講是某圖層或sprites)每個地方要用什麼圖塊。 (以前做的示意圖 http://0rz.tw/NgBGU ) 就結果而言,要看到螢幕上有什麼字就代表VRam裡有該文字的圖塊, 並在要顯示的位置給圖塊編號(不是字庫代碼)。 這些圖塊即便是每個點以2bits(4色)顯示的8x8最小文字圖, 也要用到16 bytes的連續資料來顯示。因此遊戲時常會用DMA的方式, 直接把未壓縮的連續文字圖資料一口氣在Rom、Ram、VRam間搬運,提升處理速度。 (DMA的寫法很重要,以後會用實例解釋,現在先有個印象就好) 以流程來說,要完成文字的顯示工作,我"看過還有印象的"主要有4種方式: (1)Rom --(解壓、挑字)--> Ram --(整個字串)--> VRam 這可能使用在"使用字庫較大,並顯示名詞文本"的情況下。 因為名詞長度一般了不起8個字,但因為字庫太大, 所以先在Rom裡依使用順序挑出字圖,複製到Ram裡成為字串圖, 再把Ram裡整個字串圖複製到VRam顯示出來。 若是有壓縮的字圖就要先解壓,再一個一個byte放進Ram裡, 若是未壓縮的字圖就DMA直接整個字圖傳進Ram裡,效率相對較高。 有時劇情文字也會用這種方式處理。 (2)Rom --(解壓、挑字)--> Ram --(挑字)--> VRam 這可能使用在"使用字庫較大,並顯示劇情文本"的情況下。 在Ram裡排成字串圖會發生的問題就是...有些字會重複被使用。 因為Ram裡還要放一堆其他圖像、數據,所以其實沒太多空間浪費。 如果一段對話很長,這時可以把會用到的字圖挑"一份"到Ram裡, 再根據劇情代碼從這個"Ram裡的小字庫"挑字圖到VRam顯示出來。 有時非字庫的文字圖也會用這種方式處理,例如皇騎1的地圖名稱。 (3)Rom --(挑字)--> VRam 這相當於是上面的簡化版,直接從Rom裡依序挑字到VRam。 但可能因為速度慢(從Rom挑越多字越慢)或因為文字圖必須未經壓縮, 感覺不常被使用在劇情文字。可能用在指令名稱等顯示在固定位置的內容。 (4)Rom --(解壓、整個字庫)--> Ram --(挑字)--> VRam 這可能使用在字庫夠小的情況,特別是遊戲只有使用平、片假名,沒幾個漢字時, 可以輕鬆地把整個字庫全塞到Ram裡。之後只要看文本代碼用到哪些字, 再依序拷貝到VRam就行,對遊戲而言可說是最方便的了..... .....然後做中文化的就淚目了T____T 如果看到某些SFC漢化遊戲改不出"只有平、片假名的小字庫",那可能卡這邊了。 (或是代碼數量不足等其他原因.....也可能只是單純反組譯不熟 XD) 因為這部份我不會提出實例(解釋不完),所以我把我在皇騎1的處理概念解釋一下: <i>想辦法拆成對應不同條件的小字庫: 例如皇騎1戰鬥時可以切換成有動畫或無動畫,攻擊名稱會因而有所差別。 所以我可以把所有攻擊名稱會用到的中文字,重新分成兩個不同小字庫, 各別對應有動畫跟無動畫兩種情形。 這樣只要在原架構下,寫段函式去判斷這時是有動畫或無動畫, 再依情況載入不同的小字庫就行。 缺點是....拆出來的小字庫都要小於原字庫,且可能發生代碼重複使用的問題, 也就是人名、道具名等需要統一代碼的地方不適合使用。 但如果只是指令名或是人名、道具名不多的情況就還蠻好用的。 <ii>簡單說.....就是想辦法硬幹成(1): 例如皇騎1的狀態畫面有可能顯示上千種不同人名、道具名、職業名.... 字庫最少要700個中文字,而且也沒額外條件好拆字庫時.....就只好吐血吧。 先在Ram裡找一塊沒被使用的空間,寫個函式把當下用到的中文字圖 依序寫到前述的空間,另外要同時計算目前寫了幾個字圖到Ram裡。 寫完後再把所有的字圖依序複製成為VRam裡特定位置的圖塊, 並將原本顯示日文的位置所對應的編號,固定成特定位置開始的連續編號。 這樣不管字庫多大,理論上都能"動態地"從Rom裡挑字圖到Ram, 再從Ram複製字串到"固定的"VRam位置。 光是要讓皇騎1狀態畫面不受限地顯示中文,我就多加了10幾個函式。 因為這邊其實還同時併發8x8顯示與代碼數量不足這兩個問題。 不然我通常將一個地方改中文化,只需要加1~3個函式,這裡真的改太大.... ~Part II. 反組譯log檔~ 所謂的反組譯,簡單來說就是把0與1機械碼實際運作的過程(機器看得懂,人看不懂), 改用較高階、精簡的指令集與16進位數字表現出來(也就是組合語言)。 相對於程式碼是給人看、機械碼是給電腦看,組合語言就介於中間。 因為每行都相當於是機器硬體上的一個簡單動作,所以會比高階程式語言瑣碎得多。 我們先來看看怎麼用snes9x v1.43.ep9r8將遊戲中的運作過程dump出來。 載入Rom檔後,按下右邊debuger介面中的Run鍵,開始遊戲。 根據需求上的不同,有兩種方式決定何時開始dump: (1)不知道資料在Rom裡位址的情況 例如,當我想把ロシュフォル教会改成中文,但卻找不到這些字在Rom的哪裡時, 可以在文字出現前的畫面,先去右邊debuger介面勾選Loggin下的cpu欄, 再把滑鼠移到遊戲視窗,立即按鍵顯示出目標"ロシュフォル教会"。 再馬上取消掉debuger介面中cpu欄的勾選,就會dump出勾選其間的組語代碼, (上述動作越快越好,因為一秒就會dump出幾十MB的組語代碼.....) 並存在與Rom同一資料夾下的"Rom檔名0000.log"。 http://0rz.tw/hgpPL 在這種情況下要知道確切的相關程式開始位置比較麻煩, 如果知道現在顯示文字的代碼,可以用這代碼在log檔中搜尋。 如果不知道可以搜尋何時有DMA的動作,或是觀察VRam中的變化, 再反推相關程式的開始位置(以後會給實例說明)。 (2)知道資料在Rom裡位址的情況: 按下介面中的Breakpoints鍵,貼上位址並勾選3個中斷時機, 當遊戲對該位址資料進行讀寫時就會自動中斷停止, 這時按下Step Over鍵就會開始一行一行執行指令。 http://0rz.tw/yLJAr 配合(1)用UltraEdit搜尋這幾行指令在log檔的位置, 就知道相關程式從何處開始,不用在幾十MB的組語代碼中大海撈針。 這時我們終於得到log檔,甚至知道程式大概從哪裡開始。 把(1)中例子的log檔打開來看,即使在我緩慢的準系統上只跑了一秒, 就產生了20MB的龐大log資料,每個欄位代表的意義如下: (省略後面不重要的部分,"無視"那欄的暫存器我沒遇過要改的情況,故...無視) SFC位址 機器碼 組語代碼 A暫存 X暫存 Y暫存 無視 bank ======== ======== ===================== ====== ====== ====== ====== ===== $00/BC7F E8 INX A:FF01 X:001C Y:5555 D:0000 DB:00 $00/BC80 E8 INX A:FF01 X:001D Y:5555 D:0000 DB:00 $00/BC81 E0 28 00 CPX #$0028 A:FF01 X:001E Y:5555 D:0000 DB:00 $00/BC84 90 E2 BCC $E2 [$BC68] A:FF01 X:001E Y:5555 D:0000 DB:00 $00/BC68 DA PHX A:FF01 X:001E Y:5555 D:0000 DB:00 $00/BC69 20 8C BC JSR $BC8C [$00:BC8C] A:FF01 X:001E Y:5555 D:0000 DB:00 $00/BC8C 86 1C STX $1C [$00:001C] A:FF01 X:001E Y:5555 D:0000 DB:00 $00/BC8E A9 01 LDA #$01 A:FF01 X:001E Y:5555 D:0000 DB:00 $00/BC90 8D 5E 16 STA $165E [$00:165E] A:FF01 X:001E Y:5555 D:0000 DB:00 .....(略) SFC位址: 目前的指令位址$oo/xxxx,oo是第oo個bank,xxxx是該bank裡的位址 機器碼: 實際執行的二進位指令碼,只是在此以16進位表示比較容易看。 一個基本的SFC指令會包含1~4個bytes,第1個byte一定是指令本身的代號。 第2~4個byte(如果有的話)都是資料內容。 組語代碼:即使機器碼用16進位表示也只是"變短",很難記住這機器碼對應什麼指令 所以需要用組合語言的方式來簡單表示指令與資料內容。 我隨便拿上面例子來解釋,例如.... ‧INX 表示X暫存器的值加1。 ‧CPX #$0028 表示把X暫存器的值跟數字0x0028做比較 (#$表示資料就是"數字",只有$的話表示資料是"位址內的值")。 ‧BCC $E2 表示前面比較結果是"小於"的話,移動0xe2的距離。 因為讀了2個bytes,所以現在程式指標位址是0x00bc84 + 2 = 0x00bc86。 移動距離是0xe2-0x100(0xe2是負數)=向前移0x1e, 也就是移到0x00bc86-0x1e=0x00bc68的位址。 如果前面比較結果是"大於等於"的話,就略過繼續處理0x00bc86的指令。 [$BC68]是反組譯器提供的額外資訊,表示這邊程式指標跳到0x00bc68。 ‧PHX 表示把X值放到堆疊(stack)中。 ‧JSR $BC8C 表示跳到同一個bank中,位址0xbc8c的位址。 後面[ ]的內容一樣是反組譯器提供的額外資訊.... ‧STX $1C 表示把X暫存器的內容存到同bank中,位址0x001c的位址。 ‧LDA #$01 表示把數值0x01載入A暫存器中。 ‧STA $165E 表示把A暫存器中的內容存到同bank中,位址0x165e的位址。 熟悉的話,只要看到組語代碼就能知道每行在幹什麼。 不熟悉的話可以參考我第一篇提到的網頁,裡面有所有指令代碼的解釋。 A暫存: SFC主要有3個2 bytes暫存器:A、X跟Y。 A暫存器是功能較強的,可以做加減、AND、OR、shift、比大小等工作。 大部分數學運算會在A暫存器裡完成X、Y暫存:這兩個暫存器比較算輔助性質,只能做"加1"等少數極基本功能。 經常用來輔助A暫存器,完成一些較複雜的計算或資料搬運功能bank值: 之前有說過程式執行時不太會切換bank,如此可以提升程式執行效率。 從 JSR $BC8C 這例子可以看出來,即使實際要跳過去的位址是0x00bc8c, 最前面00這個bank值卻被省略了,連帶也有效節省了指令長度 。 =========================================================================== ....先寫到這邊吧,下週再給實際的中文化例子。 看看如何解讀log檔,並且加上新的函式來突破原本程式的限制。 中間有些概念如果有錯的話,也隨時歡迎指正。 -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 118.161.167.231
tonybin:先謝再推 11/24 17:44
tonybin:後面關於反組譯的說明 真是受教了 11/24 17:52
ADHI:喔喔,越來越艱深專業了,感謝分享!! 11/24 18:27
qazxswptt:太神了 感謝留下記錄給後人參考 11/24 23:46
EDGE: 太神了,看不懂還是要推…身為OB的愛好者非常感謝您 11/25 01:09
busin:專業文推~ 11/25 03:04
※ 編輯: lulaptt 來自: 118.161.167.231 (11/25 09:08)
dansha:推專業 11/25 10:27
jachen168:推~ 11/25 21:32
willkill:不推不行 11/25 22:11
djboy:神文推! 11/26 09:42
oginome:大推~ 11/26 12:17