[心得] SFC中文化經驗談(五)—反組譯/實作篇—
【雜言】
上一篇看似無關的兩個主題:"Rom、Ram、VRam"與"反組譯log檔"會在這篇做結合。
(我上一篇可不是想到什麼講什麼=__=|||)
在這篇文章我會用一個我覺得很好的簡單例子來解釋...
如何找到文字在Rom\Ram\VRam的位置、兩種DMA搬運方式、如何插入新的程式碼等主題。
另外,之前一直忘了提一點,有時SFC Rom裡會有0x200 bytes的空白數據在檔案開頭。
強烈建議去掉這0x200 bytes以免影響實際位址與SFC位址間的換算。
【正文】
我們來看一個實例吧。假設我們希望把下圖上面那排日文改成中文。
http://0rz.tw/w1vZO
我們不知道這些字的代碼,在Rom裡找不到這些字圖(這些字圖是被壓縮的),
甚至這些字都不是8x16,而是以8x8為一個單位.....。
首先,就是用snes9x1.43.ep9r8打開Rom,先執行到上圖的前一個畫面。
就跟上一篇說的一樣,勾選cpu選項後繼續執行遊戲到出現要改的日文出現,
並產生反組譯的log檔。
我在前一篇有提到4種文字顯示方法,多數情況下我們能在Ram裡找到被挑出來的字圖。
這時可以用debuger介面中的Dump RAM按鍵,把Ram裡的資料存檔下來,
然後用YY-CHR察看是否有與顯示的日文一致的字圖。
http://0rz.tw/bn9zs
從上圖我們發現第一個日文字圖"ユ"在Ram中的位置是從0x7f0d50開始,
用UltraEdit看時可以發現0x7f0d50這個byte的資料是0xff。
這表示程式執行時可能從Rom的某處把0xff(經過解壓)存到Ram裡7f:0d50的位置。
因此,用debuger介面中的Breakpoints鍵,來找出何時會存取此位址。
這邊要注意一點的是...我們不止是要找到何時"存取7f:0d50這位址",
而是要找何時"把0xff存放到7f:0d50這位址",這樣才能確認Rom裡的字已經挑到Ram裡。
因為放到Ram的資料最終要放到VRam才能顯示,所以在確認Rom裡的字挑到Ram裡後,
繼續看何時有DMA的動作,把7f:0d50資料搬到VRam,如下。
http://0rz.tw/Rnv90
找到DMA動作後,在反組譯log檔中搜尋這個DMA的動作資料,也就是....
"$00/9DBF 8D 0B 42 STA $420B [$00:420B] A:0080 X:1801 Y:0000 D:0F99 DB:00"
(DB暫存器以後的資料不要拿來搜尋,因為後面有非固定的資料)
在log檔中找到上述程式碼後,往前推就會看到完整的DMA動作。
因為這邊是Ram->VRam,所以會指定幾個特殊位址用來保存DMA傳輸時需要的資訊:
(1)$43X0是指定DMA參數,一般存入0x01這個值。(X是傳輸時的channel)
(2)$43X1是指定DMA到VRam,一定要存入0x18。(X是傳輸時的channel)
(3)Ram的完整起始位址(3 bytes),會存放在$43X2~$43X4。
X是DMA傳輸時使用的channel,有0~7共8個channel可選用,
一次DMA只能固定使用一個channel。
(4)複製過去資料的總byte數,會存放在$43X5~$43X6。(X是傳輸時的channel)
(5)目的地的VRam起始位址(2 bytes),會存放在$2116~$2117。
(6)當$420B存入0x80這個數值後,DMA就自動進行了。
我們回頭來看前面在log檔中實際找到的DMA動作,並試著翻譯:
(前面省略,目前A暫存器存取是以1 byte為單位,X、Y暫存器存取是以2 bytes為單位)
==============================================================================
$00/9D97 A6 01 LDX $01 [$00:0F9A] A:0001 X:4300 Y:0220 D:0F99 DB:00
將位址$01的數值(從下行X:0000得知是0x0000)載入X暫存器
$00/9D99 8E 72 43 STX $4372 [$00:4372] A:0001 X:0000 Y:0220 D:0F99 DB:00
將X暫存器值(0x0000)存入$4372 (第7個channel)
$4372 -> 00
$4373 -> 00 (X暫存器存取是以2byte為單位,所以會存到$4373)
$00/9D9C A5 03 LDA $03 [$00:0F9C] A:0001 X:0000 Y:0220 D:0F99 DB:00
將位址$03的數值(從下行A:007f得知是0x007f)載入A暫存器
$00/9D9E 8D 74 43 STA $4374 [$00:4374] A:007F X:0000 Y:0220 D:0F99 DB:00
將A暫存器值(0x007f)存入$4374 (第7個channel)
$4374 -> 7f
因為$4372~$4374是"00 00 7f" 所以知道來源Ram的起始位址是7F:0000
$00/9DA1 A6 06 LDX $06 [$00:0F9F] A:007F X:0000 Y:0220 D:0F99 DB:00
將位址$06的數值(從下行X:1000得知是0x1000)載入X暫存器
$00/9DA3 8E 75 43 STX $4375 [$00:4375] A:007F X:1000 Y:0220 D:0F99 DB:00
將X暫存器值(0x1000)存入$4375 (第7個channel)
$4375 -> 00
$4376 -> 10 (X暫存器存取是以2byte為單位,所以會存到$4376)
因為$4375~$4376是"00 10" 所以知道一共會複製0x1000個bytes
$00/9DA6 C2 20 REP #$20 A:007F X:1000 Y:0220 D:0F99 DB:00
A暫存器存取都改以2 bytes為單位
$00/9DA8 A5 00 LDA $00 [$00:0F99] A:007F X:1000 Y:0220 D:0F99 DB:00
$00/9DAA 29 F0 00 AND #$00F0 A:0000 X:1000 Y:0220 D:0F99 DB:00
$00/9DAD 4A LSR A A:0000 X:1000 Y:0220 D:0F99 DB:00
$00/9DAE 4A LSR A A:0000 X:1000 Y:0220 D:0F99 DB:00
$00/9DAF 4A LSR A A:0000 X:1000 Y:0220 D:0F99 DB:00
$00/9DB0 AA TAX A:0000 X:1000 Y:0220 D:0F99 DB:00
上面不是重點,有興趣可以自行查指令意義,跳過。
$00/9DB1 E2 20 SEP #$20 A:0000 X:0000 Y:0220 D:0F99 DB:00
A暫存器存取都改以1 byte為單位
$00/9DB3 A5 00 LDA $00 [$00:0F99] A:0000 X:0000 Y:0220 D:0F99 DB:00
$00/9DB5 A4 04 LDY $04 [$00:0F9D] A:0000 X:0000 Y:0220 D:0F99 DB:00
$00/9DB7 FC 3D 9E JSR ($9E3D,x)[$00:9E5D] A:0000 X:0000 Y:0000 D:0F99 DB:00
$00/9E5D 09 80 ORA #$80 A:0000 X:0000 Y:0000 D:0F99 DB:00
上面也不是重點,有興趣可以自行查指令意義,跳過。
$00/9E5F 8D 15 21 STA $2115 [$00:2115] A:0080 X:0000 Y:0000 D:0F99 DB:00
將A暫存器值(0x80)存入$2115
$00/9E62 8C 16 21 STY $2116 [$00:2116] A:0080 X:0000 Y:0000 D:0F99 DB:00
將Y暫存器值(0x0000)存入$2116
$2116 -> 00
$2117 -> 00
因為$2116~$2117是"00 00" 所以知道目的地VRam的起始位址是0x0000
$00/9E65 A2 01 18 LDX #$1801 A:0080 X:0000 Y:0000 D:0F99 DB:00
將數值0x1801存入X暫存器
$00/9E68 60 RTS A:0080 X:1801 Y:0000 D:0F99 DB:00
函式返回
$00/9DBA 8E 70 43 STX $4370 [$00:4370] A:0080 X:1801 Y:0000 D:0F99 DB:00
將X暫存器值存入$4370 (第7個channel)
$4370 -> 01
$4371 -> 18
符合DMA到VRam的一般設定
$00/9DBD A9 80 LDA #$80 A:0080 X:1801 Y:0000 D:0F99 DB:00
將數值0x80存入A暫存器
$00/9DBF 8D 0B 42 STA $420B [$00:420B] A:0080 X:1801 Y:0000 D:0F99 DB:00
將A暫存器值存入$420B,開始啟動DMA。
==============================================================================
總結前面這一大段,就是告訴我們...
從Ram位址7f:0000開始的1000個bytes,會搬到VRam裡0x0000~0x1000的位址。
(事實上有些SFC反組譯模擬器可以把VRam也dump出來,可以直接用YY-CHR去看搬到哪裡)
在上一篇有說過,VRam包含(1)8x8圖塊素材、(2)每個圖層使用的圖塊編號。
如果VRam裡的圖塊是8x8為單位的話,我們只要看螢幕上出現的文字
是DMA複製過去的第幾個圖塊,大概就知道這些文字的編號了。
用YY-CHR來看Ram的資料,我們會發現第一個日文字"ユ"是第0xd5個8x8圖塊。
("ユ"在Ram中的位置是從7f:0d50開始,每個8x8圖塊佔0x10 byte)。
第2到第4個字分別是"ニ"(圖塊c6)、"ッ"(圖塊af)、"ト"(圖塊c4)。
我們可以用debuger中的Show Hex查看Rom、Ram、VRam的數值(但不能存檔這點很糟糕)。
把VRam的所有數值複製到文件檔並搜尋D5這個數值,會搜尋到下面一串數據:
(略)... D5 30 C6 30 AF 30 C4 30 ...(略)
很眼熟吧,正好是ユニット的圖塊用0x30隔開。
事實上,我們也能在Ram裡0x7f0108的位址發現同樣的數據,但可惜Rom裡沒發現。
沒辦法,再用UltraEdit在log檔裡搜尋"7F:0108"吧,
找看看是否有從Rom將數值0xd5存入Ram位址0x7f0108的動作。
==============================================================================
$09/8C55 BF 49 AF 09 LDA $09AF49,x[$09:B051] A:347C X:0108 Y:0000 D:0000 DB:09
以位址0x09af49(此位址在Rom裡)為基礎,X暫存器內的值(0x0108)為位移量,
將 0x09af49+0x0108 = 0x09b051 位址內的值(從下行A:20D5得知是0x20d5)存入A暫存器。
$09/8C59 18 CLC A:20D5 X:0108 Y:0000 D:0000 DB:09
清除進位器
$09/8C5A 69 00 10 ADC #$1000 A:20D5 X:0108 Y:0000 D:0000 DB:09
將A暫存器的值(0x20d5)加上數值0x1000後,A存回暫存器。
$09/8C5D 9F 00 00 7F STA $7F0000,x[$7F:0108] A:30D5 X:0108 Y:0000 D:0000 DB:09
將A暫存器的值存入"以0x7f0000(此位址在Ram裡)為基礎,
X暫存器內的值(0x0108)為位移"的位址,
也就是 0x7f000+0x0108 = 0x7f0108 (就是我們所搜尋的存入"7F:0108"動作)
==============================================================================
總結前面這一段,就是告訴我們...
Ram裡(也是VRam裡)的D5 30,是從Rom裡0x09b051~0x09b052位址複製過來的。
用Lunar Address將SFC位址0x09b051換算成實際位址0x04b051,
用UltraEdit開啟Rom檔可以看到位址0x04b051有D5 20 C6 20 AF 20 C4 20這串數據。
我們試著把他們改成4C 20 55 20 4C 20 41 20,畫面上原本顯示ユニット的地方
就會從ユニット(圖塊編號D5 C6 AF C4)變為LULA(圖塊編號4C 55 4C 41)。
http://0rz.tw/XxlEA
事實上,實際Rom內位址0x04b051附近都是顯示畫面時用的圖塊編號,
隨手更改附近的數值就會發現位址0x04b011開始是ユニット文字上面一排位置的圖塊。
我們把實際位址0x04b011開始的數據改成49 20 20 20 41 20 4D 20,
就會把"I□AM"(圖塊編號49 20 41 4D)顯示出來:
http://0rz.tw/eI2Dq
雖然"I□AM"的上半部分被螢幕邊緣切掉了,
不過上下文字加起來就有8x12(2個圖塊)的空間塞一個中文字。
事實上要用16x12(4個圖塊)來顯示較大的中文字也可以,
只是我希望顯示出的中文字越接近原日文版本越好,
所以我還是愛用接近原日文大小的8x12範圍來顯示一個中文字。
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
到這邊為止,我們總算完成一半了。可以透過更改Rom裡的特定數據,
進而更改VRam裡的圖塊編號並決定要顯示第幾個圖塊。
但問題是我們可以用的圖塊素材,還是原本的日文字。
因此我們必須自己插入一段程式,把自製的中文字圖搬入Ram裡,取代原本日文字。
最重要的就是,何時才是好的搬運時機?我認為好的搬運時機要符合下面條件:
(1)原日文字圖必須已經搬入Ram裡,這樣搬入的自製中文字圖才能覆蓋原日文字圖。
(2)最好在Ram裡資料DMA到VRam之前搬入,這樣原程式Ram->VRam的程式就不用改寫。
(3)被插入的程式位置越少被使用越好,最好是整個遊戲只有在此被呼叫一次,
否則可能有其他不相關的地方誤用了我們新加的函式,造成錯誤。
因為事實上前面這件事很難確認,所以至少也要確認在log檔裡只出現一次。
如果被插入的程式位置被重複使用,就要靠辯認A、X、Y暫存器內是否有特定值,
來確認是否為我們要處裡的case,事情會變得更加麻煩。
(4)目前的A、X、Y等暫存器內容不會再被使用,這樣我們不用把這些內容
放到堆疊(stack)裡做備份,直接更改暫存器內容也沒關係。
以目前這case來說,我最後選了要在紅色的指令位置插入新指令(4個條件都符合):
==============================================================================
$09/90D1 A0 00 10 LDY #$1000 A:0F18 X:1000 Y:8A1E D:0000 DB:09
$09/90D4 84 00 STY $00 [$00:0000] A:0F18 X:1000 Y:1000 D:0000 DB:09
$09/90D6 A9 7F LDA #$7F A:0F18 X:1000 Y:1000 D:0000 DB:09
$09/90D8 85 49 STA $49 [$00:0049] A:0F7F X:1000 Y:1000 D:0000 DB:09
$09/90DA A2 00 00 LDX #$0000 A:0F7F X:1000 Y:1000 D:0000 DB:09
$09/90DD A0 00 00 LDY #$0000 A:0F7F X:0000 Y:1000 D:0000 DB:09
$09/90E0 7B TDC A:0F7F X:0000 Y:0000 D:0000 DB:09
$09/90E1 20 F2 A8 JSR $A8F2 [$09:A8F2] A:0000 X:0000 Y:0000 D:0000 DB:09
==============================================================================
我們把SFC位址0x0990da~0x0990df....也就是實際位址0x0490da~0x0490df的數據,
從 "A2 00 00 A0 00 00" 改成 "EA EA 5C 00 E0 2F",
再把下面數據添加到實際位址0x17e000~0x17e01b (SFC位址0x2fe000~0x2fe01b):
8B C2 20 A2 00 F0 A0 00 08 A9 DF 07 54 7F 2F AB
E2 20 A2 00 00 A0 00 00 5C E0 90 09
接著我們在實際位址0x17f000 (SFC位址0x2ff000)添加要使用的中文字圖:
http://0rz.tw/sMqGn
就可以把Rom裡新添加的字圖用DMA複製到Ram裡。
如果執行修改後的Rom檔,並將反組譯log檔dump下來(青色為上面追加的指令部分):
==============================================================================
$09/90D1 A0 00 10 LDY #$1000 A:0F18 X:1000 Y:8A1E D:0000 DB:09
$09/90D4 84 00 STY $00 [$00:0000] A:0F18 X:1000 Y:1000 D:0000 DB:09
$09/90D6 A9 7F LDA #$7F A:0F18 X:1000 Y:1000 D:0000 DB:09
$09/90D8 85 49 STA $49 [$00:0049] A:0F7F X:1000 Y:1000 D:0000 DB:09
以上為原程式無關部分,不提。
$09/90DA EA NOP A:0F7F X:1000 Y:1000 D:0000 DB:09
$09/90DB EA NOP A:0F7F X:1000 Y:1000 D:0000 DB:09
空指令,什麼事也不做
$09/90DC 5C 00 E0 2F JMP $2FE000[$2F:E000] A:0F7F X:1000 Y:1000 D:0000 DB:09
程式跳到SFC位址0x2fe000
$2F/E000 8B PHB A:0F7F X:1000 Y:1000 D:0000 DB:09
將bank值放進堆疊備份,因為之後Rom->Ram的DMA過程會更改bank值
$2F/E001 C2 20 REP #$20 A:0F7F X:1000 Y:1000 D:0000 DB:09
將A暫存器存取改以2 bytes為單位
$2F/E003 A2 00 F0 LDX #$F000 A:0F7F X:1000 Y:1000 D:0000 DB:09
將數值0xf000存入X暫存器
$2F/E006 A0 00 08 LDY #$0800 A:0F7F X:F000 Y:1000 D:0000 DB:09
將數值0x0800存入Y暫存器
$2F/E009 A9 DF 07 LDA #$07DF A:0F7F X:F000 Y:0800 D:0000 DB:09
將數值0x07df存入A暫存器
$2F/E00C 54 7F 2F MVN 7F 2F A:07DF X:F000 Y:0800 D:0000 DB:09
(中間省略一堆自動產生的搬運動作)
$2F/E00C 54 7F 2F MVN 7F 2F A:0000 X:F7DF Y:0FDF D:0000 DB:7F
利用A、X、Y暫存器的值,將資料從0x2ff000搬運到0x7f0800,
一共搬運0x07DF+1=0x07e0個bytes。之前講的DMA適用於(Rom, Ram) -> VRam,
此處的DMA(也就是MVN指令)適用在(Rom, Ram) -> (Ram)
$2F/E00F AB PLB A:FFFF X:F7E0 Y:0FE0 D:0000 DB:7F
取回堆疊中備份的bank值
$2F/E010 E2 20 SEP #$20 A:FFFF X:F7E0 Y:0FE0 D:0000 DB:09
將A暫存器存取改以1 byte為單位
$2F/E012 A2 00 00 LDX #$0000 A:FFFF X:F7E0 Y:0FE0 D:0000 DB:09
$2F/E015 A0 00 00 LDY #$0000 A:FFFF X:0000 Y:0FE0 D:0000 DB:09
原程式中被取代的兩行指令在此補回
$2F/E018 5C E0 90 09 JMP $0990E0[$09:90E0] A:FFFF X:0000 Y:0000 D:0000 DB:09
程式跳到SFC位址0x0990e0,接回原程式
$09/90E0 7B TDC A:FFFF X:0000 Y:0000 D:0000 DB:09
$09/90E1 20 F2 A8 JSR $A8F2 [$09:A8F2] A:0000 X:0000 Y:0000 D:0000 DB:09
以上為原程式無關部分,不提。
========================================================================
這時我們再把Ram內資料dump出來,發現我們成功把要用的中文字圖放入Ram裡了:
http://0rz.tw/kqdqa
其實我們完全不知道原日文字圖從哪裡來、如何壓縮的...
因為那根本不重要,反正我們直接用自己的中文字圖覆蓋上去了。
我們知道這些自製字圖編號將會是0x80~0x8f,加上前面修改圖塊編號的經驗,
可以輕易地把原本8x8日文字替換顯示成8x12中文字了。
http://0rz.tw/SsE0n
看! 很簡單吧!
--
※ 發信站: 批踢踢實業坊(ptt.cc)
留言