Автор:
Хищник
Дата публикации: Ноя 25, 2006 на данном форуме.
Постфиксный нуль-операндный ассемблер
Наличие ассемблера в составе форт-системы представляется привлекательным по целому ряду факторов. Поддержка транслятором машинного языка предоставляет программисту лишнюю степень свободы и возможность реализовывать наиболее ответственные участки кода. Общая идеология Форта существенно облегчает стыковку ассемблерных модулей со словами высокого уровня. Как правило, ESP указывает на вершину стека возвратов, EBP – на вершину стека данных, а математический сопроцессор изначально организован в виде стека. Соответственно, ассемблерный модуль может довольно просто получить передаваемые ему параметры и отправить на стек результаты.
Интеграция ассемблерного кода с Фортом позволяет, кроме того, использовать высокоуровневые определения внутри ассемблерных вставок, в том числе и на этапе трансляции кода. Например, вычисление смещения или начального адреса в ассемблерном коде может быть выполнено с использованием обычных слов форт-системы, в том числе и высокоуровневых.
При создании ассемблера, интегрированного с форт-системой, разумно было бы использовать возможности транслятора по обработке входного потока. В этом случае использование постфиксной формы записи ассемблерных команд позволит минимизировать объем ассемблера, но за счет отказа от привычного ассемблерного синтаксиса. Следует заметить, однако, что выигрыш в объеме ассемблера при переходе к постфиксному представлению получается весьма значительным.
Классический пример постфиксного ассемблера для процессора i8080 (580ВМ80) приведен в книге Баранова и Ноздрунова «Язык форт и его реализации». В этой книге приведен полноценный ассемблер, объем листинга которого составляет всего полторы страницы. Ассемблер интенсивно использует особенности системы команд процессора i8080, вычисляя код операции на основе переданных на стеке параметров – номеров регистров. При этом используется тот факт, что команды i8080 слабо кодированы, т.е. код команды включает в себя битовые поля, которые могут быть разобраны отдельно и соответствуют, в том числе, номерам регистров, используемых в команде.
Например, команда пересылки между 8-разрядными регистрами в процессоре i8080 имеет формат:
0 1 a a a b b b
В этом случае очень удобно организовать компиляцию команды MOV в виде:
Код:
: MOV ( aaa, bbb а ) 8 * + 64 + TC-C, ;
Если символическим обозначениям регистров поставить в соответствие номера, то далее слово MOV можно использовать так:
Код:
A, B MOV – пересылка из B в A.
В более привычном формате эта команда выглядела бы как MOV A, B
Поскольку во всех командах, где необходимо указывать регистры, используется одна и та же интерпретация битовых полей, в ассемблере можно определить следующие слова:
A B C D E H L BC DE HL
A, B, C, D, E, H, L, BC, DE, HL,
Однако ассемблер процессора i8080 имеет интересную особенность, существенно облегчающую построение для него форт-ассемблера. Каждому типу команды соответствует уникальная мнемоника – например, загрузка в регистр непосредственного значения вместо MOV записывается как MVI (MoV Indirect – загрузить непосредственно). В ассемблере же процессоров x86 для всех типов команд загрузки используется запись MOV, которая может иметь больше десятка различных сочетаний параметров. Так же обстоит ситуация с другими группами команд. В итоге постфиксный ассемблер для процессоров x86 должен анализировать типы операндов, переданных на стеке для формирования кода операции. В отличие от этого, ассемблер, подобный ассемблеру для i8080, может определять тип команды и сочетание операндов, ориентируясь только на мнемонику.
Альтернативным подходом, обеспечивающим классическую префиксную запись команд, может быть введение отдельных наборов слов, ответственных за команды и отдельные группы операндов соответственно. При этом начало команды сбрасывает состояние внутренней машины ассемблера в исходное состояние, а машинный код генерируется после сбора транслятором информации обо всех требуемых операндах.
Например,
Код:
MOV EAX, # 1234
MOV – установка ассемблера в состояние «команда пересылки»; ассемблер ожидает два операнда
EAX, – команда «первый операнд – регистр EAX»
# – выделить следующий токен и рассматривать его как второй операнд команды – непосредственное значение; после получения второго операнда команда пересылки становится завершенной и может быть сгенерирован машинный код.
Нетрудно заметить, что быстрая разработка форт-ассемблера для процессоров x86 существенно затруднена наличием в этих процессорах разнообразных сочетаний аргументов команд. Все эти сочетания должны корректно распознаваться, недопустимые сочетания аргументов должны формировать точное описание ошибки, а от самого ассемблера требуется некоторый запас расширяемости. Например, команда MOV в процессорах 80486, Pentium и выше получила дополнительные варианты сочетаний операндов – добавились группы регистров DR и TR. Кроме того, для поддержки наборов MMX, 3DNow!, SSE и им подобных (в том числе и ожидающихся в будущем) ассемблер должен обладать существенной гибкостью. В итоге возрастание сложности и функциональной насыщенности форт-ассемблера приведет к тому, что он станет сравним по сложности разработки с обычным ассемблером, использующим классическую префиксную запись. Очевидно, что затратив подобные усилия, можно было получить и ассемблер с классическим синтаксисом, который был бы способен обрабатывать огромное количество существующих ассемблерных текстов. В то же время выгода постфиксного форт-ассемблера заключается как раз в простоте его разработки и адаптации к новым условиям.
Для нуль-операндного форт-ассемблера предлагается следующий синтаксис:
Группа операций Ассемблерная запись Постфиксная запись Пример
Пересылка данных MOV dest, src src->dest EAX->EBX
[EBP+4]->ECX
[EBX]->XMM0
1234 ->EBX
1234 [EBX+ECX*8+OFFSET]->F
Стековые операции PUSH reg PUSH_reg PUSH_EAX
POP reg POP_reg POP_EAX
Инкремент INC reg reg++ [STACK]++
AL++
AX++
EAX++
Декремент DEC reg reg-- EAX--
Сравнение CMP src, dest src==dest 1234 EAX==
AL==CL
ESI==EDI
Проверка TEST src, dest src??dest ESI??EDI
Инверсия NOT reg ~reg ~EAX
Смена знака NEG reg -reg -ECX
Логическое И AND src, dest src&&dest EAX&&EAX
1234 EAX&&
Логическое ИЛИ OR src, dest src||dest EAX||EAX
1234 EAX||
Логическое ИСКЛ ИЛИ XOR src, dest src~~dest EAX~~EAX
1234 EAX~~
Сложение ADD reg, reg/imm reg+= 1234 EAX+=
EAX+=EBX
EAX+=[EBP+4]
Вычитание SUB reg, reg/imm reg-= 1234 EAX-=
EAX-=EBX
EAX-=[EBP+4]
Умножение MUL reg *reg *ECX
IMUL reg I*reg I*ECX
Деление DIV reg /reg /ECX
IDIV reg I/reg I/ECX
Логический сдвиг SHR reg reg>> EAX>>
SHL reg reg<< EAX<<
Арифметический сдвиг SHRA reg reg>>> EAX>>>
Вращение ROR reg reg->> EAX->>
ROL reg reg<<- EAX<<-
Следующая группа имеет одинаковую запись для стандартного и постфиксного предстиавленияБезоперандные команды CLD STD CDQ CLI STI NOP IRET
Префиксы строковых операций REPZ REPNZ
Префиксы замены сегмента CS: DS: ES: FS: SS: GS:
Переход по регистру JMP reg JMP_reg JMP_EAX
CALL reg CALL_reg CALL_EAX
Условные переходы JC offset JC JA JAE JB JBE JC JE JZ
Арифметические FADDPP reg1, reg2 Fop F+
операции сопроцессора FSUBPP reg1, reg2 F-
FMULPP reg1, reg2 F*
FDIVPP reg1, reg2 F/
Рассмотрим исходный текст форт-ассемблера.
Код:
\ ========================== Start ==========================
VOCABULARY ASSEMBLER
ASSEMBLER DEFINITIONS
DECIMAL
CREATE LOCALS[] 800 ALLOT \ адреса меток
CREATE LOC-FWRD[] 800 ALLOT
\ адреса ссылок
\ 4 байта – номер метки, на которую делается ссылка
\ 4 байта – смещение, куда надо поставить
: CLEAR-LOCALS \ очистить локальные метки
100 0 DO
0 LOCALS[] I 8 * + !
0 LOC-FWRD[] I 8 * + !
LOOP
;
QUAN LAST-REF
: ?RESOLVE-REF ( Nlabel --> ) \ разрешает все метки с номером NLabel
TO LAST-REF
100 0 DO
LOC-FWRD[] I 8 * + @ LAST-REF = IF
[C]HERE LOC-FWRD[] I 8 * 4 + + @ - 4 - \ offset
LOC-FWRD[] I 8 * + 4 + @ + \ ADDR to store
[C]!
THEN
LOOP
;
: VIEW-L \ просмотреть информацию о метках (для отладки)
20 0 DO
0 I 4 + GOTOXY I .
5 I 4 + GOTOXY LOCALS[] I * * + @ .
LOOP
;
: ZC, ( C --> ) \ компилировать байт в сегмент кода
[C]C,
;
: ZW, ( W --> ) \ компилировать слово в сегмент кода
[C]W,
;
: ZD, ( D --> ) \ компилировать двойное слово в сегмент кода
[C],
;
: LABEL: ( NUM --> ) \ поставить метку
DUP
[C]HERE SWAP 8 * LOCALS[] + !
?RESOLVE-REF
;
: FORWARD ( N --> ) \ определить ссылку вперед на метку N
0 BEGIN
1+ LOC-FWRD[] OVER 8 * + @ 0 = OVER 100 = OR
UNTIL
DUP 100 = IF CR ." Too many locals" ABORT THEN
DUP ROT SWAP 8 * LOC-FWRD[] + !
[C]HERE SWAP 8 * 4 + LOC-FWRD[] + !
0 ZD,
;
: LABEL ( N --> ) \ определить ссылку назад на метку N
LOCALS[] SWAP 8 * + @ [C]HERE - 4 - ZW, [ HEX ] FFFF ZW,
[ DECIMAL ]
;
Приведенный выше фрагмент решает две задачи: управление памятью при размещении кода и организация работы с метками. Переходы явно разделены на переходы назад (адрес перехода уже известен) и переходы вперед (адрес перехода неизвестен, известно место, с которого делается переход). Объявление метки выполняется словом LABEL: ( например 1 LABEL: ). При этом текущий адрес добавляется в таблицу локальных меток, хранящуюся в массиве LOCALS[].
При объявлении новой метки может оказаться, что на нее уже имеются ссылки – объявленные ранее переходы вперед. В этом случае ссылки необходимо разрешить (resolve), т.е. вписать полученный адрес по всем запомненным ранее и зарезервированным для этой цели ячейкам. Эта операция выполняется словом ?RESOLVE-REF, которая получает на стеке номер только что объявленной ссылки и просматривает таблицу ссылок вперед на предмет наличия в ней ссылок на только что созданную метку. Каждая найденная ссылка сообщает адрес, куда необходимо вписать только что полученный адрес метки:
Код:
1234 JMP 1 FORWARD инструкция JMP (1 байт)
1235 __ __ __ __ место для вписывания адреса метки (4 байта), резервируется словом FORWARD
1239 следующая инструкция
? ?
1456 1 LABEL: объявлена метка 1 с адресом 1456. для разрешения по адресу 1235 необходимо
вписать:– 1456 для прямой адресации– 1456 – 1235 – 4 = 217
для адресации со смещением (смещение отсчитывается от адреса, следующего за
командой перехода)
Приложения следуют