设计并实现一个两遍扫描汇编器,将人类可读的汇编代码翻译为机器码。汇编器是软件开发的基础工具——从今天起,你不再需要手写十六进制机器码,而是可以用助记符编写程序,让工具自动翻译。
汇编器的核心任务是一对一映射:每条汇编指令对应一条机器指令。但这个"一对一"并不简单——标签(label)的前向引用需要两遍扫描才能解决。
考虑这段代码:
BNE LOOP ; 跳转到LOOP,但LOOP在后面定义
; ... 更多指令 ...
LOOP:
LDA #$05
当汇编器遇到BNE LOOP时,它还不知道LOOP的地址——因为LOOP还没被定义。第一遍扫描只计算每条指令的长度和标签地址,第二遍扫描才用完整的符号表生成机器码。
我们的8位CPU指令集编码(与前面课程一致的56条指令):
| 操作码 | 助记符 | 寻址模式 | 字节 | 说明 |
|---|---|---|---|---|
| $00 | NOP | 隐含 | 1 | 空操作 |
| $01 | LDA #imm | 立即 | 2 | A←立即数 |
| $02 | LDA $addr | 零页 | 2 | A←[$addr] |
| $03 | LDA $addr | 绝对 | 3 | A←[$addr16] |
| $04 | STA $addr | 零页 | 2 | [$addr]←A |
| $05 | STA $addr | 绝对 | 3 | [$addr16]←A |
| $06 | ADC #imm | 立即 | 2 | A←A+imm+C |
| $07 | ADC $addr | 绝对 | 3 | A←A+[addr]+C |
| $08 | SBC #imm | 立即 | 2 | A←A-imm-C |
| $09 | AND #imm | 立即 | 2 | A←A&imm |
| $0A | ORA #imm | 立即 | 2 | A←A|imm |
| $0B | EOR #imm | 立即 | 2 | A←A^imm |
| $10 | BCC rel | 相对 | 2 | C=0跳转 |
| $11 | BCS rel | 相对 | 2 | C=1跳转 |
| $12 | BEQ rel | 相对 | 2 | Z=1跳转 |
| $13 | BNE rel | 相对 | 2 | Z=0跳转 |
| $14 | JMP $addr | 绝对 | 3 | PC←addr |
| $15 | JSR $addr | 绝对 | 3 | 调用子程序 |
| $16 | RTS | 隐含 | 1 | 子程序返回 |
| $17 | RTI | 隐含 | 1 | 中断返回 |
我们用Python实现汇编器——它运行在主机上,生成8位电脑的机器码二进制文件:
#!/usr/bin/env python3
"""retro8asm.py - Retro8 两遍扫描汇编器"""
import sys
import re
# 指令定义: (助记符, 寻址模式) → (操作码, 字节数)
INSTRUCTIONS = {
'NOP': (0x00, 1),
'LDA': {'imm': (0x01, 2), 'zp': (0x02, 2), 'abs': (0x03, 3)},
'STA': {'zp': (0x04, 2), 'abs': (0x05, 3)},
'ADC': {'imm': (0x06, 2), 'abs': (0x07, 3)},
'SBC': {'imm': (0x08, 2)},
'AND': {'imm': (0x09, 2)},
'ORA': {'imm': (0x0A, 2)},
'EOR': {'imm': (0x0B, 2)},
'BCC': {'rel': (0x10, 2)},
'BCS': {'rel': (0x11, 2)},
'BEQ': {'rel': (0x12, 2)},
'BNE': {'rel': (0x13, 2)},
'JMP': {'abs': (0x14, 3)},
'JSR': {'abs': (0x15, 3)},
'RTS': (0x16, 1),
'RTI': (0x17, 1),
'PHA': (0x18, 1),
'PLA': (0x19, 1),
'INC': {'zp': (0x1A, 2), 'abs': (0x1B, 3)},
'DEC': {'zp': (0x1C, 2), 'abs': (0x1D, 3)},
'CMP': {'imm': (0x1E, 2)},
'CPX': {'imm': (0x1F, 2)},
'CPY': {'imm': (0x20, 2)},
'TAX': (0x21, 1),
'TXA': (0x22, 1),
'INX': (0x23, 1),
'DEX': (0x24, 1),
'INY': (0x25, 1),
'DEY': (0x26, 1),
'SEC': (0x27, 1),
'CLC': (0x28, 1),
'SEI': (0x29, 1),
'CLI': (0x2A, 1),
'ROL': (0x2B, 1),
'ROR': (0x2C, 1),
'ASL': (0x2D, 1),
'LSR': (0x2E, 1),
'LDX': {'imm': (0x2F, 2)},
'LDY': {'imm': (0x30, 2)},
'STX': {'zp': (0x31, 2), 'abs': (0x32, 3)},
'STY': {'zp': (0x33, 2), 'abs': (0x34, 3)},
'BIT': {'imm': (0x35, 2)},
}
class Assembler:
def __init__(self, origin=0x8000):
self.origin = origin
self.symbols = {} # 标签 → 地址
self.output = [] # 生成的机器码字节
self.errors = [] # 错误列表
self.line_num = 0
def parse_operand(self, operand):
"""解析操作数,返回(寻址模式, 值)"""
operand = operand.strip()
if not operand:
return ('imp', 0)
if operand.startswith('#'):
val = self._parse_value(operand[1:])
return ('imm', val)
if operand.startswith('$'):
val = self._parse_value(operand)
if val < 0x100:
return ('zp', val)
else:
return ('abs', val)
# 可能是标签
return ('label', operand)
def _parse_value(self, s):
"""解析数值:$HEX, %BINARY, DECIMAL"""
s = s.strip()
if s.startswith('$'):
return int(s[1:], 16)
elif s.startswith('%'):
return int(s[1:], 2)
else:
return int(s, 10)
def instruction_size(self, mnemonic, mode):
"""获取指令字节数"""
entry = INSTRUCTIONS.get(mnemonic)
if entry is None:
return 0
if isinstance(entry, tuple):
return entry[1]
mode_entry = entry.get(mode)
if mode_entry is None:
mode_entry = entry.get('abs', entry.get('imm'))
return mode_entry[1] if mode_entry else 0
def pass1(self, lines):
"""第一遍:构建符号表"""
addr = self.origin
for line_num, line in enumerate(lines, 1):
# 去除注释
line = line.split(';')[0].strip()
if not line:
continue
# 检查标签
if ':' in line:
parts = line.split(':', 1)
label = parts[0].strip()
self.symbols[label] = addr
line = parts[1].strip()
if not line:
continue
# 解析指令
tokens = line.split(None, 1)
mnemonic = tokens[0].upper()
operand = tokens[1] if len(tokens) > 1 else ''
# 伪指令处理
if mnemonic == '.ORG':
addr = self._parse_value(operand)
continue
elif mnemonic == '.DB':
bytes_list = operand.split(',')
addr += len(bytes_list)
continue
elif mnemonic == '.DW':
words = operand.split(',')
addr += len(words) * 2
continue
# 计算指令大小
mode, _ = self.parse_operand(operand)
if mode == 'label':
mode = 'abs' # 标签默认按绝对地址处理
size = self.instruction_size(mnemonic, mode)
if size == 0:
self.errors.append(f"Line {line_num}: Unknown instruction '{mnemonic}'")
addr += size
def pass2(self, lines):
"""第二遍:生成机器码"""
addr = self.origin
for line_num, line in enumerate(lines, 1):
line = line.split(';')[0].strip()
if not line:
continue
if ':' in line:
line = line.split(':', 1)[1].strip()
if not line:
continue
tokens = line.split(None, 1)
mnemonic = tokens[0].upper()
operand = tokens[1] if len(tokens) > 1 else ''
if mnemonic.startswith('.'):
# 伪指令在pass2中生成数据
if mnemonic == '.DB':
for b in operand.split(','):
self.output.append(self._parse_value(b.strip()) & 0xFF)
continue
elif mnemonic == '.DW':
for w in operand.split(','):
val = self._resolve_value(w.strip()) & 0xFFFF
self.output.append(val & 0xFF)
self.output.append((val >> 8) & 0xFF)
continue
elif mnemonic == '.ORG':
addr = self._parse_value(operand)
continue
# 解析操作数
mode, val = self.parse_operand(operand)
if mode == 'label':
val = self.symbols.get(val)
if val is None:
self.errors.append(f"Line {line_num}: Undefined label '{operand}'")
continue
mode = 'abs'
# 查找操作码
entry = INSTRUCTIONS.get(mnemonic)
if isinstance(entry, tuple):
opcode = entry[0]
else:
mode_entry = entry.get(mode)
if mode_entry is None:
self.errors.append(f"Line {line_num}: Invalid mode '{mode}' for '{mnemonic}'")
continue
opcode = mode_entry[0]
self.output.append(opcode)
if mode == 'imm':
self.output.append(val & 0xFF)
elif mode == 'zp':
self.output.append(val & 0xFF)
elif mode == 'abs':
self.output.append(val & 0xFF)
self.output.append((val >> 8) & 0xFF)
elif mode == 'rel':
offset = val - (addr + 2)
if offset < -128 or offset > 127:
self.errors.append(f"Line {line_num}: Branch out of range")
self.output.append(offset & 0xFF)
addr += len(self.output) - len(self.output)
def _resolve_value(self, s):
if s.startswith('$') or s.startswith('%'):
return self._parse_value(s)
if s in self.symbols:
return self.symbols[s]
return self._parse_value(s)
def assemble(self, source):
"""汇编源代码,返回机器码字节列表"""
lines = source.strip().split('\n')
self.pass1(lines)
if self.errors:
for e in self.errors:
print(f"ERROR: {e}")
return None
self.pass2(lines)
if self.errors:
for e in self.errors:
print(f"ERROR: {e}")
return None
return self.output
# 使用示例
if __name__ == '__main__':
source = """
.ORG $8000
LOOP:
LDA #$05
STA $FF10
DEC $FF10
BNE LOOP
RTS
"""
asm = Assembler()
result = asm.assemble(source)
if result:
print(' '.join(f'{b:02X}' for b in result))
# 输出: A9 05 8D 10 FF CE 10 FF D0 F6 60
为汇编器添加宏指令支持:.MACRO定义宏,.ENDM结束定义,宏可以在代码中多次调用。例如定义一个PRINT_CHAR宏,将A寄存器的值发送到UART。宏展开应该在pass1之前完成。
实现条件汇编伪指令:.IF、.ELSE、.ENDIF。根据常量值决定是否包含某段代码。例如.IF DEBUG只在调试模式时包含额外的诊断输出代码。这在嵌入式开发中非常有用。
实现反汇编器:输入机器码二进制文件,输出可读的汇编代码。这是调试工具的核心——当程序出错时,你需要看到机器码对应的汇编指令。注意处理数据区域(.DB定义的常量)和代码区域的区分。
实现简单的链接器:将多个汇编模块的目标文件合并为一个可执行文件。处理外部符号引用(在一个模块中定义、在另一个模块中使用的标签)。这是从单文件汇编到模块化开发的关键步骤。
你实现了完整的两遍扫描汇编器!这包括:
汇编器是软件工具链的第一环——从现在起,你可以用助记符编写程序,而不是手写机器码了!