作者lulaptt (Lula)
看板Emulator
标题[心得] SFC中文化经验谈(四)—反组译/概念篇—
时间Sat Nov 24 16:34:46 2012
【杂言】
我有点後悔写这系列文章了....比我预想的还难写(抱头
先说明一下,因为我不可能从计算机概论或写编译器谈起,
所以这系列的经验谈是假定读者有资工背景或具基本程式能力。
反组译这边则最好有修过组合语言或系统程式这类课程,具备基本概念即可。
一般来说,资工大三~大四学生应该就具备充足知识了.....。
之前三篇中提到的工具我并没有解释如何使用,因为那是自己花时间摸一下就会用的。
但这篇的反组译模拟器我会稍微提一下用法,因为这边有些"经验上"的用法。
【正文】
~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
1F:推 tonybin:先谢再推 11/24 17:44
2F:推 tonybin:後面关於反组译的说明 真是受教了 11/24 17:52
3F:推 ADHI:喔喔,越来越艰深专业了,感谢分享!! 11/24 18:27
4F:推 qazxswptt:太神了 感谢留下记录给後人参考 11/24 23:46
5F:推 EDGE: 太神了,看不懂还是要推…身为OB的爱好者非常感谢您 11/25 01:09
6F:推 busin:专业文推~ 11/25 03:04
※ 编辑: lulaptt 来自: 118.161.167.231 (11/25 09:08)
7F:推 dansha:推专业 11/25 10:27
8F:推 jachen168:推~ 11/25 21:32
9F:推 willkill:不推不行 11/25 22:11
10F:推 djboy:神文推! 11/26 09:42
11F:推 oginome:大推~ 11/26 12:17
12F:推 conpo:专业好文推,长知识! 11/26 13:03