Программирование на форт-ассемблере
Как описано в гл. 3, память ЭВМ представляет собой серию ключей, которые могут использоваться для представления " 1" и "О" в двоичных числах. Программы и данные запоминаются в двоичной форме, таким образом представляя числа, текст или машинные коды. В гл. 3 рассмотрены различные способы представления чисел, а в гл. 9 обсуждается запоминание и манипулирование строками текста. В этой главе мы обсудим, как писать слова, чтобы исполнять программы в машинных кодах. Машинный язык (называемый также машинным кодом) - это набор двоичных чисел, которые ЭВМ может интерпретировать как инструкции и непосредственно исполнять. Это наиболее фундаментальный способ программирования ЭВМ. Преимущество использования программы в машинных кодах, в противоположность программе на языке высокого уровня, заключается в том, что это дает возможность полностью управлять всеми аспектами работы ЭВМ и позволяет написать программу, которая будет исполняться так быстро, как только может работать машина. Наиболее общепринятый способ написания программ в машинных кодах заключается в использовании программы, называемой ассемблером.. Каждая машинная инструкция имеет имя, называемое мнемоническим, чтобы облегчить ее запоминание. "Типичный" (не форт) ассемблер воспринимает файл мнемоники, написанный с использованием редактора, и преобразует мнемонику в машинные коды, которые либо запоминаются в другом файле, либо загружаются в память, чтобы быть исполненными. После нескольких последовательных, требующих времени и памяти шагов (таких как редактирование связей и загрузка) программа в машинных кодах может быть исполнена либо сама, либо совместно с другой программой, написанной на другом языке. Форт-ассемблер использовать много проще, так как не требуется редактирования связей и загрузки, а мнемоника может быть использована как част), описаний слов Форта в Форт-программе. Мнемоника форт-ассемблера - это слова, которые компилируют машинные коды и слово, описанное с помощью слова CODE.
За подобными справками вам следует обращаться к руководству по вашему процессору. В 8-битовом процессоре 8 бит (байт) рассматриваются в качестве команд или данных. Хотя существует только 256 (2^8) возможных комбинаций из 8 битов, реализуемо более 256 типов команд, так как некоторые байты будут заставлять ЦП воспринимать еще один или несколько байтов для формирования команды. Например, 8-битовый микропроцессор Z-80 имеет более 700 типов команд, содержащих от одного до 4 байтов каждая. Шестнадцатибитовые микропроцессоры могут использовать 8- или 16-битовые команды, 8- или 16-битовые регистры и могут передавать данные по 16 или 32 битов за такт в зависимости от модели. Конечно, микроЭВМ имеют даже больше вариаций. Мы приведем примеры для микропроцессора Z-80, так как он используется в TRS-80 и в семействе СР/М машин, а также для 8086 и 8088, так как они применяются в IBM PC и семействе оборудования, использующем MS-DOS. Самый простой способ рассмотрения машинных программ на вашей ЭВМ - это пропечатка поля параметров примитивов. Используя описание DUMP из гл. 14, ' SWAP 1 DUMP в Форт-79 или ' SWAP >BODY 1 DUMP в Форт-83, отобразим машинную программу, которая меняет местами два верхних элемента в стеке. Для Форта Z-80, который использует регистр указателя стека параметров Форта, эта программа, вероятно, отпечатает (адр) D1 Е1 D5 Е5 FD E9 хх хх хх xх хх хх хх хх хх хх где мы использовали "хх" для обозначения байтов, величина которых не определена. (Это пример для MMSFORTH на TRS80.) Но командные байты ЭВМ (известные так же как коды операций) ничего не значат, если у вас нет книги по программированию на ассемблере для Z-80, с помощью которой можно декодировать их. Если бы вы рассмотрели каждый байт, то поняли бы, что D1 = POP DE Е1 = POP HL D5 = PUSH DE Е5 = PUSH HL FD E9 = JP (IY) но это вряд ли прояснит, что здесь происходит, если вы не знаете ассемблера Z-80. Различные команды (POP DE и т.д.) являются мнемоникой, о которой мы говорили. Если вы знаете смысл этой мнемоники, вы можете оценить, как работает слово SWAP.
D1 = POP DE Перенести число из стека в регистр DE Е1 = POP HL Перенести число из стека в регистр HL D5 = PUSH DE Перенести число из регистра DE в стек E5 = PUSH HL Перенести число из регистра HL в стек FD E9 = JP (IY) Передать управление по адресу, лежащему в регистре IY Программисту, работающему на Форте, довольно легко понять команды PUSH и POP, так как "аппаратный стек", реализованный в Z-80 с помощью SP-регистра (указателя стека), является в большинстве версий Форта как раз тем самым стеком, который непосредственно использует Форт. Применены здесь два регистра общего назначения с именами DE и HL, а также команда записи чисел в стек, названная PUSH, и извлечения кода из стека, называемая POP. Машинная программа для SWAP просто говорит ЦП Z-80 убрать два числа из стека и записать их в два регистра ЦП, а затем положить их в стек в обратном порядке. Это показывает, насколько легко переслать любое число кодов в регистры микропроцессора и обратно, используя стек. Команда JP (IY) (которая может быть другой в вашем Форте, даже если у вас применена система Z-80) заставляет Z-80 передать управление по адресу, записанному в регистре IY. Это инструкция, названная нами NEXT в гл. 15. Инструкция NEXT используется для завершения исполнения Форт-программы в машинных кодах (примитива) и передачи управления внутреннему интерпретатору Форта. Коды операций для NEXT в вашем Форте могут быть другими, даже если вы используете Z-80. NEXT в действительности слово MMSFORTH и не является стандартным словом, но почти все версии Форта содержат его эквивалент. Чтобы проиллюстрировать, как одно и то же задание реализуется на разных микропроцессорах (так вы не будете разочарованы, если не имеете оборудования, которое мы обсуждаем), ниже представлены программы для SWAP в двух различных Фортах для 16-битовой IBM PC, базирующейся на микропроцессоре 8088. MMSFORTH 5A = POP DX 58 = POP AX 52 = PUSH DX 50 = PUSH AX AD = LODS AX, SI Загрузка в регистр AX содержимого регистра SI 93 = XCHG AX, BX Пересылка содержимого ВХ в AX и наоборот FF 27 = JMP (ВХ) Передача управления по адресу, лежащему в регистре ВХ MVPFORTH 5В = POP ВХ 58 = POP AX 53 = PUSH ВХ 50 = PUSH AX AD = LODS AX, SI Загрузка в регистр AX содержимого регистра SI AB D8 = MOV ВХ, AX Пересылка содержимого AX в регистр ВХ FF 27 = JMP (ВХ) Передача управления по адресу, лежащему в регистре ВХ
Вы можете видеть, что так же, как в версии MMSFORTH на Z-80, оба эти описания SWAP извлекают и укладывают коды из стека и в стек с использованием регистров. MMSFORTH применяет AX- и DX-регистры, a MVPFORTH - AX и BX. Следующие три байта в MMSFORTH и следующие четыре в MVPFORTH являются двумя версиями NEXT. Обе загружают содержимое регистра SI в ВХ-регистр и затем для продолжения работы осуществляют передачу управления по адресу, хранящемуся в ВХ., В обеих версиях Форта SI-регистр используется для хранения указателя инструкций. который мы в гл. 15 назвали IP. Существуют ли какие-то причины для различий между MMSFORTH и MVPFORTH? Версия MVPFORTH немного быстрее, в то время как NEXT MMSFORTH требует на один байт меньше памяти; различие тривиально. (Но быстродействие очень важно в определении слова NEXT, поскольку оно завершает все примитивы.) Версия SWAP MVPFORTH может точно так же использоваться в MMSFORTH, и наоборот, с точно таким же результатом. Существует обычно несколько различных путей реализации одной и той же функции в машинных кодах. Прежде чем создавать слово, которое выполняет машинную программу, давайте рассмотрим откомпилированную форму программы-примитива. Поле программы слова-примитива указывает на его поле параметров. Как мы видели в гл. 15, это придает примитиву следующую форму: Поле Имени Связи Программы Параметров Адрес NFA LFA CFA PFA Содержимое Имя Адрес PFA (Машинная программа NEXT) , +
Для того чтобы вы могли написать собственный примитив, вы должны найти машинную программу NEXT или ее эквивалент в вашей системе. Это можно сделать с помощью пропечатки некоторых примитивов, чтобы узнать, как они завершаются. Если вы располагаете Форт-ассемблером, то он, вероятно, снабжен словом, подобным NEXT, которое выполняет эту функцию. Теперь давайте опишем новую версию SWAP с именем MYSWAP. Начнем с формирования заголовка MYSWAP, а затем заменим содержимое CFA так, чтобы там лежал адрес поля параметров MYSWAP. Чтобы сформировать заголовок MYSWAP напишем CREATE MYSWAP HERE DUP 2- ! Его PFA определяется с помощью слова HERE и затем эта величина записывается в поле программы MYSWAP, которое занимает два байта перед PFA. (Эта методика может не работать для Форта, который записывает слова нетрадиционным способом.) Раз вы поместили адрес поля параметров MYSWAP в его CFA, компилируйте далее машинную программу просто байт за байтом (убедитесь, что вы работаете с шестнадцатеричной системой): D1 С, Е1 С, 05 С, Е5 С, F0 С, Е9 С, для MMSFORTH на Z-80 (или другие байты, используемые в описании SWAP в вашей системе).
Не забудьте включить машинную программу, которая осуществляет возврат к Форту. В MMSFORTH вы могли бы использовать слово NEXT вместо байтов FD и Е9. Вы можете, конечно, применить эквивалентное слово из вашей версии. Если MYSWAP делает то же, что и SWAP, то открыт путь для описания аналогичным способом остальных слов-примитивов. Если MYSWAP не работает (что маловероятно). распечатайте ваше описание, чтобы убедиться, что содержимое CFA указывает на PFA и что машинная программа соответствует откомпилированному тексту SWAP, и затем испытайте. описание еще раз. Если вы собираетесь описать более чем пару слов-примитивов, вы, возможно, захотите для формирования заголовков описать : CODEHEAD CREATE HERE DUP 2- ! ; Форма обращения CODEHEAD MYSWAP (машинная программа...NEXT) CODEHEAD работает во многом аналогично слову CODE из ASSEMBLER, описанному в следующем разделе.
Если вы намереваетесь написать много слов-примитивов, используя эту методику или ассемблер, имеется пара вопросов, которые вы должны решить. Первый: использует ли ваш Форт аппаратный указатель стека (указатель стека, с которым работает микропроцессор) для хранения адреса стека параметров? Большинство версий Форта используют. Это может быть определено путем пропечатки SWAP, DUP или OVER, чтобы выяснить, как они определены. Если они используют PUSH и (или) РОР- команды, как определено для вашего микропроцессора, тогда Форт-стек и аппаратный стек совпадают. Второй: какой регистр используется Фортом в качестве указателя инструкции (который мы в гл. 15 назвали IP)? Пропечатайте машинную программу для EXIT, чтобы выяснить, в какой регистр заносится содержимое стека возвратов. Важно сохранить содержимое этого регистра, если вы должны использовать его для своих целей, и принять меры для того, чтобы восстановить его исходное значение, когда машинная программа будет завершена. Указатель слов Форта (WP) может быть, вероятно, изменен вашей программой-примитивом, так как он, вернее всего, будет изменен сразу после NEXT, когда исполнение вашего слова будет завершено.
Полезно также знать, какой регистр используется для хранения WP, так как он будет всегда содержать CFA или PFA вашей программы-примитива, когда к ней произошло обращение. Заметьте, что существует много вариантов написания Форта, поэтому использование регистров и стеков должно быть известно для вашей системы, прежде чем вы сможете уверенно программировать слова-примитивы. К сожалению, некоторые поставщики Форта не сообщают вам об использовании внутренних регистров в документации. Таким образом, написание Форт-программ в машинных кодах содержит некоторое число досадных деталей, за которыми вы должны следить. Несмотря на это, машинные программы могут быть введены в Форт-программу весьма легко - вам нужно только внимательно следить за применением в Форте определенных регистров, особенно того, который используется в качестве указателя инструкции (IР). Если для вашей программы-примитива нужны все регистры, вы можете, конечно, запомнить значение IP в стеке (или в переменной) вплоть до момента занесения его в соответствующий регистр перед завершением работы вашей программы. Мы обсудим некоторые другие вопросы, которые требуют внимания, в следующем разделе. Даже с учетом этих досадных обстоятельств интерфейс между языком высокого уровня и машинной, программой намного проще реализовать в Форте, чем в любом другом языке.
Упражнения
Ответы для упражнений этой главы приведены для микропроцессоров Z-80 и 8088, но вы, вероятно, захотите выполнить их также на вашей собственной ЭВМ. если у вас другой ЦП. В этом случае проверьте результат путем сравнения вашего решения с пропечаткой поля параметров описания в вашем Форте там, где это возможно. 1. Предложите программы-примитивы и мнемонику для Z-80 н 8088 для стековых операндов. В этих упражнениях используйте только PUSH, POP и NEXT. a) DROP; б) DUP; в) OVER; г) ROT. 2. Версия F83 Лаксена и Перри имеет несколько нестандартных слов для работы со стеком, которые вы, возможно, захотите использовать. Одно из них, TUCK, действие которого заключается в "подсовывании" копии верхнего элемента стека под второй элемент сверху (n1 n2 - n2 n1 n2), является оператором, обратным по отношению к OVER.
Опишите TUCK в виде программы-примитива для Z-80 и 8088, используя методику, описанную для MYSWAP. 3. NIP является еще одним нестандартным словом F83. используемым для удаления второго элемента из стека (п1 п2 - п2). Опишите его для Z-80 и 8088. 4. В Z-80 команда EX [SP], HL меняет местами верхний элемент стека (содержимое адреса, на который указывает регистр SP) и содержимое регистра HL. Опишите ROT. Как может быть сделано более эффективным описанное выше SWAP (использовать меньшее число байтов или обеспечить исполнение за меньшее число циклов), если применить эту команду совместно с PUSH и POP? 5. -ROT является оператором, инверсным по отношению к ROT (или тем же, что ROT ROT), и заносит верхний элемент стека в третью позицию сверху (n1 n2 n3 - n3 n1 2). Опишите его для Z-80 и 8088. 6. Как может быть сделано более эффективным ваше описание DROP путем оперирования адресом в указателе стека, не используя POP?
Форт-ассемблеры
Теперь ясно, что машинные программы могут быть встроены в слова Форта методом, использованным при описании MYSWAP. Итак, в чем заключается задача ассемблера? Просто в том, чтобы сделать это более легким. Форт-ассемблер состоит из мнемоники, которая компилирует машинные команды и применение которой намного проще, чем компиляция байтов с помощью С,. Ассемблер может также включать в себя другие слова, которые не являются мнемокодами, а служат для организации циклов и условных переходов.
Но существуют только четыре слова ассемблера, которые специфированы стандартами: ASSEMBLER, имя словаря ассемблера (смотри гл. 14); CODE, которое открывает описание слова в ассемблере и является аналогом :; END- CODE, которое подобно ; завершает описание, и ;CODE, которое действует по аналогии с DOES>, позволяя использовать мнемонику ассемблера для определения поведения производных слов, полученных с помощью слов-описателей, на фазе исполнения (смотри гл. 10). Причина для ограниченности списка стандартных слов заключается в том, что коды операций и соответствующая им мнемоника варьируются от процессора к процессору.
Другие слова ассемблера, например позволяющие организовать циклы и ветвление программы, сильно варьируются от версии к версии и не стандартизованы. Это делает трудным дать полное и общее описание ассемблера. Мы опишем некоторые аспекты MMSFORTH ассемблеров Z-80 и 8088, но мы рассчитываем на вас в случае использования этого описания для других процессоров и версий Форта. В зависимости от версии Форт-ассемблер может быть либо полным (способным компилировать все коды операций данного микропроцессора), либо частичным (включающим только наиболее часто используемые функции). В последнем случае коды операций, не генерируемые ассемблером, могут быть откомпилированы непосредственно с помощью С, точно так же, как выше компилировалось слово MYSWAP (пример приведен ниже).
Форт-ассемблер описан как словарь с именем ASSEMBLER и, как и в случае любых других контекстных словарей; команда ASSEMBLER DEFINITIONS используется, чтобы добавить новые ассемблерные слова в словарь ASSEMBLER. Имея отдельный словарь ASSEMBLER, можно использовать в ассемблере любые имена (например, слова мнемоники и условных переходов), не боясь конфликтов со словами в Форте или другом словаре.
Словом, открывающим ассемблерное описание, является CODE. Оно формирует заголовок слова-примитива, делает словарь ASSEMBLER контекстным, записывает в CFA нового слова адрес его PFA и машинную программу, которая там должна храниться. Его описание очень похоже на слова, которые мы использовали для формирования заголовка MYSWAP. : CODE CREATE HERE DUP 2- ! [COMPILE] ASSEMBLER ; где [COMPILE] необходимо в случае, если в вашем Форте имя словаря является словом немедленного исполнения.
Вот пример описания MYSWAP с использованием MMSFORTH на процессоре 8088 IBM PC. Описание для любой версии Форта на микропроцессорах 8088 или 8086 будут сходными: CODE MYSWAP ( n1 n2 - n2 n1) DX POP AX POP DX PUSH AX PUSH NEXT END-CODE
Запомните, что вам нужно использовать что-то еще вместо NEXT или, может быть, NEXT будет частью вашего Форт-описания END-CODE.
Применение мнемоники здесь очевидно. Заметьте, если вы используете обычные ассемблеры, то при записи слов применяется типичная для Форта нотация. Вместо записи POP DX, для того чтобы перенести верхний код из стека в DX-регистр, Форт использует DX POP. Позднее вы увидите почему.
Если бы вы пропечатали описание MYSWAP, то вы бы увидели, что оно идентично SWAP. Вы уже знаете, что NEXT используется для передачи управления внутреннему интерпретатору. Тогда что же делает END- CODE? В некоторых ассемблерах END-CODE может выполнять функцию NEXT. В других, таких как MMSFORTH, единственная функция END-CODE - это сделать контекстным тот словарь, который им был до начала работы CODE. Стандарты требуют, чтобы END-CODE сделал находимым в словаре имя слова, созданного CODE- (Если при компиляции выявлена ошибка, то END-CODE этого не сделает, предотвращая тем самым узнавание ошибочного слова.) END-CODE осуществляет это обычно путем установления бита-метки в соответствующее состояние (смотри гл. 14), Некоторые версии Форта, такие как MVPFORTH, используют также ;С в качества синонима END-CODE.
В описании MYSWAP мнемоника скомпилировала соответствующий объектный код в поле параметров слова. Таким образом DX POP заносит код 5A в качестве первого байта в поле параметров. Вы можете сформировать тот же самый объектный код в MMSFORTH (и во многие другие версии Форта), если вы введете CREATE MYSWAP HERE DUP 2- ! ASSEMBLER DX POP AX POP DX PUSH AX PUSH NEXT FORTH Вы можете также использовать CREATE MYSWAP HERE DUP 2- ! ASSEMBLER DX POP AX POP 52 С, 50 С, NEXT FORTH или CODE MYSWAP DX POP AX POP 52 С, 50 С, NEXT END-CODE Теперь вы можете увидеть, как можно ввести объектный код в описание CODE, даже если у вас нет полного набора мнемоники.
Могут существовать и другие способы завершения описаний типа CODE. В MMSFORTH следующее описание MYSWAP является идентичным по своему поведению нашему первому описанию: CODE MYSWAP DX POP AX POP PSH2 END-CODE PSH2 эквивалентно следующему: DX PUSH AX PUSH NEXT
Если у вас нет PSH2, которое является удобным словом, так как часто нужно засылать содержимое этих двух регистров в стек, вы можете описать его: ASSEMBLER DEFINITIONS : PSH2 DX PUSH AX PUSH NEXT ; FORTH Это должно подсказать вам мысль, как можно добавлять к ассемблеру новую мнемонику и новые слова.
Давайте рассмотрим еще одно описание, которое представляет собой еще один пример и послужит расширению вашего опыта в этой области: CODE + AX POP DX POP DX AX ADD AX PUSH NEXT END-CODE
При операции + два числа извлекаются из стека и засылаются в АХ- и DX-регистры. Слово ADD складывает содержимое АХ- и DX-регистров (результат заносится в АХ). Наконец сумма из АХ засылается в стек. Имеется, конечно, еще много мнемонических кодов, которые мы здесь используем. Как мы сказали раньше, вам следует изучить книгу по программированию на ассемблере, чтобы вполне освоить Форт-ассемблер.
Действие слова ;CODE сопоставимо с DOES>. Подобно DOES>, ;CODE отмечает начало исполняемой части программы слова-описателя, но рабочая программа производного слова в этом случае будет написана в машинных кодах с использованием мнемоники ассемблера или С,. Таким образом, описания : FORTH-CARRAY CREATE 1+ ALLOT DOES> + ; и : CODE-CARRAY-CREATE 1+ ALLOT ;CODE HL POP DE HL ADD HL PUSH NEXT END-CODE эквивалентны в ассемблере для Z-80, но слова, сформированные CODE-CARRAY, будут исполняться намного быстрее. На IBM PC в MMSFORTH 100.000 итераций описания DOES> требуют 7,2 с, в то время как эквивалентные им описания ;CODE выполнят эту работы за 2,3 с.
В отличие от слова DOES>, которое засылает в стек PFA производного слова (и используется производными словами FORTH-CARRAY для вычисления адреса элемента массива), ;CODE требует, чтобы адрес лежал в ячейке, следующей за ним. Адрес PFA или жестко связанного с ним адреса содержится в регистре, который используется в качестве указателя слов (WP). Предшествующее описание CODE-CARRAY предполагает, что WP хранится в регистре DE, как это и есть в MMSFORTH для TRS-80.
В MMSFORTH для IBM PC WP содержится в регистре ВХ и указывает на CFA (на 2 меньше, чем PFA); описание имеет вид : 8088-CODE-CARRAY CREATE 1+ ALLOT ;CODE АХ POP ВХ АХ ADD 2 # АХ ADD AX PUSH NEXT END-CODE Вы должны будете определить, как найти и использовать WP в вашей собственной версии Форта.
Упражнения
1. Сформируйте CODE-описания для: a) DUP б) OVER в) ROT г) 2DUP д) TUCK e) NIP ж) -ROT, используя мнемонику Z-80 и 8088. 2. Слово MMSFORTH-ассемблера PSH эквивалентно PSH2, за исключением того, что оно засылает в стек только содержимое регистра AХ. Опишите PSH. 3. Опишите слово PSH3, которое должно работать как PSH2, но заносить в стек содержимое регистров ВХ, DX и АХ в указанном порядке. 4. Опишите слово ;С так. чтобы оно выполняло NEXT END-CODE 5. Опишите слово типа CODE с именем @REGS, которое заносит в стек 8088 содержимое регистров Dl. SI, SP, DX, СХ, ВХ и АХ в указанном порядке. Теперь опишите слово типа двоеточие.REGS, которое использует @REGS и отображает содержимое регистров микропроцессора 8088. 6. Мнемокод микропроцессора 8088 SUB вычитает содержимое одного регистра из содержимого другого. Если SUB заменит ADD а описании слова +. приведенном выше, содержимое регистра DX будет вычитаться из содержимого регистра АХ (результат останется в АХ). Опишите слово -. 7. Используя ;CODE, опишите следующие слова: a) ARRAY б) DARRAY в) CONSTANT г) 2CONSTANT
Как работает ассемблер
Нам следует разобраться с тем, как работает ассемблер, с целью знакомства с техникой программирования, а также и потому, что вы можете захотеть расширить возможности и список мнемокодов в вашем ассемблере. Проще всего описать слово ассемблера NEXT. На TRS-80 в MMSFORTH это описание выглядит как ASSEMBLER DEFINITIONS : NEXT ( --) FD С, Е9 С, ; FORTH в то время как на IBM PC ASSEMBLER DEFINITIONS : NEXT ( -) AD С, 93 С, FF С, 27 С, ; FORTH Вам следует понимать, как Это работает. Конечно, в вашей версии Форта это может быть и по-другому.
Мнемоника ассемблера немного сложнее, но прямолинейнее.
Как мы видели, мнемоника - это просто слова, которые записывают одну или более машинных команд в тело слова CODE. Некоторые мнемокоды работают сами по себе, другие требуют аргументов, таких как имена регистров, для того чтобы скомпилировать соответствующий машинный код. Описание мнемоники может быть простым или сложным, в зависимости от числа необходимых аргументов и от количества машинных команд, которые она должна скомпилировать. Команда RET (возвращение из подпрограммы) для 8088 является однобайтовой инструкцией, не требует аргументов и может быть описана простым мнемокодом. Она описывается как : RET ( -) C3 С, ; Но поскольку существует большее число мнемоник с однозначным соответствием между мнемокодом и однобайтовой инструкцией без каких-либо аргументов, для формирования такой мнемоники лучше иметь слово-описатель ; 0ARGMAKE CREATE С, DOES> С@ С, ; После этого можно сформировать таблицу мнемоники следующим образом: C3 0ARGMAKE RET CE 0ARGMAKE INTO 90 0ARGMAKE NOP CF 0ARGMAKE IRET 9C 0ARGMAKE PUSHF 9D 0ARGMAKE POPF и т. д.
Мнемоника ассемблера, требующая одного аргумента, немного сложнее. К счастью, в, конструкции машинных команд существует логика. Для микропроцессора 8088 большинство мнемокодов, которые используют в качестве аргумента номер только одного регистра, формируют машинную инструкцию, где номер регистра закодирован в младших трех разрядах. Таким образом, в двоичном представлении инструкция DX POP имеет вид 01011010, где младшие разряды 010 указывают на регистр DX. Инструкция POP может быть сформирована путем добавления номера регистра к значению 01011000 - или в шестнадцатеричном представлении к 58. (Если вы вспомните восьмеричное счисление, вы сможете понять, что регистры и инструкции могут быть очень удобно представлены в восьмеричном виде. DX POP будет соответствовать 132, где 13 указывает на POP, a 2 - на регистр DX). PUSH будет формироваться путем добавления номера регистра к 01010000 или 50 в шестнадцатеричном представлении. XCHG будет соответствовать сумме номера регистра и 10010000 или 90 в шестнадцатеричном виде.
Номера регистров можно определить через константы. Так, для микропроцессора 8088
0 CONSTANT AX 4 CONSTANT SP 1 CONSTANT CX 5 CONSTANT BP 2 CONSTANT DX 6 CONSTANT SI 3 CONSTANT BX 7 CONSTANT DI Теперь может быть сконструировано слово-описатель для одноаргументных мнемокодов: : 1ARGMAKE CREATE С, DOES> С@ + С, ; а мнемоника формируется в другой таблице: 58 1ARGMAKE POP 50 1ARGMAKE PUSH 90 1ARGMAKE XCHG 40 1ARGMAKE INC и т.д. Теперь, поскольку действие этих производных слов заключается в добавлении числа из стека к их базовому значению и поскольку номер регистра можно занести в стек с помощью констант, которые мы описали, вы можете использовать конструкции типа SI PUSH, чтобы добавить 6 к 50 и занести результирующее число 56, соответствующее машинной инструкции, в описании слова типа CODE.
Использование мнемоники, описанной только таким способом, даст очень ограниченный, хотя и применимый, поднабор инструкций 8088. Для получения более полного набора команд ассемблера вы должны позволить мнемонике иметь переменное число аргументов, генерировать две и более машинные команды, устанавливать определенные разряды в нужное состояние, чтобы сообщить, например, что содержимое регистра следует рассматривать как байт или как пару байтов. Наша задача не в том, чтобы предложить вам программу ассемблера для 8088, а в том чтобы изложить, как это можно сделать. Если у вас есть ассемблер для вашей ЭВМ, вы можете для лучшего понимания просмотреть его исходные тексты.
Микропроцессоры имеют много разных условных инструкций, работа которых зависит от значений флагов. Флаги - это двоичные разряды регистров, обычно называемых регистрами состояния. Значения разрядов в регистре состояния определяется различными действиями. Например, флаг может быть установлен р результате арифметического переполнения, при сравнении содержимого двух регистров (например, с помощью мнемокода СМР для 8088), если результат вычитания равен 0, или в результате каких-то других операции. Например, операция AX DX СМР установит флаг Z (нуль) в единичное состояние, если значения содержимого регистров АХ и DX равны.
В MMSFORTH для 8088 команда 4000 ~ Z JMPC передаст управление по адресу 4000, если значение флага Z не равно 0, т. е. если содержимое двух регистров идентично. Мнемоника, используемая для целей ветвления в разных Форт-ассемблерах, варьируется от версии к версии. В этом случае ~ Z JMPC эквивалентно стандартной мнемонике 8088 JNZ. ~ соответствует оператору "NOT", a Z - это оператор, который говорит: "обратите внимание на флаг Z". То есть если значение флага Z не равно О, выполните ветвление. Существует много способов реализации в Форте операций с флагами, вам следует заглянуть в документацию для вашей версии.
Большинство Форт-ассемблеров поддерживают условные переходы и циклы со структурой, почти идентичной словам IF,.. ELSE... THEN, BEGIN...UNTIL и BEGIN...WHILE...REPEAT в словаре FORTH, Фактически имена слов в контекстном словаре ассемблера могут быть теми же самыми (как в примерах MMSFORTH, приведенных ниже); возможность использовать слова с идентичными именами и со сходными, но разными функциями, является главной причиной введения различных контекстных словарей. Подобно своим эквивалентам в словаре FORTH, между словами условных переходов и циклов в ассемблере также помещаются слова, но уже имеющие мнемонику ассемблера. В ассемблере такие слова, как BEGIN...UNTIL вызывают передачи управления в процессе исполнения команд в рамках отдельного слова. Но в отличие их от эквивалентов в словаре FORTH передачи управления происходят не на базе кодов, содержащихся в стеке, а на основе значений флагов. Управляющий флаг должен быть указан в качестве аргумента перед соответствующим словом. Рассмотрим пример слова, которое складывает два числа из стека, если они равны, в противном случае - вычитает. Вы знакомы со всеми этими мнемокодами из предшествующих примеров.
CODE = IF + ELSE - ( n1 n2 - n3) BX POP AX POP ( Извлекаем числа из стека) BX AX СМР ( Сравниваем их) Z IF ( Если Z равен 1. т. е. АХ = BX...) BX AX ADD ( Складываем числа) ELSE ( В противном случае ...) BX AX SUB ( Вычитаем числа) THEN ( И в любом случае ) AX PUSH ( Заносим результат е стек) NEXT END-CODE
Важным моментом здесь является то, что IF выполняет свою работу, основываясь на значении когда в регистре состояния Z, заданном оператором Z. Эквивалентная функция была бы выполнена "стандартным" Форт-описанием: : =IF+ELSE- ( n1 n2 -- n3) 2DUP = IF + ELSE - THEN ;
Хотя последнее описание намного короче и легче читается, в MMSFORTH на IBM PC при исполнении 100 000 раз оно требует 19 с, в то время как ассемблерная реализация занимает только 5 с. Если быстродействие важно, имеет смысл описать критические по времени слова, используя ассемблер.
Вот пример ассемблерного цикла BEGIN...UNTIL. Здесь перемножаются два числа в стеке путем последовательного их сложения.
CODE *NEW ( n1 n2 -- n3) 0 # BX MOV ( Обнуление ВХ, ВХ - счетчик) 0 # CX MOV ( Обнуление CX, CX - аккумулятор) AX POP DX POP ( Извлекаем числа из стека) BEGIN ( Начало бесконечного цикла) BХ INC ( Добавляем 1 к ВХ) DX CX ADD ( Сложение DX и CX) AX ВХ СМР ( Установка Z=1 при равенстве) Z UNTIL ( Продолжение цикла, пока не будет AX = ВХ) CX PUSH (Занесение результата в стек) NEXT END-CODE
Теперь вы понимаете, как работает *NEW. Это не самый быстрый способ умножения чисел, действие его на 10% медленнее, чем применения слова *, но это хороший пример использования циклов в ассемблере. Вы узнаете больше о применении циклов и передач управления из упражнений.
Упражнения
1. Микропроцессор Z-80, подобно 8088, характеризует номера 16- разрядных регистров 3-разрядными кодами, которые обычно представляются восьмеричным числом в середине кода команды. Коды регистров двойной длины: ВС - 0, DE - 2, HL - 4 и AF-6. Таким образом, DE POP будет соответствовать коду 11010001 или 321 в восьмеричном виде, в то время как HL POP даст 11100001 или 341 в восьмеричной форме. Опишите константы для номеров регистров. Теперь опишите слово, аналогичное 1ARGMAKE. которое может быть использовано для описания POP и PUSH. Производные слова с кодом регистра а качестве аргумента должны компилировать правильные машинные коды.
Вам нужно умножить величины констант на некоторое число, прежде чем добавить их к "базовой" величине мнемокода. 2. Опишите слово типа CODE с именем IF-DROP, которое удаляет два числа из стека, если они равны между собой. 3. Опишите 2* на ассемблере (не используйте циклов). 4. Опишите 10* на ассемблере (используйте циклы).
Обращение к другим программам, написанным в машинных кодах
Очень часто возникает желание обратиться к подпрограмме, написанной в машинных кодах, из описаний типа CODE. Такими подпрограммами могут быть программы, загруженные вами в память с помощью ассемблера, или это могут быть программы, хранящиеся в ПЗУ, или драйверы печати/дисплея, загруженные в память другими программами. По сравнению с известными языками обращение к машинной подпрограмме в Форте весьма простое.
Большинство микропроцессоров и Форт-ассемблеров имеют команду CALL для передачи управления по указанному адресу. RET - команда, которая возвращает управление из подпрограммы по адресу, следующему сразу после команды обращения. Адрес, куда должен быть осуществлен воз врат, обычно укладывается в стек командой CALL и извлекается оттуда командой RET (так работают микропроцессоры 8088 и Z-80). Предположим, что мы хотим обратиться к помеченной подпрограмме из описания CODE- В частности, мы хотим, чтобы подпрограмма извлекла два кода из стека (до CALL) и занесла их в регистры DX и АХ (в 8088). Ниже представлена реализация этой программы:
CREATE POPEM ASSEMBLER CX POP ( Занести в CX из стека адрес возврата, занесенный туда командой CALL) DX POP AX POP ( Извлечь из стека нужные значения) CX PUSH ( Вернуть в стек адрес возврата) RET ( Вернуться к программе, откуда произошел вызов CALL) Теперь можно обратиться к POPEM CODE MYSWAP ( n1 n2 - n2 n1) РОРEМ CALL DX PUSH AX PUSH NEXT END-CODE или использовать POPEM в ряде других описаний: CODE 2DROP ( n1-n2 -> ) POPEM CALL NEXT END-CODE CODE 2DUP ( n1 n2 - n1 n2 n1 n2) РОРЕМ CALL AX PUSH DX PUSH AX PUSH DX PUSH NEXT END-CODE CODE OVER ( n1 n2 - n1 n2 n1) РОРЕМ CALL AX PUSH DX PUSH AX PUSH NEXT END-CODE
Это, однако, плохой пример. Хотя мы обратились к подпрограмме в нескольких местах, что стоили времени и не сэкономило достаточно памяти. Инструкции CALL и RET, а также команды для запоминания и извлечения адресов возврата требуют времени и машинных команд. Чтобы сделать написание подпрограммы на ассемблере привлекательным, подпрограмма должна быть длиннее той надстройки, которая необходима для ее вызова, и даже если используется меньше памяти, какое-то время будет потеряно. Здесь обычно находится компромисс между временем исполнения и экономией памяти.
MMSFORTH имеет слово, которое вы, возможно, захотите описать в вашей системе (оно часть словаря FORTH, а не ASSEMBLER). : LABEL CREATE [COMPILE] ASSEMBLER : LABEL формирует заголовок для подпрограммы в точности так, как это делал оператор РОРЕМ из нашего примера. Таким образом, можно описать РОРЕМ как LABEL РОРЕМ СХ POP DX POP AX POP CX PUSH RET
Удобно, не так ли? Слово LABEL позволяет описывать подпрограммы, написанные на ассемблере; оно позволяет использовать одно и то же слово во многих макроассемблерах.
Вот немного более эффективное применение подпрограммы. Имя ** часто используется для слов, обозначающих возведение числа в степень. То есть 5 4 ** возведет число 5 в четвертую степень, выдав 625. Это может быть сделано путем умножения второго сверху числа, хранящегося в стеке, на само себя. Число таких умножений определяется числом на вершине стека. Мы можем описать ** так, чтобы нужное число раз вызывалась подпрограмма MULTI. Сначала мы опишем слово, осуществляющее вызов, а затем подпрограмму, хотя на практике подпрограмма пишется сначала.
CODE ** ( n1 n2 -- n3) 1 # ВХ MOV ( Заносим 1 в регистр-счетчик ВХ) AX POP ( Заносим показатель степени в АХ) DX POP ( Заносим число в регистр DX) DX СХ MOV ( Вводим число в регистр произведения) BEGIN ( Запускаем бесконечный цикл) AХ ВХ СМР ( Выполнено нужное число циклов?) WHILE ( Если нет...) ВХ INC ( Даем приращение содержимому счетчика) MULTI CALL ( Умножаем произведение на число) REPEAT ( Продолжаем цикл, пока не будет выполнено условие) СХ PUSH ( Заносим результат в стек) NEXT END-CODE
ВХ - регистр-счетчик. Когда в результате приращений его значение достигнет величины показателя степени, работа завершается. СХ - регистр произведения, содержит результат MULTI и в исходном состоянии должен быть сделан равным числу-аргументу. Обращение к MULTI производится столько раз, сколько нужно в рамках цикла BEGIN...WHILE...REPEAT, при этом каждый раз результат выдается в регистр произведения. MULTI можно описать как
LABEL MULTI AХ PUSH ВХ PUSH DX PUSH ( Сохраняем регистры в стеке) 0 # ВХ MOV ( Сбрасываем счетчик в 0) DX AX MOV ( AX используется для сравнения показателя со счетчиком) СХ DX MOV ( DX используется для сложения с регистром произведения) 0 # СХ MOV ( Сброс регистра произведения в 0 ) BEGIN ( Запуск бесконечного цикла) ВХ INC ( Даем приращение счетчику MULTI) DX СХ ADD ( Складываем DX и СХ) AХ ВХ СМР ( Сравниваем АХ и ВХ) Z UNTIL ( Пока АХ = ВХ) DX POP ВХ POP AX POP ( Восстановление регистров) RET (Возврат вызвавшей программе) END-CODE
Вы должны понять, как работает MULTI, путем сравнения с *NEW, описанным ранее. Но есть одно важное различие. Мы вынуждены были записать содержимое регистров AX, BX и DX в стек, поскольку они были нужны в **, а также в MULTI. Содержимое регистров было затем восстановлено из стека до RET в MULTI. Обычно регистры необходимы для различных целей в подпрограмме, они же используются и в основной программе, одним из выходов из положения является сохранение их в стеке. Фактически в большинстве языков и в программировании на "нормальном" ассемблере главная функция стека - запоминание величин на время выполнения каких-то операций.
Можно также описать машинные программы без заголовка и затем несколько раз к ним обращаться при описании других слов. Вот пример описания слов SWAP, 2DUP, OVER и 2DROP с подпрограммами без заголовков и с использованием стека возвратов для передачи адреса подпрограммы при компиляции: HERE >R ( Засылка адреса подпрограммы в стек возвратов) ASSEMBLER ( Делаем ассемблер контекстным словарем) СХ POP DX POP AX POP CX PUSH RET ( Текст подпрограммы,эквивалентной РОРЕМ) CODE SWAP R@ CALL DX PUSH AX PUSH NEXT END-CODE CODE 2DUP R@ CALL AX PUSH DX PUSH AX PUSH DX PUSH NEXT END-CODE CODE OVER R@ CALL AX PUSH DX PUSH DX PUSH NEXT END-CODE CODE 2DROP R> CALL NEXT END-CODE
Слово HERE выдает адрес, где начинается подпрограмма, он кладется в стек возвратов, откуда этот адрес может многократно извлекаться. Каждое описание типа CODE использует адрес из стека возвратов в качестве аргумента для CALL. Адрес удаляется из стека возвратов командой R> в описании 2DROP, после которого он более не нужен. Единственным преимуществом беззаголовочных подпрограмм по сравнению с LABEL (кроме экономии нескольких байтов) является то, что эти подпрограммы не могут быть использованы неправильно, так как не могут быть найдены в словаре. Недостаток такого метода заключается в том, что это делает программу трудночитаемой. Вы должны также помнить, что применение подпрограмм усложняет отладку программ на ассемблере.
Конечно, не важно, если вы не можете обратиться к подпрограмме, которую не вы создали. Если же вы используете Форт в рамках операционной системы, в вашем распоряжении много подпрограмм, таких как драйверы печатающего устройства или интерфейса RS-232, графические программы, процедуры для работы с диском и т.д., к которым можно обратиться из слов типа CODE. Если ваша ЭВМ имеет версию Бейсика в ПЗУ, она включает в себя подпрограммы арифметики с плавающей точкой. Вы можете воспользоваться ими, если выяснить, как к ним обращаться и что они делают со стеком и регистрами. Существует много книг, которые описывают резидентные программы в наиболее популярных микроЭВМ. Однако необходима осторожность, так как вы можете не знать все, что делает данная подпрограмма. Вам нужно убедиться, что любые регистры, которые содержат информацию, полезную для Форта, и которые будут использованы в подпрограмме (указатели, например), сохраняются в стеке и восстанавливаются после обращения к программе. Вы должны знать, что делает программа со стеком и указателем стека, так как вы не хотите терять информацию при обращении. Вы должны также быть уверены, что подпрограмма не пытается занести что-то в область памяти, занятую словарем. Вы возможно, сможете в результате расследования и определенных проб и ошибок выяснить некоторые вещи, но этот процесс потребует вашего терпения.
Если у вас есть дисассемблер, - это упростит задачу. (Дисассемблер просматривает программу в машинных кодах и транслирует ее в мнемоническую форму, облегчая понимание программы.)
Программы в машинных кодах могут быть получены из других источников, таких как статьи в журнале и подпрограммы, встроенные в Бейсик-программы. Если у вас есть исходный текст такой программы на ассемблере, обычно проще заставить ее работать в Форте, используя Форт-ассемблер. Если это не практично, вы можете скомпилировать машинную программу в слово, описанное с помощью LABEL, используя С,. Вы должны быть уверены, однако, что программа перемещаема - т.е. она не содержит каких-либо абсолютных адресов передач управления или вызовов, поскольку адрес передачи управления почти наверняка изменится после того, как вы встроете программу в слово. Все передачи управления и вызовы должны быть относительными, т.е. заданными величиной смещения к адресу позиции, откуда производится вызов.
Но существуют исключения. Некоторые версии Форта позволяют перемещать верхнюю часть словаря Форта из области больших адресов в начальную часть памяти (смотри карту памяти в гл. 14), освобождая место для большого числа машинных программ, которые созданы для работы в верхней части памяти. Если это так, вы можете поместить машинную программу в массив и перемещать ее с помощью CMOVE в верхнюю область памяти, используя адрес обращения, предусмотренный в оригинальной версии программы. Мы использовали таким способом на TRS-80 очень сложный драйвер координатографа. Это снова потребует экспериментов.
Упражнения
1. Часто полезно сохранить регистры в стеке на время выполнения машинной программы, так что вы сможете просмотреть их содержимое при отладке программы после того, как она выполнена. Используйте LABEL для описания SAVESTACK, чтобы заносить в стек содержимое регистров АХ, ВХ, СХ и DX 8088. Исполнение SAVESTACK CALL в про грамме будет выполнять тахой перенос. Будьте осторожны, не допускайте путаницу с адресом возврата! 2.
В верхней части памяти вашей ЭВМ с адресом FF00 есть программа для управления координатографом. Она рисует арифметические символы и требует, чтобы в регистр СХ было занесено значение высоты символа в сотых долях дюйма, а в регистр DX - ASCII-код символа. Опишите слова с именем PLOTEMIT, для которого высота символа должна лежать во второй сверху позиции стека, а ASCII-код - на вершине и которое обращается к драйверу координатографа. 3. У вас имеется 16-канальный аналого-цифровой преобразователь, связанный с вашей микроЭВМ. Он опрашивается машинной программой по адресу FFOO- Номер канала может быть передан в программу через регистр АХ, и программа возвращает результат для данного канала в милливольтах через тот же регистр. Опишите слово GETDATA, которое воспринимает номер канала из стека и туда же кладет результат в милливольтах. То есть если в канале 8 напряжение равно 528 мВ, 8 GETDATA запишет в стек число 528. 4. Программа из упражнения 3 изменяет величины в регистрах SI и ВХ, и вы должны быть уверены, что они не изменились при завершении исполнения GETDATA. Чтобы решить эту проблему, переопределите GETDATA.
Выводы
Форт - быстродействующий язык, даже если не использовать ассемблер. А ассемблер труднее применить, чем Форт, и он работает только на одном типе процессора. Главная проблема использования ассемблера в Форте заключается не в том, чтобы вставить все, что можно, в CODE- слова, а в том, чтобы применять разумную мнемонику. Когда эффективен ассемблер? При выполнении двух условий: если вы хотите, чтобы программа работала быстрее, или когда вы обнаружили что-то, что вы не можете сделать в Форте (последнее случается редко). В обоих случаях вы захотите использовать ассемблер в минимальном объеме. Вы добьетесь этого, правильно факторизируя Форт-программу. Те небольшие части, которые используются большую часть времени, так как они исполняются снова и снова, могут быть оформлены в виде коротких code-слов. На ассемблере могут быть запрограммированы такие критические функции, как обслуживание аналого-цифрового преобразователя или координатографа, которые не поддерживаются фортом.
Каждое слово Форта должно быть коротким и простым, это правило относится и к CODE-словам.
Цикл - редактор-ассемблер-редактор-связей-загрузка-исполнение-отладка, который необходим для любого ассемблера, исключается в форте. Форт-ассемблер позволяет компилировать и тестировать машинную программу небольшими частями, так же как и обычную Форт-программу. В результате программирование оказывается более производительным, а программы - лучше организованными и более эффективными. Время разработки программы в Форте составляет лишь долю того, что требуется для случая чисто ассемблера.
Мы начали эту книгу, сказав, что Форт позволит нам применить все доступные особенности вашей ЭВМ, использовать все быстродействие, на которое она способна, и сделать это более легко, чем в любом другом языке. Разумно закончить книгу описанием Форт-ассемблера, поскольку именно комбинация применения ассемблера и "нормального" Форта обеспечивает полную эффективность. Форт может быть использован на разных уровнях: простейший - интерактивный калькулятор для коротких программ без долговременного их хранения, наиболее сложный уровень - применение Форт-слов высокого уровня в комбинации с ассемблером для создания комплексных программ, таких как контроллеры процессов в реальном масштабе времени, системы управления базами данных, экспертные системы и даже другие языки программирования.