精華區beta Emulator 關於我們 聯絡資訊
【雜言】 上一篇看似無關的兩個主題:"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檔可以看到位址0x04b051D5 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