ФОРТ-АССЕМБЛЕР
Форт часто используют в прикладных областях, где от программ требуется высокая скорость выполнения, например, при обработке сигналов информация поступает в реальное время и компьютер должен справляться с ее обработкой. Как правило, вы существенно выигрываете в скорости, если работаете с Фортом, а не с другими языками программирования, но языку Ассемблера он все же в этом отношении уступает. (Вновь создаваемые Форт-процессоры, такие, как NOVIX NC 4000, непосредственно выполняют команды Форта высокого уровня быстрее, чем традиционные процессоры свои машинные команды. Ассемблер, описанный в данном разделе, имеет смысл только для систем, функционирующих на обычных процессорах.)
В прикладной программе почти все время выполнения приходится лишь на ее небольшую часть, а именно на так называемые внутренние циклы. Если ваша программа, написанная на Форте - языке высокого уровня, работает слишком медленно, вы можете значительно ускорить ее выполнение, переписав один или дйй внутренних цикла на языке Ассемблера.
В большинстве языков высокого уровня нет хороших средств автономного создания программ на ассемблере и соединения их с основной программой. На Форте же это обычный процесс. Определения на ассемблере Форта выглядят почти так же, как и определения высокого уровня. Если тело определения через двоеточие содержит код высокого уровня: : НОВОЕ-ИМЯ ( код высокого уровня . . .) ;
то тело ассемблерного определения включает команды на языке Ассемблера: CODE НОВОЕ-ИМЯ ( код на языке ассемблера . . .) END-CODE
(Слово завершения ассемблерного кода варьируется от системы к системе; Стандарт-83 рекомендует END-CODE.)
Слово, которое определяется посредством CODE, хранится в словаре так же, как и все остальные, и выполняется или вызывается подобно любому другому слову. Определенное соответствующим образом это слово будет выполняться со значениями из стека, поэтому вы можете передавать ему аргументы так, как если бы оно было определено через двоеточие. На самом деле, выполняя некоторое слово, вы не в состоянии установить, определено ли оно через двоеточие или через CODE (разве что по скорости выполнения).
Лучше всего писать программу на языке высокого уровня. После того как она начала правильно работать, вы можете выявить те участки, на выполнение которых тратится основное время и переписать их в машинных кодах. Повторно откомпилируйте программу, и она будет работать намного быстрее. Альтернативный способ - заранее фиксировать критичные по времени участки - не столь эффективен.
Рассмотрим создание конкретного ассемблера на примере ассемблера 8080. Очевидно, что для каждого процессора должен существовать свой ассемблер и что ассемблер 8080 подходит только для данного процессора. Если вы введете в ваш компьютер приведенный здесь пример на ассемблере, то получите сгенерированный машинный код для 8080, что, собственно, вам и требуется. Но попытавшись полученный код выполнить, вы потерпите неудачу. Наш пример показывает, как легко писать на Форт-ассемблере, объясняет основные принципы разработки ассемблера и демонстрирует мощь определяющих слов Форта.
Начнем с определяющего слова CODE. Его назначение - создание заголовка словарной статьи, которая при выполнении передаст управление по адресу, содержащему машинный код. Выполнить это проще, чем понять. Вспомните (см. гл. 9), что все определения снабжены полем кода, которое указывает машинный код. В определении CODE такой указатель должен указывать поле параметров данного определения:
Таким образом, простое определение слова CODE может иметь вид: : CODE CREATE HERE HERE 2- ! ;
Теперь нам нужен набор слов, позволяющий осуществлять трансляцию машинных команд в словарь. Для начала выберем несложное слово. Команда процессора 8080 СМА вычисляет дополнение содержимого регистра А. Код этой операции в двоичной системе счисления выглядит так: 00101111. Чтобы транслировать команду, введем следующее определение:HEX : CMA 2F C, ;
Еще одна простая команда XCHG обеспечивает обмен содержимым между парами регистров D-Е и Н-L. Код такой операции: 11101011. Далее мы можем ввести определение: : XCHG ЕВ С, ;
Подведем предварительные итоги.
Мы задали себе синтаксис написания определений машинных команд и предыдущими действиями создали средства их спецификации. Теперь можно ввести следующий текст:CODE ТЕСТ СМА ХСНG . . .
Получено слово ТЕСТ, выполняющее машинные команды СМА и XCHG. Вы можете для проверки этого слова воспользоваться словом DUMP (но ни в коем случае не инициируйте слово ТЕСТ!).
Как заканчивается CODE-определение, мы покажем позднее, а пока вернемся к определению машинных команд. У нас уже определены две команды, состоящие из восьмиразрядного кода операции. Процессор 8080 имеет довольно много команд такого типа. Поэтому нам необходимо слово для определения всех подобных команд (назовем их командами типа!) 1MI. : 1MI ( код-операции - ) CREATE С, DOES> С@ С, ;
Определим с помощью введенного слова следующие команды (первые два определения по-новому создают уже имеющиеся у нас команды):HEX 2F 1MI СМA ЕВ 1MI XCHG 00 1MI NOP 76 1MI HLT F3 1MI DI FB 1MI EI 07 IMI RLC 0F 1MI RRC 17 1MI RAL 1F IMI RAR Е9 1MI PCHL F9 1MI SPHL E3 1MI XTHL 27 1MI DAA 37 1MI STC 3F 1MI CMC C0 1MI RNZ C8 1MI RZ D0 1MI RNC D8 1MI RC Е0 1MI RPO E8 1MI RPE F0 1MI RP F8 1MI RM C9 1MI RET
Определяющее слово 1MI создает семейство команд, каждую из которых отличает уникальный код операции, но при компиляции все они ведут себя одинаково: их код заносится в словарь. Вновь образованное определение СМА функционально почти не отличается от прежнего определения через двоеточие. Единственное отличие состоит в том, что слово С, заносящее код операции в словарь, находится в части DOES> слова 1MI, а сам код (2F) - в поле параметров слова СМА.
Мы уже определили большую группу команд процессора 8080. Но остальные его команды не так просты. Например, команда ADD дополнительно вносит содержимое заданного регистра в регистр А (сумматор). Для того чтобы на обычном ассемблере 8080 добавить
содержимое регистра В к содержимому регистра А, нужно ввести ADD В
Код операции ADD в двоичной системе имеет вид 10000SSS, где SSS - три бита, используемые для указания задаваемого регистра (S означает источник).
Регистр В задается как 000, отсюда "ADD В" в двоичном коде будет выглядеть следующим образом: 10000000.
Аналогично если регистр L задается как 101 (S), то выражение "ADD L" превратится в 10000101. Иными словами, нужный код операции получается при выполнении команды OR над двоичным значением 10000000 (шестнадцатиричное 80) и числом, обозначающим регистр. Определим операцию ADD так:: ADD ( регистр# - ) 80 OR С, ;
Самый простой способ занесения номера нужного регистра в вершину стека - задать номера регистров в виде констант: 0 CONSTANT В 1 CONSTANT С 2 CONSTANT D 3 CONSTANT E 4 CONSTANT H 5 CONSTANT L 7 CONSTANT A
(Здесь перечислены все регистры, которые можно использовать в команде ADD.) Теперь для занесения в словарь кода операции сложения значений регистра В и сумматора можно написать: В ADD
Постфиксная запись ненамного усложняет дело, но зато ассемблер становится проще и сохраняется свойство расширяемости, присущее Форту (с помощью макроподстановки).
В некоторых командах, аналогичных ADD, значение регистра задается в трех младших битах. Поэтому имеет смысл для данного класса команд специфицировать свое определяющее слово:: 2MI CREATE C, DOES> ( регистр# - ) С@ OR С, ;
С помощью этого слова можно ввести следующие определения:80 2MI ADD 88 2MI ADC 90 2MI SUB 98 2MI SBB А0 2MI ANA AB 2MI XRA В0 2MI ORA B8 2MI CMP
(2MI функционирует аналогично 1MI, т. е. запоминает уникальный код операции определяемой команды (ребенка) в поле параметров последней. Отличие же заключается в том, что 2MI заставляет команду-ребенка при выполнении логически складывать посредством OR код операции ребенка с номером регистра из стека.) Существует еще один класс машинных команд, содержащих номера регистров в коде операции, но в другом месте. Например, код команды 1NR (приращение) имеет вид OODDD100 (шестнадцатиричное число 04), где DDD - регистр, подлежащий приращению (D означает «получатель»). Мы можем воспользоваться константами, обозначающими номера регистров, но при этом необходимо осуществить сдвиг на три бита влево, прежде чем команда OR сложит их с кодом операции (сдвиг на три позиции влево эквивалентен умножению на восемь):: INR ( регистр# -- ) 8 * 04 OR С, ;
Как и в предыдущем случае, введем определяющее слово:: 3MI CREATE С, DOES> ( регистр# -- ) С@ SWAP 8 * OR С, ;
04 3МI INR
Теперь выражение "С INR" занесет в словарь код операции: 00001100.
С помощью слова 3MI можно специфицировать еще один класс команд, в чем вы убедитесь, посмотрев листинг, приведенный в конце раздела.
Для создания команд остальных типов нам достаточно ввести всего два определяющих слова - 4MI и 5М1. Первое применяется для образования кодов тех операций, которые требуют дополнительно восьмиразрядного литерала, например ADI (непосредственное сложение с А). Второе слово определяет коды операций, требующих дополнительно 16-разрядного литерала. В качестве примера можно привести команды CALL, JMP и подобные им. Команды MOV, MVI и LX1 уникальны и поэтому специфицируются индивидуально посредством двоеточия без использования определяющего слова.
Изучая листинг, обратите внимание на то, что в него включены операторы управления, такие, как IF, ELSE,THEN,BEGIN, UNTIL, WHILE и REPEATE. Это не совсем те слова, с определениями которых вы уже познакомились ранее (где передача управления компилируется посредством высокоуровневых слов Форта), а их версии, созданные только для ассемблера, где передача управления и разрешение адресов, как и в традиционном ассемблере, осуществляются на уровне машинных команд. Однако они обеспечивают вам возможность программирования с использованием формата структур высокого уровня.
Но можно ли компилировать в словарь различные варианты слов IF, THEN и т. д., они ведь в нем смешаются? Конечно, так как команды ассемблера хранятся в контекстном словаре ASSEMBLER, а не в словаре FORTH. Определение CODE в нашем листинге инициирует слово ASSEMBLER, что делает этот контекстный словарь текущим всякий раз, когда мы начинаем CODE-определение.
Интересной особенностью Форт-ассемблера является и его расширяемость. Если в вашей программе имеются повторяющиеся фрагменты, то вы можете вместо них использовать макрокоманды. Ниже приводится пример макрокоманды, которая осуществляет «циклический сдвиг содержимого регистра А влево» и затем «добавляет содержимое регистра В»: : SHIFT+ RLC В ADD ;
Заметьте, что, появившись внутри ассемблерного определения, слово SHIFT+ помещает в словарь две команды, составляющие определение этого слова так, как если бы вместо него были введены сами команды: RLC В ADD
Адрес слова SHIFT+ не компилируется, а само оно при выполнении его кода не вызывается в качестве подпрограммы. Использование макросредств во время выполнения не приводит к каким-либо накладным расходам, поскольку машинные команды после макроподстановки в точности такие же, как и без нее.
Слово NEXT представляет собой одну из макрокоманд, написанных на языке Ассемблера. В нашей системе она определена так: : NEXT (NEXT) JMP ;
Иными словами, NEXT - это машинная команда, передающая управление по адресу, который оставляет в вершине стека слово (NEXT). По данному адресу расположен код адресного интерпретатора (о котором речь шла в гл. 9). Адресный интерпретатор является ядром Форта и выполняет поочередно все адреса в скомпилированном Форт-определении. Каждое определение через CODE должно заканчиваться инициированием адресного интерпретатора. Следовательно, любое ассемблерное определение должно завершаться словом NEXT. В нашем ассемблере определения также должны иметь в конце слово END-CODE, которое дополнительно восстанавливает контекст.
Ниже приводятся два примера, где используются команды описанного здесь ассемблера: HEX CODE X ( n -- n') \ Меняются местами старший и младший байты n Н POP L A MOV H L MOV A H MOV H PUSH NEXT END-CODE
(Мы пересылаем п из стека в пару регистров HL, регистр L (младшие байты) в регистр А, регистр Н (старшие байты) в L, а А в Н, помешаем содержимое пары регистров HL в стек, передаем управление NEXT).CODE BP ( a # -- ) \ Перевод из нижнего регистра в верхний D POP H POP BEGIN D A MOV Е ORA 0= NOT WHILE M A MOV 60 CPI CS NOT IF 20 SUI A M MOV THEN D DCX H INX REPEAT NEXT END-CODE
(Пересылаем счетчик в пару регистров DE, а адрес - в пару регистров HL, начинаем цикл, проверяем, выполняя команду OR над содержимым регистров D и Е, не равно ли значение счетчика нулю.
Пока его значение не равно нулю, перемещаем символ из памяти, на которую ссылается указатель, в сумматор. Если код обрабатываемого символа больше 60 (строчная «а» и выше), вычитаем десятичное число 32, преобразуя символ в прописной, и записываем в память. Уменьшаем счетчик и увеличиваем адрес. Повторяем цикл. Передаем управление NEXT).
Преимущество работы на ассемблере такого вида заключается в том, что вы во время ассемблирования «находитесь в Форте». Если вам необходимо идентифицировать некоторое устройство с помощью имени, а не числа, вы можете определить его как обычную константу и присвоить ей имя внутри ассемблерного определения. Можно воспользоваться определением через двоеточие как макрокомандой или даже обратиться к переменной, поскольку она помещает в вершину стека свой адрес и поэтому может быть задействована в команде «непосредственной загрузки». Применение машинных команд раскрывает перед вами всю мощь языка Форт.
В основу описанного здесь ассемблера положен ассемблер 8080, разработанный Дж. Кассэди. Мы внесли в него изменения в соответствии со Стандартом-83 и для простоты изучения убрали некоторые зависимые от системы фрагменты. С оригиналом вы можете познакомиться в [1 ]. Ассемблер для других процессоров описан в [2], [3], [4].\ Ассемблер 8080 \ учебная версия ассемблера 8080,основанная на фигФорте; \ разработана Джоном Кэсседи HEX VOCABULARY ASSEMBLER : CODE CREATE HERE HERE 2- ! ASSEMBLER ; ASSEMBLER DEFINITIONS : END-CODE CURRENT @ CONTEXT ! ; 0 CONSTANT В 1 CONSTANT С 2 CONSTANT D 3 CONSTANT E 4 CONSTANT H 5 CONSTANT L 6 CONSTANT PSW 6 CONSTANT M
6 CONSTANT SP 7 CONSTANT A : 1MI CREATE C, DOES> C@ C, ; : 2MI CREATE C, DOES> C@ OR C, ; : 3MI CREATE C, DOES> С@ SWAP 8 * OR C, ; : 4MI CREATE C, DOES> С@ С, С, ; : 5MI CREATE С, DOES> С@ С, , ;
\ Ассемблер 8080 HEX 00 1MI NOP 76 1MI HLT F3 1MI DI FB 1MI ED 07 1MI RLC 0F 1MI RRC 17 1MI RAL 1F 1MI RAR E9 1MI PCHL F9 1MI SPHL E3 1MI XTHL EB 1MI XCHG 27 1MI DAA 2F 1MI CMA 37 1MI STC 3F 1MI CMC 80 2MI ADD 88 2MI ADC 90 2MI SUB 98 2MI SВВ А0 2MI ANA A8 2MI XRA B0 2MI ORA B8 2MI CMP B9 3MI DAD C1 3MI POP C3 3MI PUSH B2 3MI STAX 0А 3MI LDAX 04 3MI INR 05 3MI DCR 03 3MI INX 0B 3MI DCX C7 3MI RST D3 4MI OUT DB 4MI SBI E6 4MI ANI ЕЕ 4MI XRI F6 4MI ORI FE 4MI CPI 22 5MI SHLD 2A 5MI LHLD 32 5MI STA 3А 5МI LDA CD 5MI CALL
\ Ассемблер 8880 HEX C9 1MI RET C3 5MI JMP C2 CONSTANT 0= D2 CONSTANT CS E2 CONSTANT PE F2 CONSTANT 0< : NOT 8 OR ; : MOV 8 * 40 + + C, ; : MVI 8 * 6 + C, C, ; : LXI 8 * 1+ C, , ; : THEN HERE SWAP ! ; : IF C, HERE 0, ; : ELSE C3 IF SWAP THEN ; : BEGIN HERE ; : UNTIL C, , ; : WHILE IF ; : REPEAT SWAP JMP THEN ; : NEXT (NEXT) JMP ;