GB/游戏卡带/Header

Cartridge 保存了游戏的汇编代码与全部二进制数据(图像, 音乐等), 它是一种结构化的数据存储格式. 与众多常用的文件存储格式类似, Cartridge 在文件开头也有一段特殊区域, 被称为 Header(文件头). Header 内包含了描述该 Cartridge 的信息以及关于引导 Game Boy 游戏机正常读取游戏数据的程序指令. Cartridge 的 Header 区位于 Cartridge 的 0x0100-0x0140(第 256 个字节到第 320 个字节)位置.

文件头是位于文件开头的一段承担一定任务的数据, 一般都在文件开头部分, 但也有一些位于文件结尾, 这取决于特定的文件格式. 文件头通常保存该文件的一些信息, 比如数据存储格式与压缩方式, 用于引导文件处理程序正确读取文件信息. 用一个简单的例子来说明, 比如一个 JPG 格式的图像文件, 图像浏览器无需读取文件的全部数据而仅仅需要读取 Header 中的一部分数据, 就能获知图形的长宽等信息.

0100-0103 程序执行入口

每当游戏卡带插入 Game Boy 游戏机后, 屏幕将显示任天堂的 LOGO. 在显示完成任天堂的 LOGO 后, Game Boy 内置的启动程序将会把程序计数器 PC 设置到 0x0100 地址, 之后, 该区段中包含的程序指令会使程序计数器跳转到 Cartridge 内的实际入口地址. 通常来讲, 这 4 个 Byte 包含一个 nop 指令, 和一个 jp 0x0150 指令, 这两个指令会指示 CPU 跳转到 0x0150 的位置.

这段地址区域的地址与其对应的值如下表所示.

AddrData
0x01000x00
0x01010xc3
0x01020x50
0x01030x01

其等价汇编表示为:

nop
jp  0x0150

该地址区域存储了任天堂的 Logo 图标. 当 Game Boy 开机的时候, 首先会显示这部分内容, 之后验证该图像的内容. 如果这部分内容的字节不正确, 则会锁定自身并拒绝继续运行. 这部分内容的 16 进制格式如下所示.

CE ED 66 66 CC 0D 00 0B 03 73 00 83 00 0C 00 0D
00 08 11 1F 88 89 00 0E DC CC 6E E6 DD DD D9 99
BB BB 67 63 6E 0E EC CC DD DC 99 9F BB B9 33 3E

大部分情况下 Game Boy 只会验证前 0x18 字节, 但偶尔也会有验证全部 0x30 字节的情况.

任天堂之所以要求卡带内必须附带这部分信息是有历史原因的, 因为在互联网发展早期, 各个国家的版权法/著作权法并不完善, 并不都能非常权威的界定"在电脑上复制粘贴电子文件是否属于侵权", 因此有许多盗版商复制游戏卡带的内容并制作盗版卡带用于 Game Boy 游戏机. 任天堂通过在卡带中加入商标图像并在 Game Boy 游戏机上强制去鉴定商标内容, 巧妙的将对版权或著作权的侵权转换为对公司商标的侵权: 商标侵权在那时候已经有非常完善的法律去界定了. 当然另一个重要方面, 加入 Logo 亦有助于提升公司的社会知名度, 比如现今比较有名的游戏开发引擎 Unity, 如果开发者使用的是其免费版, 其制作和发行的游戏在启动时则必须显示 Unity 的 Logo.

img

0134-0143 标题

该字节区域存储了卡带的标题. 卡带的标题总是大写的 ASCII 英文字母. 如果标题长度小于 16 个字符, 剩余的存储空间则全部使用 0x00 填充. 在发明 Game Boy Color(CGB) 前, 任天堂将这个区域的长度减少到 15 个字符, 几个月之后他们就又有了将它减少到 11 个字符的奇妙想法. 因此一个 Game Boy 仿真器总是首先需要判断这盒卡带是不是 CGB 卡带, 然后再选用正确的读取方式读取标题. 它可以分为三个部分, 第一个部分是标题, 我们主要来讲后两个部分:

013F-0142 制造商代码

在非 CGB 卡带中, 这部分内容是标题的一部分. 在 CGB 卡带中, 这部分存储了制造商的代码. 暂时不明白如此这般做法有什么目的和深层次的意义.

0143 CGB 标志

在非 CGB 卡带中, 这部分内容是标题的一部分. 在 CGB 卡带中, 其高位表示该盒卡带已启用 CGB 功能, 这对于 CGB 来说这是必需的, 否则 CGB 会将自身切换为非 CGB 模式. 典型值为:

  • 0x80 - 该游戏支持 CGB 功能, 但同样能在非 CGB 游戏机上运行.
  • 0xC0 - 该游戏仅能运行在 CGB 游戏机上.

