Ассемблер процессора i8080A на m4
Сегодня опять сычевал почти до полуночи — вечером попался кусок кода на m4 и я, пробормотав вечное «когда-нибудь надо бы его выучить», вдруг подумал — а почему бы и не сейчас? Я и так всё время откладываю, а этот вечер ничем для этой цели не хуже, чем любой другой.
Чтобы разбираться было веселее, придумал себе задачу — написать на m4 ассемблер, желательно не очень сложный, чтобы занятие на вечерок не разрослось потом до недельного проекта. Взял инструкции интеловского процессора 8080A — это был мой первый ассемблер. Советской аналог процессора стоял на компьютере «Радио-86РК», с которого я когда-то начинал.

Несколько слов о том что такое m4.
Это довольно простой макропроцессор, умеющий заменять одни указанные строки на другие. Описание что на что заменить делается параметрическими макросами. В языке есть собственные макросы, но можно определить и пользовательские. Поддерживаются так же условия и математика. В продвинутых версиях m4 есть даже циклы, но их в любой версии можно эмулировать рекурсией.
Парочка примеров того как выглядит его синтаксис:
dnl Макрос, заменяющий одну строку на другую
define(`WORLD', `KAZAN')dnl
dnl Макрос, оставляющий первый свой параметр как есть, а
dnl второй складывающий со значением другого макроса,
dnl если второй параметр не указан, его значение — единица
define(`__pr', `define(`__ip', eval(__ip + ifelse($2, `', 1, $2)))$1')dnl
Чтобы реализовать задуманное, я взял список команд процессора с кодами — их там чуть меньше 250, если брать варианты с разными регистрами, и написал на Пайтоне два генератора.
Два, потому что мне нужно было как-то решить проблему расчёта адресов ссылок. Первая программа генерирует файл linker8080.m4, который этим и занимается. Адреса он на следующем этапе передаёт в виде макросов второму файлу — asm8080.m4, который делает основную работу — переводит ассемблер в машинные коды.
Сначала у меня всё было сделано в одном файле, но я быстро выяснил, что макросы распространяются сверху вниз, то есть определённые ниже макросы не влияют на текст выше. Если ссылка, для которой я подсчитал адрес, находится ниже точки вызова, я этот вызов уже не могу заменить.
Вот программа на этом ассемблере, в действии её можно посмотреть на скриншоте:
ORG(h1100)
LXI H, ADDR(TEXT)
CALL hF818
JMP hF86C
LABEL(TEXT)
BYTE(h48, h45, h4C, h4C, h4F, h20, h57, h4F, h52, h4C, h44, hD, hA, h0)
Не слишком отличается от привычного вида, но отличия всё-таки есть.
Во-первых, числа должны быть только шестнадцатеричными. Часто так и бывает, но тут запись немного непривычная — hFF вместо более привычной FFh.
Во-вторых, многие вещи, не генерирующие код, выглядят чуть иначе — адрес запуска задаётся конструкцией ORG(…), метка — через LABEL(…), а её адрес получается через ADDR(…).
В-третьих, произвольные цепочки байт задаются не директивой DB, а конструкцией BYTE(…).
Результат можно увидеть у меня в репозитории.