m4

Немного о сложностях написания ассемблера на m4

Не думаю, что я много буду использовать язык m4 в будущем, если вообще буду, поэтому хотел дать себе и, возможно, кому-то из читателей чуть больше контекста о том как писался на этом макропроцессоре ассемблер процессора 8080A. А то со временем всё забудется.

Для этого я чуть-чуть больше расскажу о m4 и ассемблере 8080A.

Ассемблер 8080А довольно простой. Будучи школьником, я его выучил по комментариям к кускам кода, которые печатались в журнале «Радио» — другой литературы в моём доступе не было. В нём всего несколько команд, большинство из них имеют один или два параметра.

Посмотрим, например, на команды ADD B и ADD C. Первая эквивалентна A += B на других языках, вторая — A += C. Буквами обозначаются регистры, для простоты можно считать их именованными переменными. Их мало, всего несколько штук и они имеют свои особенности. Например, в этих двух командах мы видим особенность регистра A — к нему можно прибавлять значения других регистров.

Команды ADD B и ADD C для исполнения компьютером переводятся в числа — в машинные коды. Обычно их записывают в шестандцатеричном виде, я тоже так буду делать. Данные команды сложения кодируются как числа 80 и 81 соответственно.

Теперь немного про m4. Я писал выше, что m4 — макропроцессор, если упрощать, язык макропроцессора позволяет указать как одни «слова» заменить на другие. «Слова» при этом должны начинаться с буквы или подчёркивания и содержать только буквы, цифры или знак подчёркивания. Это ограничение очень важное. Не будь его, я бы задал по одному правилу на каждую команду: ADD B → 80, ADD C → 81 и был таков.

К сожалению, так это не работает. Из-за этого ограничения, когда макропроцессор встречает ADD, он ещё не знает что будет дальше, поэтому не может выбрать на какой код заменить это слово, а когда дальше он видит B или C, ему надо помнить, что было до этого, так как с этими регистрами работает много команд. Другими словами, макропроцессору нужно запоминать контекст.

В руководствах по m4 указывается, что этот макропроцессор контекст учитывать не умеет. Так и есть, встроенных конструкций, предназначенных именно для этого нет, но можно выкрутиться менее специфичными. Дело в том, что m4 может проверять как выглядят его текущие правила замен одного слова на другое.

Поэтому мы можем завести специальное правило, куда будем записывать какая команда нам встретилась, а позже, встретив имя регистра, будет проверять чему равно наше специальное правило и производить замену, основываясь на этом.

При этом m4 не делает различий между, так сказать, текстом программы и обрабатываемым текстом, для него это одно и то же. То есть макропроцессор делает возможным метапрограммирование — позволяет менять программе свой собственный текст.

Вот как это выглядит в коде:

dnl заменяем строку ADD на определение макроса, сама строка ADD при этом пропадёт
define(`ADD', `define(`__cmd', `__s_ADD')')dnl

dnl когда встречаем B, смотрим чему равно определение макроса __cmd
define(`B', `
ifelse(
    defn(`__cmd'), `__s_ADC', 88,
    defn(`__cmd'), `__s_ADD', 80,
    …тут остальные команды…
    `')
')dnl

dnl когда встречаем C, смотрим чему равно определение макроса __cmd
define(`C', `
ifelse(
    defn(`__cmd'), `__s_ADC', 89,
    defn(`__cmd'), `__s_ADD', 81,
    …тут остальные команды…
    `')
')dnl

Чтобы дойти до этого трюка, пришлось поломать голову, но тем интереснее. Возможно он где-то уже описан, но искать информацию по m4 очень тяжело — поисковые машины охотно путают его с одноимённым процессором, а нейросети пишут какую-то пургу, не имеющую никакого отношения к реальности.

2 комментария

Ассемблер процессора i8080A на m4

Сегодня опять сычевал почти до полуночи — вечером попался кусок кода на m4 и я, пробормотав вечное «когда-нибудь надо бы его выучить», вдруг подумал — а почему бы и не сейчас? Я и так всё время откладываю, а этот вечер ничем для этой цели не хуже, чем любой другой.

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

Скриншот веб-эмулятора РАДИО-86РК: в консоли выводится «HELLO WORLD», в окне ассемблера виден код на языке ассемблера для процессора Z80, URL rk86.ru/console.html.
Строка «HELLO WORLD», выведенная программой, переведённой в машинный код моим ассемблером

Несколько слов о том что такое 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(…).

Результат можно увидеть у меня в репозитории.

Оставить комментарий