在 Game Boy 实机上, 如果该标志位设置了第 7 位, 以及第 2 或第 3 位, 会切换到特殊的非 CGB 模式, 该模式下系统调色盘未初始化. 目的未知, 但猜测最终这应该用于着色单色游戏上, 这些单色游戏可能在卡带的某个部分自己设定了调色盘数据.

0144-0145 许可协议代码

使用两个字节存储许可协议的代码, 这些代码和游戏公司或发布者相关. 一些典型的例子如下:

00  none                01  Nintendo R&D1   08  Capcom
13  Electronic Arts     18  Hudson Soft     19  b-ai
20  kss                 22  pow             24  PCM Complete
25  san-x               28  Kemco Japan     29  seta
30  Viacom              31  Nintendo        32  Bandai
33  Ocean/Acclaim       34  Konami          35  Hector
37  Taito               38  Hudson          39  Banpresto
41  Ubi Soft            42  Atlus           44  Malibu
46  angel               47  Bullet-Proof    49  irem
50  Absolute            51  Acclaim         52  Activision
53  American sammy      54  Konami          55  Hi tech entertainment
56  LJN                 57  Matchbox        58  Mattel
59  Milton Bradley      60  Titus           61  Virgin
64  LucasArts           67  Ocean           69  Electronic Arts
70  Infogrames          71  Interplay       72  Broderbund
73  sculptured          75  sci             78  THQ
79  Accolade            80  misawa          83  lozc
86  tokuma shoten i*    87  tsukuda ori*    91  Chunsoft
92  Video system        93  Ocean/Acclaim   95  Varie
96  Yonezawa/s'pal      97  Kaneko          99  Pack in soft
A4  Konami (Yu-Gi-Oh!)

0146 SGB 标志

该字节告知游戏是否支持 Super Game Boy(SGB) 功能, 常见值为:

  • 0x00: 游戏不支持 SGB 功能.
  • 0x03: 游戏支持 SGB 功能.

如果此字节设置为 0x03 以外的其他值, 则视为不支持 SGB 功能. SGB 是 Game Boy 的周边产品, 玩家可以把 SGB 插到 SFC 上然后在 SGB 上插上一张 GB 卡带, 这样玩家就可以在电视上玩 GB 游戏了, 实机如下.

img

0147 卡带类型

该字节告知游戏卡带所使用的 Memory Bank Controller 类型(缩写为 MBC, 下一节将会对此进行详细讨论), 以及游戏卡带中是否使用了其它外部硬件, 比如电池, 红外线感光器, 相机等. 通常来讲有以下几种常见硬件:

  • ROM: 不可变存储, 用来存储游戏本体数据
  • RAM: 可变存储, 通常用来存储游戏记录, 关闭 Game Boy 后数据清空
  • BATTERY: 电池, 用来持久化存储 RAM 中的内容, 关闭 Game Boy 后可向 RAM 供电从而保持 RAM 内的数据不变.
  • TIMER: 内部时钟, 用于记录时间.

值与其含义的对照如下所示:

0x00  ROM ONLY                 0x19  MBC5
0x01  MBC1                     0x1A  MBC5+RAM
0x02  MBC1+RAM                 0x1B  MBC5+RAM+BATTERY
0x03  MBC1+RAM+BATTERY         0x1C  MBC5+RUMBLE
0x05  MBC2                     0x1D  MBC5+RUMBLE+RAM
0x06  MBC2+BATTERY             0x1E  MBC5+RUMBLE+RAM+BATTERY
0x08  ROM+RAM                  0x20  MBC6
0x09  ROM+RAM+BATTERY          0x22  MBC7+SENSOR+RUMBLE+RAM+BATTERY
0x0B  MMM01
0x0C  MMM01+RAM
0x0D  MMM01+RAM+BATTERY
0x0F  MBC3+TIMER+BATTERY
0x10  MBC3+TIMER+RAM+BATTERY   0xFC  POCKET CAMERA
0x11  MBC3                     0xFD  BANDAI TAMA5
0x12  MBC3+RAM                 0xFE  HuC3
0x13  MBC3+RAM+BATTERY         0xFF  HuC1+RAM+BATTERY

0148 ROM 大小

该字节标明该卡带的 ROM 大小. 通常是 32KB 的整数倍. 常用的值与其实际表示的容量大小对应如下:

0x00 -  32KByte (no ROM banking)
0x01 -  64KByte (4 banks)
0x02 - 128KByte (8 banks)
0x03 - 256KByte (16 banks)
0x04 - 512KByte (32 banks)
0x05 -   1MByte (64 banks)  - only 63 banks used by MBC1
0x06 -   2MByte (128 banks) - only 125 banks used by MBC1
0x07 -   4MByte (256 banks)
0x08 -   8MByte (512 banks)
0x52 - 1.1MByte (72 banks)
0x53 - 1.2MByte (80 banks)
0x54 - 1.5MByte (96 banks)

