fc nes模拟器 6502汇编

原文:Re: 从零开始的红白机模拟 - [04]6502汇编 - Dust Loong的文章 - 知乎 https://zhuanlan.zhihu.com/p/44051504

fc nes模拟器 6502汇编

Re: 从零开始的红白机模拟 - [04]6502汇编

fc nes模拟器 6502汇编

Dust Loong

兴趣:人机交互 https://github.com/dustpg

5 人赞了该文章

本文github备份地址

STEP2: 6502汇编

写模拟器当然要了解使用到的汇编, 不管是调试模拟器还是模拟器调试都需要.

前面知道了6502汇编用\$表示十六进制, 那先讲讲6502机器码, 由一个操作码和0~2个地址码构成, 都是8位的:

/// <summary>
/// StepFC: 6502机器码
/// </summary>
typedef union {
    // 用32位保存数据
    uint32_t    data;
    // 4个8位数据
    struct {
        // 操作码 
        uint8_t op;
        // 地址码1
        uint8_t a1;
        // 地址码2
        uint8_t a2;
        // 显示控制
        uint8_t ctrl;
    };

} sfc_6502_code_t;

其中, 值得注意的是C11才支持的匿名struct/union. 请检查自己编译器支持的情况.

理论上, 6502拥有256条操作码, 这里是所有的指令表: 非官方OpCode

有一些被称为非法或者说未被文档记录的操作码, 但是文档提到

An accurate NES emulator must implement all instructions, not just the official ones. A small number of games use them (see below).

我们必须实现所有操作码.

指令

举一个简单的指令LDA, 就是"load 'A'(累加器)"的意思

寻址方式

指令LAD对应多个机器码, 区别是寻址方式的不同, 现在先列举所有的寻址方式以及格式, 详细的解释在下一节.

  • 累加器A寻址 Accumulator 单字节指令, 格式:
INS A
  • 隐含寻址 Implied Addressing 单字节指令, 格式:
INS
  • 立即寻址 Immediate Addressing 双字节指令, 格式
INS #$AB
  • 绝对寻址 Absolute Addressing 三字节指令, 格式
INS #$ABCD
  • 绝对X变址 Absolute X Indexed Addressing 三字节指令, 格式
INS #$ABCD, X
  • 绝对Y变址 Absolute Y Indexed Addressing 三字节指令, 格式
INS #$ABCD, Y
  • 零页寻址 Zero-page Absolute Addressing 双字节指令, 格式
INS #$A
  • 零页X变址 Zero-page X Indexed Addressing 双字节指令, 格式
INS #$A, X
  • 零页Y变址 Zero-page Y Indexed Addressing 双字节指令, 格式
INS #$A, Y
  • 间接寻址 Indirect Addressing 三字节指令, 格式
INS ($ABCD)
  • 间接X变址: Pre-indexed Indirect Addressing 双字节指令, 格式
INS (#$A, X)
  • 间接Y变址: Post-indexed Indirect Addressing 双字节指令, 格式
INS ($AB), Y
  • 相对寻址: Relative Addressing 双字节指令, 格式
INS $AB   - 16进制
INS +-abc - 有符号十进制
INS xxxxx - 跳转的目标地址的标签, 由汇编器自动计算
INS $ABCD - 跳转的目标地址, 由汇编器自动计算

反汇编

由于我们只需要反汇编而不需要汇编, 所以输出格式看自己喜好.比如除了最后一个, 看自己喜好实现吧.

现在就是根据操作码查找寻址方式和指令就能反汇编了:

/// <summary>
/// StepFC: 指定地方反汇编
/// </summary>
/// <param name="address">The address.</param>
/// <param name="famicom">The famicom.</param>
/// <param name="buf">The buf.</param>
void sfc_fc_disassembly(uint16_t address, const sfc_famicom_t* famicom, char buf[]) {
    // TODO: 根据操作码读取对应字节

    sfc_6502_code_t code;
    code.data = 0;
    // 暴力(NoMo)读取3字节
    code.op = sfc_read_cpu_address(address, famicom);
    code.a1 = sfc_read_cpu_address(address + 1, famicom);
    code.a2 = sfc_read_cpu_address(address + 2, famicom);
    // 反汇编
    sfc_6502_disassembly(code, buf);
}

目前暴力(?)读取3字节, 下次再实现读取指定字节数量.

建立一张表用于反汇编:

/// <summary>
/// 命令名称
/// </summary>
struct sfc_opname {
    // 3字名称
    char        name[3];
    // 寻址模式
    uint8_t     mode;
};

/// <summary>
/// 反汇编用数据
/// </summary>
static const struct sfc_opname s_opname_data[256] = {
    { 'B', 'R', 'K', SFC_AM_IMP },
    // 下略
};

就能反汇编了:

  • 00 - BRK
  • 等等等等
  • 这些全部要自己查表才行
  • 自己使用的微软编译器, 对C11支持不大行, 像_Alignas之类的明明C++那边实现了, C这边却没有
  • 对于查表用的数据可以 以CPU缓存行为单位对齐

反汇编

有了数据反汇编就太简单了, 核心部分应该是尽可能少使用外部函数. 这里可以用snprintf之类的格式化函数, 但是毕竟核心部分, 自己还是自己手写了像格式化位16进制字符串.

输出每个向量的第一个执行代码的汇编代码

fc nes模拟器 6502汇编

C004 - SEI

 

这里, RESET执行的第一个指令是: SEI, 即 'Set I flag'

项目地址Github-StepFC-Step2

作业

  • 基础: 利用反汇编函数输出所有256个机器码的汇编代码
  • 扩展: 删掉反汇编实现函数, 自己实现反汇编函数.
  • 从零开始: 从零开始实现自己的模拟器吧

REF