Немного о сложностях написания ассемблера на 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 или 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 очень тяжело — поисковые машины охотно путают его с одноимённым процессором, а нейросети пишут какую-то пургу, не имеющую никакого отношения к реальности.