Re: 從零開始的紅白機模擬 - [04]6502彙編
本文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進位字元串.