像任何其他“编译器”一样,汇编器最好写成词法分析器,输入到语言语法处理器中。
汇编语言通常比常规编译语言更容易,因为您无需担心构造跨越行边界并且格式通常是固定的。
大约两年前,出于教育目的,我为(虚构的)CPU 编写了一个汇编程序,它基本上将每一行视为:
- 可选标签(例如,
:loop
)。
- 操作(例如,
mov
)。
- 操作数(例如,
ax,$1
)。
最简单的方法是确保标记易于区分。
这就是为什么我制定了标签必须从头开始的规则:
——它使对线条的分析变得如此容易。处理一条线的过程是:
- 去掉注释(首先
;
在字符串之外到行尾)。
- 提取标签(如果存在)。
- 第一个词是操作。
- 其余是操作数。
您可以轻松地坚持不同的操作数也具有特殊标记,以使您的生活更轻松。所有这一切都假设您可以控制输入格式。如果您需要使用 Intel 或 AT&T 格式,那就有点困难了。
我处理它的方法是调用一个简单的按操作函数(例如,、、、doJmp
)doCall
,doRet
该函数决定操作数中允许的内容。
例如,doCall
只允许数字或标签,doRet
不允许任何内容。
例如,这是该encInstr
函数的代码段:
private static MultiRet encInstr(
boolean ignoreVars,
String opcode,
String operands)
{
if (opcode.length() == 0) return hlprNone(ignoreVars);
if (opcode.equals("defb")) return hlprByte(ignoreVars,operands);
if (opcode.equals("defbr")) return hlprByteR(ignoreVars,operands);
if (opcode.equals("defs")) return hlprString(ignoreVars,operands);
if (opcode.equals("defw")) return hlprWord(ignoreVars,operands);
if (opcode.equals("defwr")) return hlprWordR(ignoreVars,operands);
if (opcode.equals("equ")) return hlprNone(ignoreVars);
if (opcode.equals("org")) return hlprNone(ignoreVars);
if (opcode.equals("adc")) return hlprTwoReg(ignoreVars,0x0a,operands);
if (opcode.equals("add")) return hlprTwoReg(ignoreVars,0x09,operands);
if (opcode.equals("and")) return hlprTwoReg(ignoreVars,0x0d,operands);
这些hlpr...
函数只是获取操作数并返回一个包含指令的字节数组。当许多操作具有相似的操作数要求时,它们很有用,例如 adc ,
add和and
` 在上述情况下都需要两个寄存器操作数(第二个参数控制为指令返回的操作码)。
通过使操作数的类型易于区分,您可以检查提供了哪些操作数、它们是否合法以及要生成哪些字节序列。将操作分离到它们自己的功能中提供了一个很好的逻辑结构。
此外,大多数 CPU 遵循从操作码到操作的合理逻辑转换(以使芯片设计人员的工作更轻松),因此所有操作码上都会有非常相似的计算,例如,允许索引寻址。
为了在允许可变长度指令的 CPU 中正确创建代码,最好分两遍进行。
在第一遍中,不生成代码,只生成指令的长度。这允许您在遇到所有标签时为其分配值。第二遍将生成代码并可以填写对这些标签的引用,因为它们的值是已知的。上面的ignoreVars
代码段中的 用于此目的(返回代码的字节序列,因此我们可以知道长度,但对符号的任何引用仅使用 0)。