Ассемблер, как и любой другой «компилятор», лучше всего писать как лексический анализатор, подающий в процессор грамматики языка.
Язык ассемблера, как правило, проще, чем обычные скомпилированные языки, так как вам не нужно беспокоиться о конструкциях, пересекающих границы строк, и формат обычно является фиксированным.
Я написал ассемблер для (вымышленного) процессора около двух лет назад для образовательных целей, и он в основном рассматривал каждую строку как:
- необязательный ярлык (например,
: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
и все они требуют двух операндов регистра в приведенном выше случае (второй параметр контролирует, какой код операции был возвращен для инструкции).
Делая типы операндов легко различимыми, вы можете проверить, какие операнды предоставляются, являются ли они допустимыми и какие последовательности байтов генерировать. Разделение операций на их собственные функции обеспечивает хорошую логическую структуру.
Кроме того, большинство процессоров следуют разумно логичному переводу с кода операции на операцию (чтобы облегчить жизнь разработчикам микросхем), поэтому для всех кодов операций будут очень похожие вычисления, которые позволяют, например, индексированную адресацию.
Для того, чтобы правильно создать код в ЦП, который допускает инструкции переменной длины, лучше всего делать это в два прохода.
На первом этапе не генерируйте код, просто генерируйте длины инструкций. Это позволяет назначать значения всем меткам по мере их появления. Второй проход сгенерирует код и может заполнить ссылки на эти метки, поскольку их значения известны. ignoreVars
в этом сегменте кода выше использовался для этой цели (были возвращены последовательности байтов кода, чтобы мы могли знать длину, но любые ссылки на символы только что использовали 0).