作者lulaptt (Lula)
看板Emulator
標題[心得] SFC中文化經驗談(五)—反組譯/實作篇—
時間Sat Dec 1 14:41:42 2012
【雜言】
上一篇看似無關的兩個主題:"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)
◆ From: 111.249.67.122
推 oginome:淚推lulaptt大辛苦勞動的成果~ 12/01 14:44
推 hirokofan:這篇....我真的是在看中文嗎XD 12/01 15:26
推 qazxswptt:感謝l大 12/01 15:41
推 owenkuo:好專業...我只會用pctools改數據而已...(DOS時代)=w= 12/01 16:01
推 Jay915:太強了... 12/01 17:22
推 tonybin:淚推 這次的作業難做了 12/01 17:35
※ 編輯: lulaptt 來自: 111.249.67.122 (12/01 18:17)
推 WeasoN:神推 12/01 20:07