超级任天堂编程/SNES 内存映射
SNES 卡带主要有两种类型,SNES 社区将它们称为 LoROM 和 HiROM 卡带。两者都有不同的内存映射。在解释 LoROM 和 HiROM 之前,我们应该在这里定义一些关键词。
$ 或 0x 前缀: 以下数字为十六进制。地址通常显示为十六进制值。
Bank: 64 千字节 (65536 或 $10000 字节),基本上是 CPU 理解的 3 字节地址的最高有效字节。地址 $AABBCC 的 bank 为 $AA (170)。使用三个字节的地址空间,SNES 可以寻址高达 16 兆字节 (2^24 或 1<<24 或 16777216 字节 == 16384 千字节 == 16 兆字节)。请注意,仅仅因为 SNES 可以寻址 16 兆字节并不意味着它也有 16 MB 的 RAM(这里称为 WRAM) - 我们将在后面详细解释。记住,因此 SNES 有 $100 或 256 个 bank(从 $00 开始,到 $FF 结束)。
Page: 256 字节 ($0100 字节)。每当机器需要执行映射任务时,都会使用页面(例如,确保地址 $AABBCC 和地址 $DDBBCC 指向完全相同的数据,如果机器应该这样工作的话)。页面也用于普通的 PC(x86/x86-64 架构),因此并非 SNES 独有。页面是机器的最小可映射单元。你会注意到下面的表格中没有条目,其中某个内存范围没有以 $FF 结尾 - 这是因为 $FF 是一个页面的最后一个地址,在此字节之后一个新页面开始。SNES 严重依赖于映射 - 例如,如果没有映射,它将无法将 $00 – $3F 的所有 bank 的两个 WRAM 页面直接放在 bank 的开头。一个 bank 包含 256 ($100) 个页面,因此 SNES 有 $100 (bank) * $100 (page) == $10000 (65536) 个页面可用。
LoROM 和 HiROM 都可以存储高达 4 兆字节 (32 兆位) 的 ROM 数据。ExLoROM、ExHiROM 和一些扩展芯片(如 SA-1 和 SDD-1)据说可以存储高达 8 兆字节 (64 兆位)。
在 SNES 标头中的字节 $15(有关更多详细信息,请参见下文),存储了 ROM 组成字节。
值 | 位掩码 | 定义 | 示例 ROM | 示例 ROM 大小 |
---|---|---|---|---|
$20 | %0010 0000 | LoROM | 最终幻想 4 | 1048576 字节 / 1 MB |
$21 | %0010 0001 | HiROM | 最终幻想 5 | 2097152 字节 / 2 MB |
$23 | %0010 0011 | SA-1 ROM | 超级马里奥 RPG | 4194304 字节 / 4 MB |
$30 | %0011 0000 | LoROM + FastROM | 终极 VII | 1572864 字节 / 1.5 MB |
$31 | %0011 0001 | HiROM + FastROM | 最终幻想 6 | 3145728 字节 / 3 MB |
$32 | %0011 0010 | SDD-1 ROM | 星之海洋 | 6291456 字节 / 6 MB |
$35 | %0011 0101 | ExHiROM | 幻想传说 | 6291456 字节 / 6 MB |
要使用的位掩码是 001A0BCD,基本值为 $20
- A == 0 表示 SlowROM (+ $0),A == 1 表示 FastROM (+ $10)。
- B == 1 表示 ExHiROM (+ $4)
- D == 0 表示 LoROM (+ $0),D == 1 表示 HiROM (+ $1),在扩展 ROM 的情况下与 B 和 C 一起使用。
请记住,有些人有时使用“模式 20”来指代 LoROM 映射模型,使用“模式 21”来指代 HiROM,尽管这在技术上是错误的。如表所示,有两个 LoROM 和两个 HiROM 映射,它们的标记字节与名称暗示的不同。
ExLoROM 是一个非官方的映射,因此它没有自己的类型值。要检测它,通常会检查游戏是否为普通的 LoROM,然后检查 ROM 文件大小。
请注意,像 SuperFX 这样的扩展芯片有自己的内存映射,这里没有涵盖。特别是 SA-1 和 SDD-1 是 MMC 芯片,这意味着它们可以进行 bank 切换,以更改为给定 bank 访问的 ROM 部分。
这是 LoROM 内存映射
Bank | 偏移量 | 定义 | ROM 地址 | 镜像 |
---|---|---|---|---|
$00–$3F | $0000-$1FFF | LowRAM,从 bank $7E 镜像 | (没有 ROM 映射) | $7E (WRAM 的前两个页面) |
$2000–$20FF | 未使用 | (没有 ROM 映射) | $80-$BF | |
$2100–$21FF | PPU1、APU、硬件寄存器 | (没有 ROM 映射) | $80-$BF | |
$2200–$2FFF | 未使用 | (没有 ROM 映射) | $80-$BF | |
$3000–$3FFF | DSP、SuperFX、硬件寄存器(有关于此页面的文档吗?我找不到任何来源) | (没有 ROM 映射) | $80-$BF | |
$4000–$40FF | 旧式手柄寄存器 | (没有 ROM 映射) | $80-$BF | |
$4100–$41FF | 未使用 | (没有 ROM 映射) | $80-$BF | |
$4200–$44FF | DMA、PPU2、硬件寄存器 | (没有 ROM 映射) | $80-$BF | |
$4500–$5FFF | 未使用 | (没有 ROM 映射) | $80-$BF | |
$6000–$7FFF | 保留(增强芯片内存) | (没有 ROM 映射) | $80-$BF | |
$8000-$FFFF | LoROM 部分(程序内存) | $00: $000000 - $007FFF
$01: $008000 - $00FFFF
$02: $010000 - $017FFF
...
$3D: $1E8000 - $1EFFFF
$3E: $1F0000 - $1F7FFF
$3F: $1F8000 - $1FFFFF
|
$80-$BF | |
$40–$6F | $0000-$7FFF | 如果芯片不是 MAD-1,可以映射为更高 bank ($8000 - $FFFF)。否则该区域未使用。 | 未映射或$40: $200000 - $207FFF
$41: $208000 - $20FFFF
$42: $210000 - $217FFF
...
$6D: $368000 - $36FFFF
$6E: $370000 - $377FFF
$6F: $378000 - $37FFFF
|
$C0-$EF |
$8000-$FFFF | LoROM 部分(程序内存) | $40: $200000 - $207FFF
$41: $208000 - $20FFFF
$42: $210000 - $217FFF
...
$6D: $368000 - $36FFFF
$6E: $370000 - $377FFF
$6F: $378000 - $37FFFF
|
$C0-$EF | |
$70–$7D | $0000-$7FFF | 卡带 SRAM - 最大 448 千字节 | (没有 ROM 映射) | $F0-$FD |
$8000-$FFFF | LoROM 部分(程序内存) | $70: $380000 - $387FFF
$71: $388000 - $38FFFF
$72: $390000 - $397FFF
...
$7B: $3D8000 - $3DFFFF
$7C: $3E0000 - $3E7FFF
$7D: $3E8000 - $3EFFFF
|
$F0-$FD | |
$7E | $0000-$1FFF | LowRAM (WRAM) | (没有 ROM 映射) | $00–$3F (WRAM 的前两个页面) |
$2000–$7FFF | HighRAM (WRAM) | (没有 ROM 映射) | (没有映射) | |
$8000-$FFFF | 扩展 RAM (WRAM) | (没有 ROM 映射) | (没有映射) | |
$7F | $0000-$FFFF | 扩展 RAM (WRAM) | (没有 ROM 映射) | (没有映射) |
$80-$BF | $0000-$FFFF | 镜像 $00–$3F | (参见 bank $00–$3F) | $00–$3F |
$C0-$EF | $0000-$FFFF | 镜像 $40–$6F | (参见 bank $40–$6F) | $40–$6F |
$F0-$FD | $0000-$FFFF | 镜像 $70–$7D | (参见 bank $70–$7D) | $70–$7D |
$FE-$FF | $0000-$7FFF | 卡带 SRAM - 64 千字节(总计 512 KB) | (没有 ROM 映射) | (没有映射) |
$8000-$FFFF | LoROM 部分(程序内存) | $7E: $3F0000 - $3F7FFF
$7F: $3F8000 - $3FFFFF
|
(没有映射) (请记住这些 bank) (在 <$7F 范围内被 WRAM 覆盖了) |
在 LoROM 模式下,ROM 始终映射在每个 bank 的上半部分,因此每个块 32 千字节。bank $00 – $7D(地址:$8000 - $FFFF)保存连续数据,以及 bank $80 - $FF。卡带上的 SRAM 连续且重复映射 - 8 千字节的 SRAM 映射在 $0000 - $1FFF、$2000 – $3FFF、$4000 – $5FFF 等处。由于 SNES 的 WRAM 映射在 bank $7E - $7F,因此这些 bank 不会映射到最后一个 SRAM/ROM 块。必须通过 bank $80 - $FF 访问此内存。在 LoROM 和 HiROM 模式下,都没有其他方法可以访问此内存。
LoROM 的建立是为了确保系统 bank ($00 – $3F) 的更高页面 (>7) 实际上被使用。这是通过仅将整个 ROM 加载到更高页面以及 32 千字节块中来实现的。32 KB * $80 bank == 4 兆字节。
这是 HiROM 内存映射
Bank | 偏移量 | 定义 | ROM 地址 | 镜像 |
---|---|---|---|---|
$00–$1F | $0000-$1FFF | LowRAM,从 bank $7E 镜像 | (没有 ROM 映射) | $7E (WRAM 的前两个页面) |
$2000–$20FF | 未使用 | (没有 ROM 映射) | $80–$9F | |
$2100–$21FF | PPU1、APU、硬件寄存器 | (没有 ROM 映射) | $80–$9F | |
$2200–$2FFF | 未使用 | (没有 ROM 映射) | $80–$9F | |
$3000–$3FFF | DSP、SuperFX、硬件寄存器(有关于此页面的文档吗?我找不到任何来源) | (没有 ROM 映射) | $80–$9F | |
$4000–$40FF | 旧式手柄寄存器 | (没有 ROM 映射) | $80–$9F | |
$4100–$41FF | 未使用 | (没有 ROM 映射) | $80–$9F | |
$4200–$44FF | DMA、PPU2、硬件寄存器 | (没有 ROM 映射) | $80–$9F | |
$4500–$5FFF | 未使用 | (没有 ROM 映射) | $80–$9F | |
$6000–$7FFF | 保留 | (没有 ROM 映射) | $80–$9F | |
$8000-$FFFF | HiROM 部分(程序内存) | $00: $008000 - $00FFFF
$01: $018000 - $01FFFF
$02: $028000 - $02FFFF
...
$1D: $1D8000 - $1DFFFF
$1E: $1E8000 - $1EFFFF
$1F: $1F8000 - $1FFFFF
|
$80–$9F | |
$20–$3F | $0000-$1FFF | LowRAM,从 bank $7E 镜像 | (没有 ROM 映射) | $7E (WRAM 的前两个页面) |
$2000–$20FF | 未使用 | (没有 ROM 映射) | $A0-$BF | |
$2100–$21FF | PPU1、APU、硬件寄存器 | (没有 ROM 映射) | $A0-$BF | |
$2200–$2FFF | 未使用 | (没有 ROM 映射) | $A0-$BF | |
$3000–$3FFF | DSP、SuperFX、硬件寄存器(有关于此页面的文档吗?我找不到任何来源) | (没有 ROM 映射) | $A0-$BF | |
$4000–$40FF | 旧式手柄寄存器 | (没有 ROM 映射) | $A0-$BF | |
$4100–$41FF | 未使用 | (没有 ROM 映射) | $A0-$BF | |
$4200–$44FF | DMA、PPU2、硬件寄存器 | (没有 ROM 映射) | $A0-$BF | |
$4500–$5FFF | 未使用 | (没有 ROM 映射) | $A0-$BF | |
$6000–$7FFF | 卡带 SRAM - 8 千字节(总计 256 KB) | (没有 ROM 映射) | $A0-$BF | |
$8000-$FFFF | HiROM 部分(程序内存) | $20: $208000 - $20FFFF
$21: $218000 - $21FFFF
$22: $228000 - $22FFFF
...
$3D: $3D8000 - $3DFFFF
$3E: $3E8000 - $3EFFFF
$3F: $3F8000 - $3FFFFF
|
$A0-$BF | |
$40–$7D | $0000-$FFFF | HiROM 部分(程序内存) | $00: $000000 - $00FFFF
$01: $010000 - $01FFFF
$02: $020000 - $02FFFF
...
$3B: $3B0000 - $3BFFFF
$3C: $3C0000 - $3CFFFF
$3D: $3D0000 - $3DFFFF
|
$C0-$FD |
$7E | $0000-$1FFF | LowRAM (WRAM) | (没有 ROM 映射) | $00–$3F (WRAM 的前两个页面) |
$2000–$7FFF | HighRAM (WRAM) | (没有 ROM 映射) | (没有映射) | |
$8000-$FFFF | 扩展 RAM (WRAM) | (没有 ROM 映射) | (没有映射) | |
$7F | $0000-$FFFF | 扩展 RAM (WRAM) | (没有 ROM 映射) | (没有映射) |
$80–$9F | $0000-$FFFF | 镜像 $00–$1F | (参见 bank $00–$1F) | $00–$1F |
$A0-$BF | $0000-$FFFF | 镜像 $20–$3F | (参见 bank $20–$3F) | $20–$3F |
$C0-$FD | $0000-$FFFF | 镜像 $40–$7D | (参见 bank $40–$7D) | $40–$7D |
$FE-$FF | $0000-$FFFF | HiROM 部分(程序内存) | $3E: $3E0000 - $3EFFFF
$3F: $3F0000 - $3FFFFF
|
(没有映射) (请记住这些 bank) (在 <$7F 范围内被 WRAM 覆盖了) |
HiROM 的理解稍微复杂一些。与 LoROM 不同,它不使用 $80 (128) 个区域来映射 ROM 到 SNES 的地址空间,而只使用 $40 (64) 个区域。同样与 LoROM 不同,这些区域被充分利用,即每个块为 64 KB。64 KB * $40 个区域 == 4 MB。区域 $40 - $7D (地址:$0000 - $FFFF) 以及区域 $C0 - $FF 包含连续数据。注意,HiROM 还为区域 $00 - $3F 和 $80 - $BF 创建了映射。由于这些是系统区域,它们的低页 (<8) 已被映射 - 但高页是空闲的,因此 ROM 的许多部分被镜像四次到 SNES 的地址空间。卡带上的 SRAM 被映射到区域 $20 - $3F,以 8 KB 块的形式。由于只有 32 个区域为此保留,因此理论上 HiROM 中可访问的 SRAM 量低于 LoROM (256 KB 对 512 KB)。由于 SNES 的 WRAM 被映射到区域 $7E - $7F,因此这些区域不会映射到最后一个 ROM 块。必须通过 $80 - $FF 区域访问此内存。
区域 $80 - $FF 也可以用于更快的内存访问。许多内存区域 <$80 以 2.68 MHz (200 ns) 的速度访问。如果地址 $420D (硬件寄存器) 的值为 1,则访问内存 >$80 以 3.58 MHz (120 ns) 的速度进行。
LoROM 基本上意味着地址线 A15 被卡带忽略,因此卡带不会区分任何区域中的 $0000-$7FFF 和 $8000-$FFFF。较小的 ROM 使用此模型来防止在区域 $00–$3F 中浪费空间。
LoROM 和 HiROM 被设计用来将 4 MB 存储在 ROM 中,并将其映射到 SNES 的 24 位地址空间(如果忘记了,它最多可以寻址 16 MB)。通常,对于许多游戏来说,这已经足够了,特别是考虑到 SNES 最初并不打算只使用控制台来渲染许多后来的游戏。这也是 SNES 为 SuperFX 芯片保留一些地址空间的原因,这些芯片可以使游戏节省 CPU - 它并没有打算让 SNES 在没有增强芯片的帮助下玩像《梦幻模拟战》、《最终幻想 6》或《星之卡比》这样的游戏。
除了 SNES 设计上的硬件问题之外,程序员也面临着巨大的空间限制。《最终幻想 6》的英文剧本 - 确切地说,是早期草稿 - 被削减了 75%,因为否则它会耗尽用于日版游戏的 24 兆位 (3 MB) 卡带。将 ROM 从 24 MBits 扩展到 32 MBits 不会造成太大的问题,但超过 4 MB 的任何内容都会超出 LoROM/HiROM 的范围。
SNES 的一个巨大优势在于它没有定义如何将某些地址块映射到卡带的 ROM 中。卡带负责将它需要使用的地址正确映射到它的 ROM 中。对于想要编程模拟器的人来说,这种行为会带来一些问题,因为他们必须编程并模拟 SNES 硬件以及卡带。请记住,卡带与 SNES 一样,包含它自己的处理器和微芯片,这些处理器和微芯片可能会表现出不同的行为。例如,一个常见的问题:在卡带中使用 MAD-1 地址解码芯片,它将 $40–7D:8000-FFFF ROM 区域的内容映射到区域的低半部分(因此 $400005 和 $408005 之间没有区别 - 请参阅 LoROM 表以了解确切的地址)。真正的 SNES 不必关心这一点,模拟器必须关心。
因此,使用一种格式,允许使用高达 8 MB 的 ROM 空间而无需使用扩展 MMC 芯片,不会造成明确的硬件问题,因为卡带上的地址解码芯片需要处理这个问题。两种新的格式被引入:ExLoROM(如果好奇,前缀“Ex”代表“扩展”),以及 ExHiROM。只有两款游戏实际上从未在北美正式发布,使用了 ExHiROM,分别是《梦幻模拟战》和《大怪兽物语 2》。另一方面,ExLoROM 没有用于任何商业游戏,被认为是一种非官方的映射,因此模拟器对它的支持有时已知存在不足或不一致。但是,包含新游戏内容的 ROM 补丁通常会扩展原始 ROM 的大小,并且可能会使用这两种格式中的一种。事实上,《时空之轮》的“DoctorL”和“KajarLab”的重新翻译都将游戏从 4 MB 扩展到 6 MB。
关于 ExLoROM 和 ExHiROM 的内存布局,有很多误导性或难以理解的解释和示例,原因有两个。
- 黑客通常不会遇到这些格式。
- 无法理解为什么有人需要如此多的内存。*
首先让我们介绍一下 ExLoROM 的内存映射 - 我们不会详细介绍系统区域低半部分的映射,因为这些已经在之前介绍过了,并且不会改变扩展 ROM 格式。
Bank | 偏移量 | ROM 地址 | 镜像 |
---|---|---|---|
$00–$3F | $8000-$FFFF | $00: $400000 - $407FFF
$01: $408000 - $40FFFF
$02: $410000 - $417FFF
...
$3D: $5E8000 - $5EFFFF
$3E: $5F0000 - $5F7FFF
$3F: $5F8000 - $5FFFFF
|
$80-$BF |
$40–$6F | $8000-$FFFF | $40: $600000 - $607FFF
$41: $608000 - $60FFFF
$42: $610000 - $617FFF
...
$6D: $768000 - $76FFFF
$6E: $770000 - $777FFF
$6F: $778000 - $77FFFF
|
(没有映射) |
$70–$7D | $8000-$FFFF | $70: $780000 - $787FFF
$71: $788000 - $78FFFF
$72: $790000 - $797FFF
...
$7B: $7D8000 - $7DFFFF
$7C: $7E0000 - $7E7FFF
$7D: $7E8000 - $7EFFFF
|
$F0-$FD |
$80-$BF | $8000-$FFFF | (参见 bank $00–$3F) | $00–$3F |
$C0-$EF | $8000-$FFFF | $C0: $200000 - $207FFF
$C1: $208000 - $20FFFF
$C2: $210000 - $217FFF
...
$ED: $368000 - $36FFFF
$EE: $370000 - $377FFF
$EF: $378000 - $37FFFF
|
(没有映射) |
$F0-$FD | $8000-$FFFF | $F0: $380000 - $387FFF
$F1: $388000 - $38FFFF
$F2: $390000 - $397FFF
...
$FB: $3D8000 - $3DFFFF
$FC: $3E0000 - $3E7FFF
$FD: $3E8000 - $3EFFFF
|
$70–$7D |
$FE-$FF | $8000-$FFFF | $FE: $3F0000 - $3F7FFF
$FF: $3F8000 - $3FFFFF
|
(没有映射) |
注意:这些信息是错误的,因为我不得不意识到。从偏移量 $200000 开始的 ROM 毫无意义。此外,由于它仍然是 LoROM 格式,因此第一个 ROM 区域必须加载到 $00:8000-FFFF。但是,模板将很快被修正,并且全部删除它没有任何意义。
(ExHiROM 的解释和表格即将推出)。
(*第二个原因实际上是一种保护机制。如果黑客遇到有人想要将 ROM 扩展到 32 兆位限制之外,黑客会自动假设这个人是一个新手,对他在做什么没有太多概念,而且在很多情况下,他是对的。但是,这并不能证明关于这些主题的文档缺乏。
在区域 0(卡带的第一个区域)的末尾,存储了 64 ($40) 字节的卡带信息。这些信息对于执行存储在卡带上的 ROM 至关重要,因为它包含 ROM 的内部名称、中断向量(ROM 中机器代码的地址,以 16 位格式)、版本等。ROM 的这部分也称为 SNES 标头(不要与我们很快会谈到的 SMC 标头混淆)。
ROM 的第一个区域的末尾取决于它使用的内存映射。在 LoROM 的情况下,第一页的末尾位于 $7FFF,对于 HiROM,第一页的末尾位于 $FFFF。因此,您基本上必须检查 ROM 的这两个位置,以查看信息是否符合标准标头格式,也就是说,它们是否足够合理。
一些卡带附带所谓的 SMC 标头。SMC 标头通常由复制设备在 ROM 的开头写入,并占用 $200 (512) 字节的额外空间。补丁通常依赖于您的 ROM 是“带标头的”还是“无标头的”,因为这会改变 ROM 中的偏移量。应用到错误类型 ROM 的补丁很容易破坏卡带的程序代码。
创建 SMC 标头“相对”简单,删除它更简单。通常,SMC 标头包含 ROM 的大小除以 8 KB 单位 &$FF (按位与 255) 在字节 0 中,大小 >> 8 (按位右移一个完整的字节) 在字节 1 中,以及卡带类型在字节 2 中。剩下的其他字节 (509) 被设置为零。要创建标头,您只需在 ROM 的开头添加一个 512 字节的块,其中包含这些信息。要删除标头,您只需在“忘记”前 512 字节的情况下将 ROM 保存到您的 HDD/SSD 上。
我们为什么会在应该处理 SNES 标头时关注 SMC 标头 - 实际上,有两个原因。
1. 在将 ROM 映射到内存时,必须考虑 SMC 标头是否存在。带标头的 ROM 中的区域不会从偏移量 $0 开始,而是从 $200 (512,标头的大小) 开始,因此区域的末尾也会相应地移动。
2. 如果 ROM 中没有 SMC 标头,或者您在解析 ROM 时不信任这些信息,您唯一确定您要加载的卡带类型的方法是检查已经提到的位置的格式正确的标头。但是,如果您信任这些信息(您不应该),这是一种确定您是处理 LoROM 还是 HiROM 的方法。
在尝试加载这些信息之前,您必须确定 SMC 的大小,无论它是否存在。ROM 大小模 1024 (ROM 大小 % $400) 会给出 SMC 标头的大小。如果为 0,则没有标头。如果不是 512,则标头格式错误。
ROM 中的以下偏移量是无标头 ROM 的偏移量。您必须将 SMC 标头的大小添加到 ROM 中的所有地址,才能正确读取它。x 是当前 ROM 区域的最后一页(如果卡带使用 LoROM 映射,则为 $7,如果使用 HiROM,则为 $F)。
偏移量 | 名称 | 描述 |
---|---|---|
$xFC0 | 游戏标题 | 21 字节,通常为大写 ASCII。 |
$xFD5 | 映射模式 | 001ABBBB; A==1 表示 FastROM ($10)。如果 BBBB 是映射模式。 |
$xFD6 | ROM 类型 | 表示卡带包含扩展芯片、SRAM、电池等。 |
$xFD7 | ROM 大小 | ROM 的大小,存储为 。如果您执行 $400<<(ROM 大小),您将获得以字节为单位的总大小。 |
$xFD8 | SRAM 大小 | SRAM 的大小,或称为卡带 RAM。存储为 。通常用于保存游戏进度。要再次获取大小,请使用 $400<<(RAM 大小)。 |
$xFD9 | 开发人员 ID | 如果使用标题版本 3,则为 $33 |
$xFDB | 版本号 | |
$xFDC | 校验和补码 | |
$xFDE | 校验和 |
在此信息之后,中断向量表开始。中断是硬件直接发送给 CPU 的信号,如果中断未被屏蔽,则需要处理该信号。中断向量指定处理给定类型中断的代码的地址。这些向量的大小均为 16 位,应位于 ROM 的第一个区块中。这是因为 DBR(数据区块寄存器)和 PBR(程序区块寄存器)在每次更改仿真模式或执行中断时都会被设置为 0。
向量的大小均为 2 字节,并以小端格式存储(如果读者不知道这一点 - CPU 是小端 CPU,这意味着 MSB 存储在更高的位置。大端意味着 MSB 存储在更低的位置)。程序执行在仿真模式下从复位向量 ($xFFC-D) 开始。
中断名称 | ROM 中的偏移量 | 描述 |
---|---|---|
COP | $xFE4-5 | 软件中断。由 COP 指令触发。类似于 BRK。 |
BRK | $xFE6-7 | 软件中断。由 BRK 指令触发。类似于 COP。 |
ABORT | $xFE8-9 | 在 SNES 中未使用。 |
NMI | $xFEA-B | 不可屏蔽中断。在垂直刷新(vblank)开始时调用。 |
IRQ | $xFEE-F | 中断请求。可以设置为在水平刷新周期的特定位置调用。 |
中断名称 | ROM 中的偏移量 | 描述 |
---|---|---|
COP | $xFF4-5 | 软件中断。由 COP 指令触发。 |
ABORT | $xFF8-9 | 在 SNES 中未使用。 |
NMI | $xFFA-B | 不可屏蔽中断。在垂直刷新(vblank)开始时调用。 |
RES | $xFFC-D | 复位向量,执行从此向量开始。 |
IRQ/BRK | $xFFE-F | 中断请求。可以设置为在水平刷新周期的特定位置调用。也是由 BRK 指令触发的软件中断。 |
本节介绍 LoROM 和 HiROM 的初始解析方法。本文将要分析的游戏是 *最终幻想 4*(LoROM)和 *最终幻想 6*(HiROM),它们都是无标题的。
首先,我们查看 ROM 中的地址 $7FC0 - LoROM 区块的最后一个字节是 $7FFF,标题占用高达 64/$40 字节。因此,如果它是 LoROM(注意,我们还不知道),标题的第一个字节位于 $7FC0。
00007FC0 46 49 4E 41 4C 20 46 41 4E 54 41 53 59 20 49 49 |FINAL FANTASY II|
00007FD0 20 20 20 20 20 20 02 0A 03 01 C3 00 0F 7A F0 85 | .......z..|
00007FE0 FF FF FF FF FF FF FF FF FF FF 00 02 FF FF 04 02 |................|
00007FF0 FF FF FF FF FF FF FF FF FF FF FF FF 00 80 FF FF |................|
看起来不错。在前两行,写入了 ROM 的名称(由于 *最终幻想 4* 在北美发布时被称为 *最终幻想 2*,所以这实际上是有道理的)。前 21 个字节只是 ASCII 字符(也是在 ROM 中看到明文的难得机会,因为对话通常以与 ASCII 不同的格式存储)。此字符串之后的下一个字节可能会被误认为是另一个空格字符,但实际上 $D5 处的字节表明它是一个 LoROM 卡带(现在我们有两个证明该卡带是 LoROM:首先是标题的位置,其次是指示卡带使用 LoROM 的字节。顺便说一句,如果这两个不匹配,则 ROM 被认为是损坏的)。
人们可能会在那里进行其他检查(例如,ROM 的大小与标题中存储的大小,国家和许可证代码,SRAM 大小...),但就目前而言,只有另外两个字节对启动 ROM 至关重要:复位中断向量在仿真模式下的位置(在 ROM 被映射到 SNES 地址空间的那一刻发出)。该向量以小端格式存储在 $7FFC-D 中(见上文),这意味着向量按以下方式计算
emulation_reset_vector = rom_content[0x7FFC] | (rom_content[0x7FFD] << 8);
在这个例子中,向量是 $8000 - 启动代码位于第一个区块的开头(是的,第一个区块。请记住第一个区块是如何存储在 $8000 - $FFFF 中的,而 $0000 - $7FFF 包含系统信息?)。
让我们看一下那里的代码
十六进制
00000000 78 18 FB C2 10 E2 20 9C 0D 42 9C 0B 42 9B 0B 42 |x..... ..B..B..B|
00000010 A9 8D 8D 00 21 A9 00 8D 00 42 A9 00 EB A9 00 48 |....!....B.....H|
反汇编(使用我正在开发的程序,所以不要问我在哪里找到它)
$00:8000: 78 ; SEI, disables interrupts. This is a crucial point in the execution of the ROM, so the CPU must not be interrupted.
$00:8001: 18 ; CLC, clears the carry flag. This makes sense in the next instruction.
$00:8002: FB ; XCE, switches into native mode if the carry flag is clear.
$00:8003: C210 ; REP #$10, sets the X and Y registers into 16 bit mode.
$00:8005: E220 ; SEP #$20, sets the accumulator into 8 bit mode. This is all possible with the CPU still being in native mode.
$00:8007: 9C0D42 ; STZ $420D, sets the memory access speed at 200ns when accessing banks >$7F.
$00:800A: 9C0B42 ; STZ $420B, disables all DMA processes that might have been started.
$00:800D: 9C0C42 ; STZ $420C, disables all HDMA processes that might have been started.
$00:8010: A98F ; LDA #$8F, bitmask 10001111 ...
$00:8012: 8D0021 ; STA $2100, ... disables rendering and sets brightness to max.
$00:8015: A900 ; LDA #$00, sets NMI and V/H count to zero and disables the joypad.
$00:8017: 8D0042 ; STA $4200, OK, actually this is done here ...
$00:801A: A900 ; LDA #$00, stuff that is of no interest here ...
$00:801C: EB ; XBA
$00:801D: A900 ; LDA #$00
$00:801F: 48 ; PHA
您会看到这似乎足以成为完全有效的启动代码。
再次,让我们查看偏移量 $7FC0
00007FC0 CD 85 CB C2 20 A5 D0 0A AA BF 02 E6 CC 85 C9 A5 |.... ...........|
00007FD0 D0 CF 00 E6 CC 90 05 7B E2 20 E6 CB 7B E2 20 A9 |.......{. ..{. .|
00007FE0 01 8D 68 05 60 9C 67 05 AD B9 1E 29 40 D0 05 AD |..h.`.g....)@...|
00007FF0 45 07 D0 04 9C 45 07 60 A9 64 8D 67 05 A9 CE 85 |E....E.`.d.g....|
这似乎有点不对劲。也许它使用 HiROM?
0000FFC0 46 49 4E 41 4C 20 46 41 4E 54 41 53 59 20 33 20 |FINAL FANTASY 3 |
0000FFD0 20 20 20 20 20 31 02 0C 03 01 33 00 CD a0 32 5F | 1....3...2_|
0000FFE0 FF FF FF FF FF FF FF FF FF FF 10 FF FF FF 14 FF |................|
0000FFF0 FF FF FF FF FF FF FF FF FF FF FF FF 00 FF FF FF |................|
是的,它确实如此。复位向量位于 $FF00。再次使用十六进制表示
0000FF00 78 18 FB 5C 19 00 C0 FF FF FF FF FF FF FF FF FF |x..\............|
反汇编
$00:FF00: 78 ; SEI, same start-up code ...
$00:FF01: 18 ; CLC, ... as before ...
$00:FF02: FB ; XCE, ... isn't it?
$00:FF03: 5C1900C0; JML $C00019, this is a bit different. The CPU preforms a long jump to a different location
简而言之,此代码告诉正在执行的 CPU $C0:0019 是下一个要执行的指令。由于这是 HiROM 内存映射,这意味着我们必须查看区块 $C0 被映射到哪里。参考以前的 HiROM 映射表,区块 $C0 - $FD 是 $40 – $7D 的直接映射。事实上,这些区块连续地包含了所有 ROM 内容 - 由于 $C0 <=> $40 且 $40 是开头,因此要执行的代码基本上位于 ROM 的开头,偏移量 $19
00000000 20 79 68 6B 03 00 08 08 4C 76 C1 4C 3F 4A EA EA | yhk....Lv.L?J..|
00000010 EA EA EA EA EA 20 24 BB 6B 78 18 FB E2 20 C2 10 |..... $.kx... ..|
00000020 A2 FF 15 9A A2 00 00 DA 2B 7B 48 AB A9 01 8D 0D |........+{H.....|
反汇编
$C0:0019: 78 ; SEI, same code as before?
$C0:001A: 18 ; CLC, basically it IS the same code. This leads to the conclusion that the programmers wanted to make sure there are no interrupts enabled.
$C0:001B: FB ; XCE
$C0:001C: E220 ; SEP #$20, sets the accumulator into 8 bit mode.
$C0:001E: C210 ; REP #$10, sets the X and Y registers into 16 bit mode.
$C0:0020: A2FF15 ; LDX #$15FF, now THIS is interesting. The next instruction sets the stack. By default, the stack in
; emulation mode is set to $100 and grows downwards ...
$C0:0023: 9A ; TXS, ... and this is not changed with entering native mode. This instruction sets the stack to $15FF.
$C0:0024: A20000 ; LDX #$0000, the next three instructions set the X register to 0, push that value on the new stack, and pull it to the
; Direct Page register immediately.
$C0:0027: DA ; PHX, the reason why they have to do this is because the accumulator is 8 bit and the direct page register is 16 bits.
$C0:0028: 2B ; PLD
$C0:0029: 7B ; TDC, and now they set A to that value.
$C0:002A: 48 ; PHA, now this makes sense. The DBR register can be directly set only via pulling the value from stack ...
$C0:002B: AB ; PLB, ... which is done here. So all relative accesses should be directed to bank $00.
$C0:002C: A901 ; LDA #$01
$C0:002E: 8D0D42 ; STA $420D, and again the hardware registers are set ...