0149 RAM 大小

该字节标明该卡带中外部 RAM 的大小. 常用的值与其实际表示的容量大小对应如下:

0x00 - None
0x01 - 2 KBytes
0x02 - 8 Kbytes
0x03 - 32 KBytes (4 banks of 8KBytes each)
0x04 - 128 KBytes (16 banks of 8KBytes each)
0x05 - 64 KBytes (8 banks of 8KBytes each)

需要注意的是, 当使用的是 MBC2 卡带时, 该位置必须设置为 0x00, 即使 MBC2 本身包含一个内置的容量为 512 x 4bits 的 RAM.

014A 发售地代码

该字节标明该卡带是否在非日本地区销售. 只有两个可选值:

0x00 - Japanese
0x01 - Non-Japanese

早期 Game Boy 和其游戏卡带只在日本国内市场有销售, 但随着它的扩张, 其发售地逐渐扩张到了美国, 欧洲, 台湾等国家和地区. 仅仅用一个"是/否日本地区发售"的标记显然已经无法满足需要, 因此该字节实际上处于一个没什么用处的尴尬地位.

014D 标题校验和

该字节包含卡带标题(地址区间 0x0134-0x014C)的 8 位校验和. 校验和计算伪代码如下:

x=0:FOR i=0134h TO 014Ch:x=x-MEM[i]-1:NEXT

其对应的 Rust 代码可以用如下一个简短函数表示:

fn check_checksum(rom: &[u8]) -> bool {
    let mut v: u8 = 0;
    for i in 0x0134..0x014d {
        v = v.wrapping_sub(rom[i]).wrapping_sub(1);
    }
    rom[0x014d] == v
}

返回结果的低 8 位必须与此条目中的值相同. 如果此校验和不正确, 则游戏将不会被执行.

014E-014F 全局校验和

该字节区域包含一个 16 位校验和, 此校验和将校验卡带的全部数据内容. Game Boy 并不会主动去验证该校验和.

由于缺乏相关的直接技术资料, 全局校验和的计算过程是从开源的 Game Boy 引擎 GBDK 的源代码中反推而来的, 其 C 语言源代码如下.

chk = 0;
cart[0x014E/SEGSIZE][0x014E%SEGSIZE] = 0;
cart[0x014F/SEGSIZE][0x014F%SEGSIZE] = 0;
for(i = 0; i < NBSEG; i++)
    for(pos = 0; pos < SEGSIZE; pos++)
        chk += cart[i][pos];
cart[0x014E/SEGSIZE][0x014E%SEGSIZE] = (chk>>8)&0xFF;
cart[0x014F/SEGSIZE][0x014F%SEGSIZE] = chk&0xFF;

其中变量 chk(checksum) 为一个 unsigned long 类型, 在开始计算全局校验和之前, 首先设置 0x014E 与 0x014F 为 0, 然后逐字节读取 Cartridge 中的数据并与 chk 相加. 循环结束后, chk 的值被保存在地址 0x014E 与 0x014F 中.

示例代码

本节将会编写一段简单的程序, 用来解析 Cartridge Header 的部分信息. 用作测试的游戏 ROM 是《Boxes》游戏, 读者可以在 https://github.com/mohanson/gameboy/tree/master/res 下载到该游戏 ROM. 程序的目的是读取游戏的标题, 并确认其标题校验和正确.

use std::io::Read;

// 获取游戏卡带的标题
fn rom_name(rom: &[u8]) -> String {
    let mut buf = String::new();
    let ic = 0x0134;
    // 根据是否支持 CGB 功能, 标题有不同的长度
    let oc = if rom[0x0143] == 0x80 { 0x013e } else { 0x0143 };
    for i in ic..oc {
        match rom[i] {
            0 => break,
            v => buf.push(v as char),
        }
    }
    buf
}

// 验证标题校验和
fn check_checksum(rom: &[u8]) -> bool {
    let mut v: u8 = 0;
    for i in 0x0134..0x014d {
        v = v.wrapping_sub(rom[i]).wrapping_sub(1);
    }
    rom[0x014d] == v
}

fn main() -> Result<(),  Box<std::error::Error>> {
    let mut f = std::fs::File::open("boxes.gb")?;
    let mut rom = Vec::new();
    f.read_to_end(&mut rom).unwrap();
    assert!(rom.len() >= 0x0150);
    assert!(check_checksum(&rom[..]));

    let rom_name = rom_name(&rom[..]);
    println!("{}",  rom_name); // BOXES
    Ok(())
}

运行上述代码, 此游戏的标题被成功打印至标准输出. 同时, 注意到代码中使用了一个 assert 语句来对标题校验和进行断言.

img

除了获取标题之外, 读者可以自行去完善这部分代码来尝试获取软件开发商, 发售地等信息.