Table of Contents

Внутреннее устройство Forth-машины

Словарь

В Forth, как вы уже знаете, функции называются "словами", и, так же, как и в других языках программирования, у них есть имя и определение. Вот два слова Forth:

: DOUBLE DUP + ;              \ имя: "DOUBLE"     определение: "DUP +"
: QUADRUPLE DOUBLE DOUBLE ;   \ имя: "QUADRUPLE"  определение: "DOUBLE DOUBLE"

Слова, как встроенные, так и те, которые программист определяет позже, хранятся в словаре, который является связным списком записей словаря.

forth-dict-list.png

Мы дойдем до определения слова позже. Сейчас просто посмотрите на его заголовок (dictionary entry / header). Первые 4 байта - это LINK-указатель. Он указывает на предыдущее слово в словаре, и для первого слова в словаре является указателем NULL. Затем идет байт LENGTH/FLAGS. Длина слова может составлять до 31 символа (используется 5 бит), а три верхних бита используются для различных флагов, про которые я расскажу позже. За этим следует само имя, и в этой реализации имя всегда кратно 4 байтам, и первоначально заполнено нулевыми байтами. Кратность нужна просто для того, чтобы определение начиналось с 32-битной границы - такого рода выравнивание важно для быстродействия.

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

DOUBLE и QUADRUPLE могут выглядеть так::

forth-dict-2words.png

Вы должны увидеть из этого, как можно реализовать поиск слова в словаре (просто пройти по записям, начинающимся с той, на которую указывает LATEST, и сопоставляя имена, пока вы не найдете совпадение или не наткнетесь на указатель NULL в конце словаря).

И как добавить слово в словарь (создать новое определение, установить его LINK в LATEST и установить LATEST, чтобы он указывал на новое слово). Мы увидим именно эти функции, реализованные на ассемблере позже.

Одним из интересных последствий использования связанного списка является то, что вы можете переопределять слова, и более новое определение слова переопределяет более старое. Это важная концепция в Forth, потому что это означает, что любое слово (даже "встроенные" или "стандартные" слова) могут быть переопределены новым определением, либо для его улучшения, либо для его ускорения или даже для его отключения. Однако из-за того, как компилируются слова Forth, слова, определенные с использованием старого определения слова, продолжают использовать старое определение. Только новые слова, определенные после нового определения, используют новое определение.

Прямой шитый код

Теперь мы перейдем к действительно важному, для понимания Forth, аспекту. Если вы не поймете этот раздел, то вы не поймете как работает Forth, и это будет неудачей с моей стороны.

Давайте поговорим сначала о том, что означает "шитый код". Представьте себе своеобразную версию Cи, где вам разрешено вызывать только функции без аргументов. (Не беспокойтесь, о том, что такой язык будет совершенно бесполезен) Итак, в нашем своеобразном Cи код будет выглядеть так:

f () {
    a ();
    b ();
    c ();
}

…и так далее. Как бы функция, скажем, f выше, была скомпилирована стандартным компилятором Cи в машинный код? Например для i386 так:

f:
    CALL a          #  E8 08 00 00 00
    CALL b          #  E8 1C 00 00 00
    CALL c          #  E8 2C 00 00 00
    ;;  сейчас мы пока игнорируем возврат из функции

E8 - это машинный код x86 для «CALL» функции. В первые 20 лет компьютерная память была ужасно дорогой, и мы могли бы беспокоиться о том, что расходуем впустую память повторенными байтами «E8». Мы можем сэкономить 20% в размере кода (и, следовательно, дорогостоящей памяти), сжав это:

08 00 00 00   #  Просто адреса функций, без CALL
1C 00 00 00
2C 00 00 00

На 16-битной машине, подобной той, на которой Forth был запущен в первый раз, экономия еще больше - 33%.

Историческое примечание: Если модель исполнения, используемая Forth, кажется странной, то она была полностью мотивирована необходимостью экономить память на ранних компьютерах. Это сжатие не так важно сейчас, когда наши машины имеют больше памяти в своих кэшах L1, чем в ранних компьютерах, но модель исполнения по-прежнему обладает некоторыми полезными свойствами. Кроме того, на современных процессорах, Forth-система способна целиком поместиться в кэше процессора, что делает ее прямо таки чудовищно быстрой.

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

На машине i386 получается, что этот интерпретатор можно легко написать в двух ассемблерных инструкциях, которые превращаются всего в 3 байта машинного кода. Давайте сохраним в регистре %esi указатель на следующее слово для выполнения:

forth-interpret-01.png

В i386 есть инструкция LODSL (или в терминологии руководств Intel, LODSW). Она делает две вещи:

  • читает из памяти, на которую указывает %esi 4 байта в регистр %eax
  • увеличивает значение в регистре %esi на 4

Итак, после выполнения инструкции LODSL ситуация выглядит так:

forth-interpret-02.png

Сейчас нам надо сделать jmp на адрес, содержащийся в %eax. Это снова всего одна x86-инструкция, которая записывается как JMP *(%eax). И после того как мы сделаем JMP ситуация выглядит так:

forth-interpret-03.png

Для выполнения этой работы каждая подпрограмма сопровождается двумя инструкциями: LODSL; JMP *(%eax), которые буквально переходят к следующей подпрограмме.

И это подводит нас к нашей первой части реального кода! Ну, то есть, это макрос.

.macro NEXT
    lodsl
    jmp *(%eax)
.endm

Этот макрос называется NEXT. Он раскрывается в эти две инструкции.

Каждый примитив Forth, который мы пишем, должен быть завершен NEXT. Думайте об этом как о return.

Все, что описано выше, называется прямым шитым кодом.

Подводя итог: мы сжимаем наши вызовы функций до списка адресов и используем макрос, чтобы переходить к следующей функции в списке. Мы также используем один регистр (%esi), как своего рода указатель инструкции (Instruction Pointer), указывающий на следующую функцию в списке.

Я просто дам вам намек на то, что должно произойти, сказав, что определение Forth, такое как:

: QUADRUPLE DOUBLE DOUBLE ;   \ имя: "QUADRUPLE"  определение: "DOUBLE DOUBLE"

на самом деле компилирует (не совсем точно, но мы сразу увидим, почему) список адресов функций для DOUBLE, DOUBLE и специальную функцию EXIT для завершения.

На данный момент, остроглазые эксперты ассемблера могут воскликнуть: "вы сделали ошибку!".

Ага, я лгал вам о JMP *(%eax).

Коссвенный шитый код

Оказывается, что прямой шитый код интересен, только если вы хотите просто выполнить список функций, написанных на ассемблере. Поэтому QUADRUPLE будет работать только в том случае, если DOUBLE является функцией языка ассемблера. В прямом шитом коде QUADRUPLE будет выглядеть так:

forth-interpret-04.png

Мы можем добавить дополнительный уровень косвенности, позволяющей нам запускать как слова, написанные на ассемблере (примитивы, написанные для скорости), так и слова, написанные на Forth-е, как списки адресов.

Дополнительная косвенность является причиной скобок в JMP *(%eax).

Давайте посмотрим, как QUADRUPLE и DOUBLE действительно выглядят в Forth:

forth-interpret-05.png

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

В словах, написанных на Forth (например, QUADRUPLE и DOUBLE), codeword указывает на функцию-интерпретатор.

Я вскоре покажу вам функцию-интерпретатор, но давайте вспомним наш косвенный JMP *(%eax) с "дополнительными" скобками. Возьмем случай, когда мы выполняем DOUBLE, как показано, и вызывается DUP. Обратите внимание, что %esi указывает на адрес +

Ассемблерный код для DUP в конце делает NEXT. Это:

  • читает адрес + в %eax - теперь %eax указывает на codeword для кода +
  • увеличивает %esi на 4
  • выполняет jmp на содержимое того адреса, который лежит в %eax → т.е. jmp по адресу, лежащему в codeword слова +, → т.е. jmp на ассемблерный код, реализующий +.

forth-interpret-06.png

Поэтому я надеюсь, что я убедил вас, что NEXT делает примерно то, что вы ожидаете. Это коссвенный шитый код.

Я не сказал о четырех вещах. Интересно, сможете ли вы догадаться о них, не читая дальше?

Вот список этих вещей:

  • что делает EXIT?
  • как происходит вызов функции, т.е. как %esi начинает указывать на часть QUADRUPLE, а затем указывать на часть DOUBLE?
  • Что входит в codeword для слов, написанных на Forth?
  • Как компилировать функцию, которая делает что-то еще, кроме вызова других функций, например функцию, которая содержит число, такую как : DOUBLE 2 * ;?

Интерпретатор и стек возвратов

Не останавливаясь на этом, давайте поговорим о третьей и второй проблемах, интерпретаторе и стек возврата.

Слова, которые определены в Forth, нуждаются в codeword, указывающий на небольшое количество кода, который протягивает им "руку помощи". Им не нужно много, но им нужно то, что известно как интерпретатор, хотя на самом деле он не является интерпретатором в том же смысле, как, например, медленный интерпретатор байт-кода Java. Этот интерпретатор просто устанавливает несколько машинных регистров, чтобы затем слово могло выполняться на полной скорости с использованием модели коссвенного шитого кода, показанной выше.

Одна из вещей, которые должны произойти, когда QUADRUPLE вызывает DOUBLE, заключается в том, что мы сохраняем старый указатель инструкций %esi и создаем новый, указывающий на первое слово в DOUBLE. Поскольку нам нужно будет восстановить старый %esi в конце слова DOUBLE (в конце концов, это как вызов функции), нам понадобится стек для хранения этих "адресов возврата" (старых значений %esi).

Как вы, наверно видели в документации, Forth имеет два стека: обычный стек параметров и немного загадочный стек возвратов. Но наш стек возвратов - это просто тот стек, о котором я говорил в предыдущем абзаце, используемый для сохранения %esi когда из одного слова Forth вызывается другое слово Forth.

В этом Forth мы используем указатель обычного аппаратного стека (%esp) для стека параметров. Мы будем использовать другой указатель стека i386 (%ebp, называемый "указателем фрейма") для стека возвратов.

У меня есть два макроса, которые просто оборачивают детали использования %ebp для стека возвратов. Вы используете их так: PUSHRSP %eax (push %eax в стек возвратов) или POPRSP %ebx (pop значение, на которое указывает вершина стека возвратов %ebp в регистр %ebx).

.macro PUSHRSP reg
    lea     -4(%ebp), %ebp  # декремент %ebp
    movl    \reg, (%ebp)    # push reg в стек возвратов
.endm
.macro POPRSP reg
    mov     (%ebp), \reg    # pop вершину стека возвратов в reg
    lea     4(%ebp), %ebp   # инкремент %ebp на 4
.endm

И с этим всем мы теперь можем поговорить об интерпретаторе.

В Forth функция-интерпретатор часто называется DOCOL (я думаю, что это означает "DO COLON", потому что все определения Forth начинаются с двоеточия, как например в выражении : DOUBLE DUP ;

Интерпретатору (на самом деле это не "интерпретация") нужно push-нуть старый %esi в стек возвратов и установить %esi так, чтобы он указывал на первое слово в определении. Помните, как мы перешли к функции с помощью JMP *(%eax)? Вследствие этого удобно, что %eax содержит адрес этого codeword, поэтому просто добавляя к нему 4, мы получаем адрес первого слова идущего за codeword. Мы будем называть эту область param-field, в будущем это пригодится.

Наконец, после установки %esi, он просто делает NEXT, который вызывает запуск первого слова. Удобно, что в архитектуре i386 можно одной командой увеличить %EAX на 4 и послать результат в %ESI. Таким образом, определение DOCOL будет выглядеть так:

    .text
    .align 4
DOCOL:
    PUSHRSP %esi            # Сохранить %esi в стеке возвратов
    leal    4(%eax), %esi   # %esi теперь указывает на param-field
    NEXT                    # Делаем NEXT

Чтобы это было совершенно ясно, посмотрим, как работает DOCOL при прыжке с QUADRUPLE в DOUBLE:

forth-interpret-07.png

Во-первых, вызов DOUBLE вызывает DOCOL (codeword DOUBLE). DOCOL делает следующее: он push-ит старый %esi на стек возвратов. %eax указывает на codeword DOUBLE, поэтому мы просто добавляем к нему 4, чтобы получить наш новый %esi:

forth-interpret-08.png

Затем он делает NEXT и так как из-за магии шитого кода, это увеличивает %esi снова, то вызывается DUP.

Здесь есть одна второстепенная вещь. Поскольку DOCOL - это первый кусок ассемблерного кода, который должен быть определен в этом файле (остальные - только макросы), и поскольку я обычно (но не в этом случае) компилирую этот код с сегментом .text, начинающимся с адреса 0, DOCOL имеет адрес 0. Поэтому, если вы дизассемблируете код и увидите слово с codeword 0, вы сразу же поймете, что это слово Forth (а не ассемблерный примитив), и поэтому оно использует DOCOL в качестве интерпретатора.

К сожалению, это не сработает в современных дистрибутивах Linux, где блокируеются попытки доступа к младшим адресам памяти. За это отвечает параметр CONFIG_DEFAULT_MMAP_MIN_ADDR и на моей системе вызов cat /proc/sys/vm/mmap_min_addr возвращает 65536. Можно изменить опции линкера на "-Wl,-Ttext,10000" (адрес надо задавать шестнадцатиричным значением). Но так как я компилирую в обычный исполняемый файл ELF для Linux, статически слинкованный с библиотекой Си (которая нам понадобится для разных практических целей) при дизассемблировании придется запомнить адрес DOCOL.

Начинаем работу

Теперь давайте перейдем к гайкам и болтам. Когда мы запускаем программу, нам нужно настроить несколько вещей, таких как стек возвратов. Но как только мы сможем, мы хотим перейти в код Forth (хотя большая часть «раннего» кода Forth все равно должна быть написана как примитивы на host-языке).

Это то, что делает код настройки:

  • Делает небольшую вступительную часть - сбрасывает флаг направления DF.
  • Настраивает отдельный стек возврата (NB: Linux уже дает нам обычный стек параметров)
  • затем сразу переходит к слову Forth, называемому QUIT. Несмотря на свое название QUIT никуда не выходит. Он сбрасывает некоторое стек возвратов и начинает чтение и интерпретацию команд. (Причина, по которой он называется QUIT, заключается в том, что вы можете вызывать QUIT из вашего собственного кода Forth, чтобы «выйти» из вашей программы и вернуться к вводу и интерпретации команд).

Здесь мы настраиваем указатель HERE на начало области данных data_buffer, который я выделил в сегменте .bcc. Так проще, нежели пытаться определять и расширять data segment с помощью системного вызова brk(2), который у меня возвращает -1.

Мы используем обычный стек процесса (на который указывает регистр %esp) в качестве стека параметров, потому что операции с этим стеком - самые частые в Forth-программе, а команды работы со стеком процесса быстрее и короче. Стек возвратов используется Forth-программой реже, поэтому мы адресуемся к нему через регистр %ebp

    /* Assembler entry point. */

    .text
    .globl  forth_asm_start
    .type   forth_asm_start, @function
forth_asm_start:
    # Сбрасываем флаг направления
    cld
    # Записываем вершину стека параметров %esp в переменную S0
    mov     %esp, var_S0
    # Устанавливаем стек возвратов %ebp
    mov     $return_stack_top, %ebp
    # Устанавливаем указатель HERE на начало области данных.
    mov     $data_buffer, %eax
    mov     %eax, var_HERE
    # Инициализируем IP
    mov     $cold_start, %esi
    # Запускаем интерпретатор
    NEXT

    .section .rodata
cold_start:                             # High-level code without a codeword.
    .int QUIT

Встроенные слова

Помните наши словарные записи? Давайте приведем их вместе с кодовым словом и словами данных, чтобы увидеть, как

: DOUBLE DUP ;

действительно выглядит в памяти.

forth-interpret-09.png

Вначале мы не можем просто написать буквально : DOUBLE DUP; , потому что нам еще пока нечем читать строку, разбивать ее на слова, анализировать каждое слово и.т.д. Поэтому вместо этого нам придется определять встроенные слова, используя конструкторы данных ассемблера GNU (например, .int, .byte, .string, .ascii и.т.д.)

    .int  <указатель на предыдущее слово>
    .byte 6         # len
    .ascii "DOUBLE" # name
    .byte 0         # padding
DOUBLE:
    .int DOCOL      # codeword
    .int DUP        # указатель на codeword DUP
    .int PLUS       # указатель на codeword +
    .int EXIT       # указатель на codeword EXIT

Но это быстро утомляет, поэтому я определяю ассемблерный макрос, чтобы я мог просто написать:

defword "DOUBLE",6,,DOUBLE
    .int DUP,PLUS,EXIT

и получить точно такой же эффект. Не беспокойтесь о деталях реализации этого макроса - он может показаться сложным. Мы определим здесь значение флагов, реализацию которых обсудим несколько позже.

.set F_IMMED,0x80
.set F_HIDDEN,0x20
.set F_LENMASK,0x1f  # length mask

А вот и наш макрос defword:

    .set link,0   #  Store the chain of links.
.macro defword name, namelen, flags=0, label
    .section .rodata
    .align 4
    .globl name_\label
name_\label :
    .int link               # link
    .set link,name_\label
    .byte \flags+\namelen   # flags + байт длины
    .ascii "\name"          # имя
    .align 4                # выравнивание на 4-х байтовую границу
    .globl \label
\label :
    .int DOCOL              # codeword - указатель на функцию-интепретатор
    # дальше будут идти указатели на слова
.endm

Таким образом я хочу писать слова, написанные на ассемблере. Мы должны написать некоторое количество базового кода, прежде чем будет достаточно "инфраструктуры", чтобы начать писать слова на Forth, но также я хочу определить некоторые общие слова на ассемблере для скорости, хотя я мог бы написать их на Forth.

Вот как DUP выглядит в памяти:

forth-interpret-10.png

Опять же, для краткости я собираюсь написать макрос ассемблера с именем defcode. Как и в случае с defword выше, не беспокойтесь о сложных деталях макроса.

.macro defcode name, namelen, flags=0, label
    .section .rodata
    .align 4
    .globl name_\label
name_\label :
    .int    link               # link
    .set    link,name_\label
    .byte   \flags+\namelen    # flags + байт длины
    .ascii  "\name"            # имя
    .align  4                  # выравнивание на 4-х байтовую границу
    .globl  \label
\label :
    .int    code_\label        # codeword
    .text
    //.align 4
    .globl  code_\label
code_\label :              # далее следует ассемблерный код
.endm

Теперь несколько простых примитивов Forth. Они написаны на ассемблере для скорости. Если вы понимаете язык ассемблера i386, то стоит их прочитать.

defcode "DROP",4,,DROP
    popl    %eax            # сбросить верхний элемент стека
    NEXT

defcode "SWAP",4,,SWAP
    popl    %eax            # поменять местами два верхних элемента на стеке
    popl    %ebx
    pushl   %eax
    pushl   %ebx
    NEXT

defcode "DUP",3,,DUP
    mov     (%esp), %eax    # дублировать верхний элемент стека
    pushl   %eax
    NEXT

defcode "OVER",4,,OVER
    mov     4(%esp), %eax   # взять второй от верха элемент стека
    pushl   %eax            # и положить его копию сверху
    NEXT

defcode "ROT",3,,ROT
    popl    %eax
    popl    %ebx
    popl    %ecx
    pushl   %ebx
    pushl   %eax
    pushl   %ecx
    NEXT

defcode "-ROT",4,,NROT
    popl    %eax
    popl    %ebx
    popl    %ecx
    pushl   %eax
    pushl   %ecx
    pushl   %ebx
    NEXT

defcode "2DROP",5,,TWODROP
    popl    %eax            # сбросить два верхних элемента со стека
    popl    %eax
    NEXT

defcode "2DUP",4,,TWODUP
    movl    (%esp), %eax    # дублировать два верхних элемента на стеке
    movl    4(%esp), %ebx
    pushl   %ebx
    pushl   %eax
    NEXT

defcode "2SWAP",5,,TWOSWAP
    popl    %eax            # поменять местами две пары элементов на стеке
    popl    %ebx
    popl    %ecx
    popl    %edx
    pushl   %ebx
    pushl   %eax
    pushl   %edx
    pushl   %ecx
    NEXT

defcode "?DUP",4,,QDUP
    movl    (%esp), %eax    # дублировать верхний элемент стека если он не нулевой
    test    %eax, %eax
    jz      1f
    pushl   %eax
1:
    NEXT

defcode "1+",2,,INCR
    incl    (%esp)          # увеличить верхний элемент стека на единицу
    NEXT

defcode "1-",2,,DECR
    decl    (%esp)          # уменьшить верхний элемент стека на единицу
    NEXT

defcode "4+",2,,INCR4
    addl    $4, (%esp)      # увеличить верхний элемент стека на 4
    NEXT

defcode "4-",2,,DECR4
    subl    $4, (%esp)      # уменьшить верхний элемент стека на 4
    NEXT

defcode "+",1,,ADD
    popl    %eax            # взять верхний элемент со стека
    addl    %eax, (%esp)    # прибавиь его значение к элементу, который стал верхним
    NEXT

defcode "-",1,,SUB
    popl    %eax            # взять верхний элемент со стека
    subl    %eax, (%esp)    # вычесть его значение из элемента, который стал верхним верхним
    NEXT

defcode "*",1,,MUL
    popl    %eax            # взять со стека верхний элемент
    popl    %ebx            # взять со стека следующий верхний элемент
    imull   %ebx, %eax      # умножить их друг на друга
    pushl   %eax            # игнорируем переполнение
    NEXT

В этом Forth только /MOD примитив. Позже мы определим слова / и MOD в терминах примитива /MOD. Конструкция ассемблерной команды idiv, которая оставляет как частное, так и остаток, делает этот выбор очевидным.

defcode "/MOD",4,,DIVMOD
    pop     %ebx
    pop     %eax
    cdq
    idivl   %ebx
    pushl   %edx            # push остаток
    pushl   %eax            # push частное
    NEXT

defcode "U/MOD",5,,UDIVMOD
    xor %edx, %edx
    pop %ebx
    pop %eax
    divl %ebx
    push %edx               # push остаток
    push %eax               # push частное
    NEXT

Множество сравнительных операций, таких как =, <, >, и.т.д

Стандарт ANSI Forth говорит, что слова сравнения должны возвращать все двоичные разряды равные единице для TRUE, и все двоичные разряды равные нулю для FALSE. Однако это немного странное соглашение, поэтому этот Forth не следует ему и возвращает более нормальное (для программистов на Си) значение 1 для TRUE и 0 для FALSE.

Причиной этого соглашения является то, что слова AND, OR, XOR и INVERT могут функционировать как логические операторы, так и как побитовые операторы. Для сравнения, с соглашением Си, что FALSE = 0 и TRUE = 1, вам нужны два набора операторов: && и &, || and |, и.т.д.

Ну и кто тут теперь страннее?

defcode "=",1,,EQU
    popl    %eax            # два верхних элемента стека равны?
    popl    %ebx
    cmpl    %ebx, %eax
    sete    %al
    movzbl  %al, %eax
    pushl   %eax
    NEXT

defcode "<>",2,,NEQU
    popl    %eax            # два верхних элемента стека не равны?
    popl    %ebx
    cmpl    %ebx, %eax
    setne   %al
    movzbl  %al, %eax
    pushl   %eax
    NEXT

defcode "<",1,,LT
    popl    %eax
    popl    %ebx
    cmpl    %eax, %ebx
    setl    %al
    movzbl  %al, %eax
    pushl   %eax
    NEXT

defcode ">",1,,GT
    popl    %eax
    popl    %ebx
    cmpl    %eax, %ebx
    setg    %al
    movzbl  %al, %eax
    pushl   %eax
    NEXT

defcode "<=",2,,LE
    popl    %eax
    popl    %ebx
    cmpl    %eax, %ebx
    setle   %al
    movzbl  %al, %eax
    pushl   %eax
    NEXT

defcode ">=",2,,GE
    popl    %eax
    popl    %ebx
    cmpl    %eax, %ebx
    setge   %al
    movzbl  %al, %eax
    pushl   %eax
    NEXT

defcode "0=",2,,ZEQU
    popl    %eax            # верхний элемент стека равен нулю?
    test    %eax, %eax
    setz    %al
    movzbl  %al, %eax
    pushl   %eax
    NEXT

defcode "0<>",3,,ZNEQU
    popl    %eax            # верхний элемент стека не равен нулю?
    testl   %eax, %eax
    setnz   %al
    movzbl  %al, %eax
    pushl   %eax
    NEXT

defcode "0<",2,,ZLT
    popl    %eax            # comparisons with 0
    test    %eax, %eax
    setl    %al
    movzbl  %al, %eax
    pushl   %eax
    NEXT

defcode "0>",2,,ZGT
    popl    %eax
    testl   %eax, %eax
    setg    %al
    movzbl  %al, %eax
    pushl   %eax
    NEXT

defcode "0<=",3,,ZLE
    popl    %eax
    testl   %eax, %eax
    setle   %al
    movzbl  %al, %eax
    pushl   %eax
    NEXT

defcode "0>=",3,,ZGE
    popl    %eax
    test    %eax, %eax
    setge   %al
    movzbl  %al, %eax
    pushl   %eax
    NEXT

defcode "AND",3,,AND
    popl    %eax            # битовый AND
    andl    %eax, (%esp)
    NEXT

defcode "OR",2,,OR
    popl    %eax            # битовый OR
    orl     %eax, (%esp)
    NEXT

defcode "XOR",3,,XOR
    popl    %eax            # битовый XOR
    xorl    %eax, (%esp)
    NEXT

defcode "INVERT",6,,INVERT
    notl    (%esp)          # это битовая функция "NOT" (см. NEGATE and NOT)
    NEXT

EXIT - Возвращение из форт-слов

Время поговорить о том, что происходит, когда мы делаем EXIT. На этой диаграмме QUADRUPLE вызывает DOUBLE, и DOUBLE собирается сделать EXIT (посмотрите, куда указывает %esi)

forth-interpret-11.png

Что происходит, когда функция выполняет NEXT? Выполняется следующий код:

defcode "EXIT",4,,EXIT
    POPRSP  %esi            # Восстановить указатель из стека возвратов в %esi
    NEXT                    # Сделать NEXT

EXIT получает старый %esi, который мы сохранили ранее (когда выполняли DOCOL) в стеке возвратов, и помещает его в %esi. Итак, после этого (но до NEXT) мы получаем:

forth-interpret-12.png

И NEXT просто завершает работу, в этом случае, просто вызвав DOUBLE снова.

Литералы

Последний момент, который я "замалчивал" раньше, заключался в том, как иметь дело с функциями, которые делают что-либо помимо вызова других функций. Например, предположим, что DOUBLE был определен следующим образом:

: DOUBLE 2 * ;

Он делает то же самое, но как мы его скомпилируем, если он содержит буквально цифру 2? Одним из способов было бы иметь функцию под названием 2 (которую вы должны были бы написать на ассемблере), но вам понадобится такая функция для каждого отдельного литерала, который вы бы хотели использовать.

Forth решает это, компилируя функцию, используя специальное слово LIT:

forth-interpret-13.png

LIT выполняется обычным способом, но то, что он делает дальше, определенно не нормально. Он смотрит на %esi (который теперь указывает на число 2), берет это число и кладет его в стек, а затем манипулирует %esi, чтобы пропустить число 2, как если бы его никогда не было.

Что интересно, так это то, что весь захват и манипуляция может быть выполнена с использованием одной байтовой команды i386, нашего старого друга LODSL. Вместо того, чтобы рисовать диаграммы, посмотрите, можете ли вы узнать, как работает LIT:

defcode "LIT",3,,LIT
    # %esi указывает на следующую команду, но в этом случае это указатель на следующий
    # литерал, представляющий собой 4 байтовое значение. Получение этого литерала в %eax
    # и инкремент %esi на x86 -  это удобная однобайтовая инструкция! (см. NEXT macro)
    lodsl
    # push literal в стек
    push %eax
    NEXT

Память

Важным моментом в Forth является то, что он дает вам прямой доступ к самым низкоуровневым деталям виртуальной машины. Манипулирование памятью часто осуществляется в Forth, и вот примитивы для этого:

defcode "!",1,,STORE
    popl    %ebx            # забираем со стека адрес, куда будем сохранять
    popl    %eax            # забираем со стека данные, которые будем сохранять
    movl    %eax, (%ebx)    # сохраняем данные по адресу
    NEXT

defcode "@",1,,FETCH
    popl    %ebx            # забираем со стека адрес переменной, значение которой надо вернуть
    movl    (%ebx), %eax    # выясняем значение по этому адресу
    pushl   %eax            # push-им значение в стек
    NEXT

defcode "+!",2,,ADDSTORE
    popl    %ebx            # забираем со стека адрес переменной, которую будем увеличивать
    popl    %eax            # забираем значение на которое будем увеличивать
    addl    %eax, (%ebx)    # добавляем значение к переменной по этому адресу
    NEXT

defcode "-!",2,,SUBSTORE
    popl    %ebx            # забираем со стека адрес переменной, которую будем уменьшать
    popl    %eax            # забираем значение на которое будем уменьшать
    subl    %eax, (%ebx)    # вычитаем значение из переменной по этому адресу
    NEXT

! и @ (STORE и FETCH) работают с 32-битными словами. Также полезно иметь возможность читать и писать байты, поэтому мы также определяем стандартные слова C@ и C!. Байт-ориентированные операции работают только на архитектуре, которая их разрешает (i386 является одной из них).

defcode "C!",2,,STOREBYTE
    popl    %ebx            # забираем со стека адрес, куда будем сохранять
    popl    %eax            # забираем со стека данные, которые будем сохранять
    movb    %al, (%ebx)     # сохраняем данные по адресу
    NEXT

defcode "C@",2,,FETCHBYTE
    popl    %ebx            # забираем со стека адрес переменной, значение которой надо вернуть
    xorl    %eax, %eax      # очищаем регистр %eax
    movb    (%ebx), %al     # выясняем значение по этому адресу
    push    %eax            # push-им значение в стек
    NEXT

# C@C! - это полезный примитив для копирования байт
defcode "C@C!",4,,CCOPY
    movl    4(%esp), %ebx   # адрес источника
    movb    (%ebx), %al     # получаем байт из источника
    popl    %edi            # адрес приемника
    stosb                   # копируем байт в приемник
    push    %edi            # увеличиваем адрес приемника
    incl    4(%esp)         # увеличиваем адрес источника
    NEXT

# CMOVE - операция копирования блока байтов
defcode "CMOVE",5,,CMOVE
    movl    %esi, %edx      # сохраним %esi
    popl    %ecx            # length
    popl    %edi            # адрес приемника
    popl    %esi            # адрес источника
    rep     movsb           # копируем источник в приемник length раз
    movl    %edx, %esi      # восстанавливаем %esi
    NEXT

Встроенные переменные

Это некоторые встроенные переменные и соответствующие стандартные слова Forth. Из них единственное, что мы обсуждали до сих пор, было LATEST, указывающее на последнее определенное в словаре Forth слово. LATEST также является словом Forth, которое выталкивает адрес переменнуй LATEST в стек, поэтому вы можете читать или писать ее с помощью операторов @ и !. Например, чтобы напечатать текущее значение LATEST (и это применимо к любой переменной Forth), вы должны:

LATEST @ . CR

Чтобы уменьшить определение переменных, я использую макрос defvar, похожий на defword и defcode выше. (Фактически, defvar макрос использует defcode для создания заголовка записи в словаре).

.macro defvar name, namelen, flags=0, label, initial=0
    defcode \name,\namelen,\flags,\label
    push    $var_\name
    NEXT
    .data
    .align 4
    var_\name :
    .int \initial
.endm

Встроенные переменные:

  • STATE - состояние интерпретации (ноль) или компиляции слова (не ноль)
  • LATEST - указатель на последнее заданное слово в словаре.
  • HERE - указатель на следующий свободный байт памяти. При компиляции скомпилированные слова помещаются по этому указателю, а потом он передвигается дальше.
  • S0 - хранит адрес вершины стека параметров.
  • BASE - текущая база (radix) для печати и чтения чисел.
defvar "STATE",5,,STATE
defvar "HERE",4,,HERE
defvar "LATEST",6,,LATEST,name_SYSCALL0  # SYSCALL0 должен быть последним встроенным словом
defvar "S0",2,,SZ
defvar "BASE",4,,BASE,10

Встроенные константы

Встроенные константы:

  • VERSION - это текущая версия этого Forth.
  • R0 - адрес вершины стека возвратов.
  • DOCOL - Указатель на DOCOL.
  • F_IMMED - текущее значение флага IMMEDIATE.
  • F_HIDDEN - Текущее значение флага HIDDEN.
  • F_LENMASK - Маска длины в flags/len байте
  • SYS_* и числовые коды различных системных вызовов Linux (из <asm/unistd.h>)
.macro defconst name, namelen, flags=0, label, value
    defcode \name,\namelen,\flags,\label
    push $\value
    NEXT
.endm
.set JONES_VERSION,47

defconst "VERSION",7,,VERSION,JONES_VERSION
defconst "R0",2,,RZ,return_stack_top
defconst "DOCOL",5,,__DOCOL,DOCOL
defconst "F_IMMED",7,,__F_IMMED,F_IMMED
defconst "F_HIDDEN",8,,__F_HIDDEN,F_HIDDEN
defconst "F_LENMASK",9,,__F_LENMASK,F_LENMASK

.set sys_exit,1
.set sys_read,3
.set sys_write,4
.set sys_open,5
.set sys_close,6
.set sys_creat,8
.set sys_unlink,0xA
.set sys_lseek,0x13
.set sys_truncate,0x5C

.set stdin,2
.set stderr,2

defconst "SYS_EXIT",8,,SYS_EXIT,sys_exit
defconst "SYS_OPEN",8,,SYS_OPEN,sys_open
defconst "SYS_CLOSE",9,,SYS_CLOSE,sys_close
defconst "SYS_READ",8,,SYS_READ,sys_read
defconst "SYS_WRITE",9,,SYS_WRITE,sys_write
defconst "SYS_CREAT",9,,SYS_CREAT,sys_creat

defconst "O_RDONLY",8,,__O_RDONLY,0
defconst "O_WRONLY",8,,__O_WRONLY,1
defconst "O_RDWR",6,,__O_RDWR,2
defconst "O_CREAT",7,,__O_CREAT,0100
defconst "O_EXCL",6,,__O_EXCL,0200
defconst "O_TRUNC",7,,__O_TRUNC,01000
defconst "O_APPEND",8,,__O_APPEND,02000
defconst "O_NONBLOCK",10,,__O_NONBLOCK,04000

Стек возвратов

Эти слова позволяют получить доступ к стеку возвратов. Напомним, что регистр %ebp всегда указывает на вершину стека возвратов.

defcode ">R",2,,TOR
    popl    %eax            # pop со стека данных в %eax
    PUSHRSP %eax            # push %eax на стек возвратов
    NEXT

defcode "R>",2,,FROMR
    POPRSP  %eax            # pop со стека возвратов в %eax
    pushl   %eax            # push %eax на стек параметров
    NEXT

defcode "RSP@",4,,RSPFETCH
    pushl    %ebp
    NEXT

defcode "RSP!",4,,RSPSTORE
    popl    %ebp
    NEXT

defcode "RDROP",5,,RDROP
    addl    $4, %ebp
    NEXT

Стек данных

Эти функции позволяют вам управлять стеком параметров. Напомним, что Linux устанавливает для нас стек параметров, и он доступен через регистр %esp.

defcode "DSP@",4,,DSPFETCH
    mov     %esp, %eax
    push    %eax
    NEXT

defcode "DSP!",4,,DSPSTORE
    popl    %esp
    NEXT

Ввод и вывод: KEY EMIT WORD NUMBER

Это наши первые действительно сложные примитивы Forth. Я решил написать их на ассемблере, но удивительно, что в реальных реализациях Forth они часто пишутся в терминах более фундаментальных примитивов Forth.

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

Давайте сначала обсудим ввод.

Слово KEY считывает следующий байт из stdin (и push-ит его на стек параметров). Поэтому, если KEY вызывается, и кто-то нажимает на клавишу пробела, то число 32 (ASCII-код пробела) помещается в стек.

В Forth нет различий между чтением кода и чтением ввода. Мы могли бы читать и компилировать код, мы могли бы читать слова для выполнения, мы могли бы попросить пользователя набрать свое имя - в конечном итоге все это происходит через KEY.

Реализация KEY использует входной буфер определенного размера (определенный в конце этого файла). Он вызывает системный вызов Linux read(2) для заполнения этого буфера и отслеживает его положение в буфере с помощью пары переменных, и если у него заканчивается входной буфер, он автоматически заполняет его. Если KEY обнаруживает, что stdin закрыт, он выходит из программы, поэтому, когда вы нажимаете ^D, система Forth завершается.

forth-interpret-14.png

    defcode "KEY",3,,KEY
    call _KEY
    push    %eax            #       # push-им возвращенный символ на стек
    NEXT                    #
_KEY:                       # <--+
    mov     (currkey), %ebx #    |  # Берем указатель currkey в %ebx
    cmp     (bufftop), %ebx #    |  # (bufftop >= currkey)? - в буфере есть символы?
    jge     1f              #-+  |  # ?-Да, переходим вперед
    xor     %eax, %eax      # |  |  # ?-Нет, (1) переносим символ, на который
    mov     (%ebx), %al     # |  |  #        указывает bufftop в %eax,
    inc     %ebx            # |  |  #        (2) инкрементируем копию bufftop
    mov     %ebx, (currkey) # |  |  #        (3) записываем ее в переменную currkey,
    ret                     # |  |  #        и выходим (в %eax лежит символ)
    # ---------------- RET    |  |
1:  #                     <---+  |  # Буфер ввода пуст, сделаем read из stdin
    mov     $sys_read, %eax #    |  # param1: SYSCALL #3 (read)
    mov     $stdin, %ebx    #    |  # param2: Дескриптор #2 (stdin)
    mov     $input_buffer, %ecx #|  # param3: Кладем адрес буфера ввода в %ecx
    mov     %ecx, currkey   #    |  # Сохраняем адрес буфера ввода в currkey
    mov     $INPUT_BUFFER_SIZE, %edx # Максимальная длина ввода
    int     $0x80           #    |  # SYSCALL
    # Проверяем возвращенное     |  # должно быть количество символов + '\n'
    test    %eax, %eax      #    |  # (%eax <= 0)?
    jbe     2f              #-+  |  # ?-Да, это ошибка, переходим вперед
    addl    %eax, %ecx      # |  |  # ?-Нет, (1) добавляем в %ecx кол-во прочитанных байт
    mov     %ecx, bufftop   # |  |  #        (2) записываем %ecx в bufftop
    jmp     _KEY            # |  |
    # ------------------------|--+
2:  #                     <---+     # Ошибка или конец потока ввода - выходим
    mov     $sys_exit, %eax         # param1: SYSCALL #1 (exit)
    xor     %ebx, %ebx              # param2: код возврата
    int     $0x80                   # SYSCALL
    # --------------- EXIT
    .data
    .align 4
currkey:
    # Хранит смещение на текущее положение в буфере ввода (следующий символ будет прочитан по нему)
    .int input_buffer
bufftop:
    # Хранит вершину буфера ввода (последние валидные данные + 1)
    .int input_buffer

Выход намного проще. Слово EMIT выводит один байт в stdout. Эта реализация просто использует системный вызов write. Никакой попытки сделать буфер не производится, но было бы хорошим упражнением добавить его.

defcode "EMIT",4,,EMIT
    popl    %eax
    call    _EMIT
    NEXT
_EMIT:
    movl    $1, %ebx            # param1: stdout
    mov     %al, emit_scratch   # берем байт и заносим его в emit_scratch
    mov     $emit_scratch, %ecx # param2: адрес выводимого значения
    mov     $1, %edx            # param3: длина
    mov     $sys_write, %eax    # SYSCALL #4 (write)
    int     $0x80
    ret

    .data           # NB: проще записать в .data section
emit_scratch:
    .space 1        # Место для байта, который выводит EMIT

Вернемся к вводу. WORD - это слово , которое читает следующее полное слово со стандартного ввода. Если подробнее, оно сначала пропускает любые пробельные символы (пробелы, табуляции, символы новой строки и.т.д.). Затем оно вызывает KEY, чтобы читать символы в буфере ввода, пока не наткнется на пробел. Затем оно вычисляет длину прочитанного слова и возвращает адрес и длину как два слова в стеке (при этом длина сверху).

Обратите внимание, что WORD имеет единственный внутренний буфер, который он перезаписывает каждый раз (как статическая строка в Си). Также обратите внимание, что внутренний буфер WORD составляет всего 32 байта, и нет проверки на переполнение. 31 байт - это максимальная длина слова Forth, которую мы поддерживаем, и это то, для чего WORD и используется: чтения слов Forth при компиляции и выполнении кода. Возвращенные строки НЕ заканчиваются NUL.

Начальный адрес и длина строки - это обычный способ представления строк в Forth (не заканчивающийся символом ASCII NULL, как в C), и поэтому строки Forth могут содержать любой символ, включая NUL, и могут быть любой длины.

WORD не подходит для простого считывания строк (например, пользовательского ввода) из-за всех вышеперечисленных особенностей и ограничений.

Обратите внимание, что при выполнении в немедленном режиме вы увидите:

WORD FOO

который помещает FOO и длину 3 в стек, но при компиляции:

: BAR WORD FOO ;

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

    defcode "WORD",4,,WORD
    call    _WORD
    push    %edi            # push base address
    push    %ecx            # push length
    NEXT
_WORD:
    # Ищем первый непробельный символ, пропуская комменты, начинающиеся с обратного слэша
1:                      # <---+
    call    _KEY            # |     # Получаем следующую букву, возвращаемую в %eax
    cmpb    $'\\', %al      # |     # (Это начало комментария)?
    je      3f              #-|---+ # ?-Да, переходим вперед
    cmpb    $' ', %al       # |   | # ?-Нет. (Это пробел, возрат каретки, перевод строки)?
    jbe     1b              #-+   | # ?-Да, переходим назад
    #                             |
    # Ищем конец слова, сохраняя символы по мере продвижения
    mov     $word_buffer, %edi  # | # Указатель на возвращаемый буфер
2:                      # <---+   |
    stosb                   # |   | # Добавляем символ в возвращаемый буфер
    call    _KEY            # |   | # Вызываем KEY символ будет возвращен в %al
    cmpb    $' ', %al       # |   | # (Это пробел, возрат каретки, перевод строки)?
    ja      2b              #-+   | # Если нет, повторим
    #                       #     |
    # Вернем слово (указатель на статический буфер черех %ecx) и его длину (через %edi)
    sub     $word_buffer, %edi  # |
    mov     %edi, %ecx      #     | # return: длина слова
    mov     $word_buffer, %edi  # | # return: адрес буфера
    ret                     #     |
    # ----------------- RET       |
    #                             |
    # Это комментарий, пропускаем | его до конца строки
3:                      # <---+ <-+
    call    _KEY            # |
    cmpb    $'\n', %al      # |     # KEY вернул конец строки?
    jne     3b              #-+     # Нет, повторим
    jmp     1b              #
    # ---------------- to 1

    .data
    # Статический буфер, в котором возвращается WORD.
    # Последующие вызовы перезаписывают этот буфер.
    # Максимальная длина слова - 32 символа.
word_buffer:
    .space 32

Помимо чтения слов, нам нужно будет читать цифры, и для этого мы используем функцию NUMBER. Она анализирует числовую строку, например, возвращаемую WORD, и push-ит число в стек.

эта функция использует переменную BASE в качестве базы (radix) для преобразования, поэтому, например, если BASE равна 2, мы ожидаем двоичное число. Обычно BASE составляет 10

Если слово начинается с символа '-', тогда возвращаемое значение отрицательно.

Если строка не может быть проанализирована как число (или содержит символы за пределами текущей BASE), тогда нам нужно вернуть индикацию ошибки. Таким образом, NUMBER фактически возвращает два элемента в стеке. В верхней части стека он возвращает количество неразобранных символов (т.е. если 0, то все символы были разобраны, поэтому нет ошибки). Второй элемент от вершины стека - это распарсенное число (или частичное значение, если произошла ошибка).

defcode "NUMBER",6,,NUMBER
    pop     %ecx            # length of string
    pop     %edi            # start address of string
    call    _NUMBER
    push    %eax            # parsed number
    push    %ecx            # number of unparsed characters (0 = no error)
    NEXT

_NUMBER:
    xor     %eax, %eax
    xor     %ebx, %ebx
    # Попытка распарсить пустую строку это ошибка но мы возвращаем 0
    test    %ecx, %ecx
    jz  5f                  #-> RET #
    # Строка не пуста, будем разбирать
    movl    var_BASE, %edx  #       # Получаем BASE в %dl
    # Проверим, может быть первый символ '-'?
    movb    (%edi), %bl     #       # %bl = первый символ строки
    inc     %edi            #       #
    push    %eax            #       # push 0 в стек
    cmpb    $'-', %bl       #       # (Отрицательное число)?
    jnz 2f                  #-+     # ?-Нет, переходим к конвертации (2)
    pop     %eax            # |     # ?-Да, заберем обратно 0 из стека,
    push    %ebx            # |     #       push не ноль в стек, как индикатор отрицательного
    dec     %ecx            # |     #       уменьшим счетчик оставшихся символов
    jnz 1f                  #-----+ #       (Строка закончилась)? ?-Нет: переход на (1)
    pop     %ebx            # |   | #       ?-Да - это ошибка, строка "-". Забираем из стека
    movl    $1, %ecx        # |   | #            помещаем в возвращаемую нераспарсенную длину
    ret                     # |   | #            единицу и выходим.
    # --------------------- # |   | # -------------------------------------------------------
    # Цикл чтения чисел     # |   | #
1:  #                    <========+ #
    imull   %edx, %eax      # |   | # %eax *= BASE
    movb    (%edi), %bl     # |   | # %bl = следующий символ в строке
    inc     %edi            # |   | # Увеличиваем указатель
2:  #                    <----+   | #
    # Преобразуем 0-9, A-Z в числа 0-35.
    subb    $'0', %bl       #     | # (< '0')?
    jb  4f                  #---+ | # ?-Да, херня какая-то, а не цифра, ошибка, идем на (4)
    cmp     $10, %bl        #   | | # ?-Нет, (<= '9')?
    jb  3f                  #-+ | | #        ?-Да, идем на (3), это число между 0 и 9
    subb    $17, %bl        # | | | #        ?-Нет, (< 'A')? потому что (17 = 'A'-'0')
    jb  4f                  #---+ | #               ?-Да, это ошибка, идем на (4)
    addb    $10, %bl        # | | | #               ?-Нет, добавляем к значению 10
3:  #                     <---+ | | #
    cmp     %dl, %bl        #   | | #                      (RESULT >= BASE)?
    jge 4f                  #---+ | #                      ?-Да, перебор, идем на (4)
    add     %ebx, %eax      #   | | #                      ?-Нет, все в порядке. Добавляем
    dec     %ecx            #   | | #                        RESULT к %eax и LOOP-им дальше.
    jnz 1b                  #---|-+ #
4:  #                     <-----+   #
    # Тут мы оказываемся если цикл закончился - тогда у нас %ecx=0
    # В ином случае %ecx содержит количество нераспарсенных символов
    # Если у нас отрицательный результат, то первый символ '-' (сохранен в стеке)
    pop     %ebx            #       #
    test    %ebx, %ebx      #       # (Отрицательное число)?
    jz  5f                  #-+     # ?-Нет, возвращаем как есть (5)
    neg     %eax            # |     # ?-Да, инвертируем
5:  #                     <---+
    ret

Просмотр словаря

Мы подходим к нашей прелюдии о том, как компилируется код Forth, но сначала нам нужно еще немного инфраструктуры.

Слово FIND принимает строку (слово, которое анализируется WORD - см. выше) и находит его его в словаре. Фактически он возвращает адрес заголовка словаря для найденного слова. Если слово не найдено, он возвращает 0

Поэтому, если DOUBLE определен в словаре, тогда

WORD DOUBLE FIND

возвращает следующий указатель:

forth-interpret-15.png

См. также >CFA и >DFA.

FIND не находит словарные записи, помеченные как HIDDEN. См. ниже, почему.

    defcode "FIND",4,,FIND
    pop     %ecx            # %ecx = length
    pop     %edi            # %edi = address
    call    _FIND
    push    %eax            # %eax = address of dictionary entry (or NULL)
    NEXT
_FIND:
    push    %esi            # Сохраним %esi - так мы сможем его использовать
                            # для сравнения строк командой cmpsb
    # Здесь мы начинаем искать в словаре это слово от конца к началу словаря
    mov     var_LATEST, %edx # %edx = LATEST указыввет на name header
                             # последнего слова в словаре
1:  #                   <------------+
    test    %edx, %edx      # (NULL указатель, т.е. словарь кончился)?
    je  4f                  #----+   |  # ?-Да, переходим вперед к (4)
    #                             |  |
    # Сравним ожидаемую длину и длину слова
    # Внимание, если F_HIDDEN установлен для этого слова, то она не совпадет.
    xor     %eax, %eax      #     |  |  #
    movb    4(%edx), %al    #     |  |  # %al = flags+length
    andb    $(F_HIDDEN|F_LENMASK), %al  # %al = длина имени
    cmpb    %cl, %al        #     |  |  # (Длины одинаковые?)
    jne 2f                  #--+  |  |  # ?-Нет, переходим вперед к (2)
    #                          |  |  |
    # Переходим к детальному сравнению
    push    %ecx            #  |  |  |  # Сохраним длину
    push    %edi            #  |  |  |  # Сохраним адрес, потому что repe cmpsb двигает %edi
    lea     5(%edx), %esi   #  |  |  |  # Загружаем в %esi адрес начала слова
    repe    cmpsb           #  |  |  |  # Сравниваем
    pop     %edi            #  |  |  |  # Восстанавливаем адрес
    pop     %ecx            #  |  |  |  # Восстановим длину
    jne 2f                  #--+  |  |  # ?-Если не равны - переходим вперед к (2)
    #                          |  |  |
    # Строки равны - возвратим указатель на заголовок в %eax
    pop     %esi            #  |  |  |  # Восстановим %esi
    mov     %edx, %eax      #  |  |  |  # %edx все еще содержит возвращаемый указатель
    ret                     #  |  |  |  # Возврат
    # ----------------- RET    |  |  |
2:  #                     <----+  |  |
    mov     (%edx), %edx    #     |  |  # Переходим по указателю к следующему слову
    jmp 1b                  #     |  |  # И зацикливаемся
    # ----------------------------|--+
4:  #                     <-------+
    # Слово не найдено
    pop     %esi
    xor     %eax, %eax      # Возвратим ноль в #eax
    ret                     # Возврат

FIND возвращает указатель словаря, но при компиляции нам нужен указатель кодового слова (напомним, что определения Forth скомпилированы в списки указателей на codeword-ы). Стандартное слово >CFA превращает указатель словаря в указатель на codeword.

В приведенном ниже примере показан результат:

WORD DOUBLE FIND >CFA

forth-interpret-16.png

NB: поскольку имена различаются по длине, это не просто простое приращение.

В этом Forth вы не можете легко превратить указатель кодового слова обратно в указатель на элемент словаря, но это не так для большинства реализаций Forth, где они хранят обратный указатель в определении (с очевидной стоимостью по памяти/сложности).

Причина, по которой они делают это, заключается в том, что это бывает полезно, чтобы быстро декомпилировать определения Forth.

Что означает CFA? Мое лучшее предположение - "Code Field Address"

    defcode ">CFA",4,,TCFA
    pop     %edi
    call    _TCFA
    push    %edi
    NEXT
_TCFA:
    xor     %eax, %eax
    add     $4, %edi        # Пропускаем LINK - указатель на предыдущее слово
    movb    (%edi), %al     # Загружаем flags+len в %al
    inc     %edi            # Пропускаем flags+len байт
    andb    $F_LENMASK, %al # Маскируем, чтобы получить длину имени, без флагов
    add     %eax, %edi      # Пропускаем имя
    addl    $3, %edi        # Учитываем выравнивание
    andl    $~3, %edi
    ret

В связи с >CFA рассмотрим >DFA, который берет адрес записи словаря, возвращаемый FIND, и возвращает указатель на первое поле данных.

forth-interpret-17.png

(Обратите внимание на этот момент, кто знаком с исходным кодом FIG-Forth / ciforth: Это >DFA определение отличается от их, потому что у них есть дополнительная косвенность).

Как легко можно увидеть >DFA легко определяется в Forth, просто путем добавления 4 к результату >CFA.

defword ">DFA",4,,TDFA
    .int TCFA       # >CFA     (получаем code field address)
    .int INCR4      # 4+       (добавляем 4, чтобы получить адрес первого слова в опредении)
    .int EXIT       # EXIT     (возвращаемся)

Компиляция

Теперь мы поговорим о том, как Forth компилирует слова. Напомним, что определение слова выглядит следующим образом:

: DOUBLE DUP + ;

и мы должны превратить это в:

forth-interpret-18.png

Теперь нам нужно решить несколько задач:

  • Куда поместить новое слово?
  • Как мы читаем слова?
  • Как мы определяем слова : (COLON) и ; (SEMICOLON)?

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

Forth имеет функцию INTERPRET (настоящий интерпретатор на этот раз, а не DOCOL), которая работает в цикле, читая слова (используя WORD), просматривая их (используя FIND), превращая их в указатели кодового слова (используя >CFA) и решая, что с ними делать.

То, что он делает, зависит от режима интерпретатора (в переменной STATE).

Когда STATE равно нулю, интерпретатор просто запускает каждое слово, как только находит его. Это называется "немедленным режимом" (immediate mode).

Интересные вещи происходят, когда STATE не равен нулю - в "режим компиляции" (compiling mode). В этом режиме интерпретатор добавляет указатель codeword в пользовательскую память (переменная HERE указывает на следующий свободный байт пользовательской памяти).

Таким образом, вы сможете увидеть, как мы можем определить : (COLON). Общий план:

  • (1) Использовать WORD для чтения имени определяемой функции.
  • (2) Построить запись словаря - только заголовочную часть - в пользовательской памяти:

forth-interpret-19.png

  • (3) Установить LATEST, чтобы указать на новое слово, …
  • (4) .. и самое главное установить HERE, чтобы он указывал сразу после нового codeword. Здесь интерпретатор будет добавлять кодовые слова.
  • (5) Установить STATE в 1. Это вызовет переход в режим компиляции, поэтому интерпретатор начинает добавлять кодовые слова к нашему частично сформированному заголовку.

После того, как : запущен, наш вход находится здесь:

forth-interpret-20.png

поэтому интерпретатор (теперь он находится в режиме компиляции, поэтому его можно считать компилятором) читает "DUP", находит его в словаре, получает его указатель на codeword и добавляет его.

forth-interpret-21.png

Затем мы читаем +, получаем указатель его codeword и добавляем его:

forth-interpret-22.png

Теперь проблема заключается в следующем. Очевидно, что мы не хотим, чтобы мы читали ; скомпилировали его и продолжали компилировать все подряд.

На этом этапе Forth использует трюк. Помните, что длина байта в определении словаря не просто байт длины, но также может содержать флаги. Один флаг называется флагом IMMEDIATE (F_IMMED в этом коде). Если слово в словаре помечено как IMMEDIATE, тогда интерпретатор запускает его немедленно даже если он находится в режиме компиляции.

Вот как это слово ; (SEMICOLON) работает - как слово, помеченное в словаре как IMMEDIATE.

Все, что оно делает, - это добавляет кодовое слово для EXIT в текущее определение и возвращает к немедленному режиму (установкой STATE на 0). Вскоре мы увидим его фактическое определение; и мы увидим, что это действительно очень простое определение, объявленное IMMEDIATE.

После чтения интерпретатором ; и выполнения его "немедленно", мы получаем это:

forth-interpret-23.png

и STATE установлена в 0;

И это вся работа, наше новое определение скомпилировано, и мы вернулись в непосредственный режим, простых чтений и выполнений слов, возможно, включая вызов, чтобы проверить наше новое слово DOUBLE.

Единственная последняя заминка в том, что, хотя пока слово компилируется, оно было в полуготовом состоянии. Мы, разумеется, не хотели бы, чтобы DOUBLE был вызван кем-то в это время. Есть несколько способов сделать это это, но в Forth мы устанавливаем байт длины слова с флагом HIDDEN (F_HIDDEN в этом коде) во время его компиляции. Это предотвращает обнаружение компилируемого слова с помощью FIND и, таким образом, теоретически предотвращает любой шанс его вызова.

Вышеприведенное объясняет, как компилировать : (COLON) и ; (SEMICOLON), и через некоторое время я их определю. Функция: (COLON) может быть сделана немного более общей, если написать ее в двух частях. Первая часть, называемая CREATE, создает только заголовок:

forth-interpret-24.png

и вторая часть, фактическое определение : (COLON), вызывает CREATE и добавляет кодовое слово DOCOL:

forth-interpret-25.png

CREATE является стандартным словом Forth, и преимущество этого разделения состоит в том, что мы можем его повторно использовать для создания других типов слов (а не только тех, которые содержат код, но например и таких, которые содержат переменные, константы и другие данные).

defcode "CREATE",6,,CREATE

    # Получаем length и address имени
    pop     %ecx            # %ecx = length
    pop     %ebx            # %ebx = address

    # Формируем указатель LINK
    movl    var_HERE, %edi  # %edi теперь адрес заголовка
    movl    var_LATEST, %eax    # Получаем значение указателя LINK
    stosl                   # и сохраняем его в заголовок

    # Байт длины и с само слово
    mov     %cl,%al         # Получаем длину
    stosb                   # Сохраняем length/flags байт.
    push    %esi            # Ненадолго сохраним %esi
    mov     %ebx, %esi      # %esi теперь указывает на имя слова
    rep     movsb           # Копируем имя слова
    pop     %esi            # Восстановим %esi
    addl    $3, %edi        # Вычислим выравнивание
    andl    $~3, %edi

    # Обновим LATEST и HERE.
    movl    var_HERE, %eax
    movl    %eax, var_LATEST
    movl    %edi, var_HERE
    NEXT

Поскольку я хочу определить : (COLON) в Forth, а не в ассемблере, нам нужно еще несколько слов Forth.

Первый - это  ,  (COMMA), который является стандартным словом Forth, которое добавляет 32-битное целое к пользовательской памяти, на которое указывает HERE, а потом добавляет 4 к HERE. Таким образом, действие  ,  (COMMA) выглядит так:

forth-interpret-26.png

DATA - любое 32-битное значение, которое лежит на вершине стека

 ,  (COMMA) является довольно фундаментальной операцией при компиляции. Оно используется для добавления codeword-ов в текущее слово, которое компилируется.

defcode ",",1,,COMMA
    pop     %eax        # Code pointer to store.
    call    _COMMA
    NEXT
_COMMA:
    movl    var_HERE, %edi  # HERE
    stosl                   # Store it.
    movl    %edi, var_HERE  # Update HERE (incremented)
    ret

Наши определения : (COLON) и ; (SEMICOLON) необходимо будет переключать в режим компиляции и из него.

Глобальная переменная STATE определяет текущий режим (немедленный или режим компиляции) и, изменяя эту переменную, мы можем переключаться между этими двумя режимами.

По различным причинам, которые проявятся позже, Forth определяет два стандартных слова, называемых [ и ] (LBRAC и RBRAC), которые переключают между этими режимами:

Слово Ассемблерное имя Действие Эффект
[ LBRAC STATE = 0 Переключение в немедленный режим.
] RBRAC STATE = 1 Переключение в режим компиляции.

[ (LBRAC) является НЕМЕДЛЕННЫМ (IMMEDIATE) словом. Причина такова: если бы это было не так и мы находились в режиме компиляции, и интерпретатор увидел [ - тогда он скомпилировал бы ее, а не выполнил бы ее. И мы никогда не смогли бы вернуться к немедленному режиму! Поэтому мы помечаем слово как IMMEDIATE, так что даже в режиме компиляции [ запускается в немедленном режиме, переключая нас обратно в немедленный режим.

defcode "[",1,F_IMMED,LBRAC
    xor     %eax, %eax
    movl    %eax, var_STATE     # Установить STATE в 0
    NEXT

defcode "]",1,,RBRAC
    movl    $1, var_STATE       # Установить STATE в 1
    NEXT

Теперь мы можем определить : (COLON), используя CREATE. Он просто вызывает CREATE, добавляет DOCOL (как codeword), устанавливает HIDDEN и переходит в режим компиляции.

defword ":",1,,COLON
    .int WORD               # Получаем имя нового слова
    .int CREATE             # CREATE заголовок записи словаря
    .int LIT, DOCOL, COMMA  # Добавляем DOCOL (как codeword).
    .int LATEST, FETCH, HIDDEN # Делаем слово скрытым (см. ниже определение HIDDEN).
    .int RBRAC              # Переходим в режим компиляции
    .int EXIT               # Возврат из функции

; (SEMICOLON) также элегантно прост. Обратите внимание на флаг F_IMMED.

defword ";",1,F_IMMED,SEMICOLON
    .int LIT, EXIT, COMMA   # Добавляем EXIT (так слово делает RETURN).
    .int LATEST, FETCH, HIDDEN # Переключаем HIDDEN flag  (см. ниже для определения).
    .int LBRAC              # Возвращаемся в IMMEDIATE режим.
    .int EXIT               # Возврат из функции

Расширение компилятора

Слова, помеченные IMMEDIATE (F_IMMED), предназначены не только для использования компилятором Forth. Вы также можете определить свои собственные IMMEDIATE-слова, и это важный аспект при расширении базового Forth, поскольку он позволяет фактически расширять сам компилятор. GCC позволяет вам это делать?

Стандартные слова Forth, такие как IF, WHILE,  ."  и.т.д., написаны как расширения базового компилятора, и все это IMMEDIATE-слова.

Слово IMMEDIATE переключает флаг F_IMMED (IMMEDIATE) на последнее определенное слово или на текущее слово, если вы вызываете его в середине определения.

Типичное использование:

: MYIMMEDWORD IMMEDIATE
    ...definition...
;

но некоторые Forth-программисты пишут это вместо этого:

: MYIMMEDWORD
    ...definition...
; IMMEDIATE

Эти два способа использования эквивалентны в первом приближении.

defcode "IMMEDIATE",9,F_IMMED,IMMEDIATE
    movl    var_LATEST, %edi    # LATEST слово в %edi.
    addl    $4, %edi            # Теперь %edi указывает на байт name/flags
    xorb    $F_IMMED, (%edi)    # Переключить the F_IMMED бит.
    NEXT

addr HIDDEN переключает HIDDEN флаг (F_HIDDEN) слова, определенного в addr. Чтобы скрыть последнее заданное слово (используемое выше в : и ; определениях), вы должны:

LATEST @ HIDDEN

HIDE word переключает флаг названного слова word.

Установка этого флага предотвращает нахождение слова с помощью FIND, поэтому его можно использовать для создания "private" слов. Например, чтобы разбить большое слово на более мелкие части, вы можете сделать:

: SUB1 ... subword ... ;
: SUB2 ... subword ... ;
: SUB3 ... subword ... ;
: MAIN ... defined in terms of SUB1, SUB2, SUB3 ... ;
HIDE SUB1
HIDE SUB2
HIDE SUB3

После этого только MAIN "экспортируется" и остается видимым для остальной части программы.

defcode "HIDDEN",6,,HIDDEN
    pop     %edi                # Указатель на слово в %edi
    addl    $4, %edi            # Теперь указывает на байт length/flags.
    xorb    $F_HIDDEN, (%edi)   # Переключаем HIDDEN бит.
    NEXT

defword "HIDE",4,,HIDE
    .int    WORD                # Получаем слово (ищущее за HIDE).
    .int    FIND                # Ищем его в словаре
    .int    HIDDEN              # Устанавливаем F_HIDDEN флаг.
    .int    EXIT                # Выходим

 '  (TICK) - это стандартное слово Forth, которое возвращает указатель codeword следующего слова.

Общее использование:

' FOO ,

это способ добавить codeword FOO к текущему слову, которое мы определяем (это работает только в компилируемом коде).

Вы, как правило, используете  '  в IMMEDIATE словах. Например, альтернативный (и довольно бесполезный) способ определения литерала 2 может быть:

: LIT2 IMMEDIATE
    ' LIT ,   \ Добавляет LIT к определяемому в настоящий момент слову
    2 ,       \ Добавляет число 2 к определяемому в настоящий момент слову
;

Таким образом, вы можете сделать:

: DOUBLE LIT2 * ;

(Если вы не понимаете, как работает LIT2, вы должны просмотреть материал о компиляции слов и немедленном режиме).

Это ассемблерное определение  '  использует чит, который я скопировал из buzzard92. В результате он работает только в скомпилированном коде. Можно написать версию  '  на основе WORD, FIND, >CFA, которая также работает в немедленном режиме.

defcode "'",1,,TICK
    lodsl                   # Получить адрес следующего слова и пропустить его
    pushl    %eax           # Push его в стек
    NEXT

Ветвление

Оказывается, все, что вам нужно для определения циклов, IF-выражений и.т.д. - это два примитива.

BRANCH - безусловная ветвь (эквивалентная команде безусловного перехода ассемблера). 0BRANCH - условная ветвь (переход будет осуществлен, если значение на вершине стека равно нулю).

Диаграмма ниже показывает, как BRANCH работает в некотором воображаемом скомпилированном слове. Когда BRANCH выполняется, %esi указывает на поле смещения (сравните с LIT выше):

forth-interpret-27.png

Смещение добавляется к %esi, чтобы сформировать новый %esi, и результатом является то, что при выполнении NEXT выполнение продолжается по целевому адресу ветки. Отрицательные смещения тоже работают, как ожидается.

0BRANCH - это то же самое, за исключением того, что ветвление происходит по условию.

Теперь стандартные Forth слова, такие как IF, THEN, ELSE, WHILE, REPEAT и т. Д., Могут быть полностью реализованы в Forth. Это НЕМЕДЛЕННЫЕ слова, которые добавляют различные комбинации BRANCH или 0BRANCH в слово, которое в настоящее время компилируется.

Например, код, написанный следующим образом:

condition-code IF true-part THEN rest-code

компилируется в:

forth-interpret-28.png

Вот определение:

defcode "BRANCH",6,,BRANCH
    add     (%esi),%esi     # добавить offset к instruction pointer
    NEXT

defcode "0BRANCH",7,,ZBRANCH
    pop     %eax
    test    %eax, %eax      # Вершина стека равна нулю?
    jz      code_BRANCH     # Если да, вернуться назад к функции BRANCH выше
    lodsl                   # иначе пропустить смещение
    NEXT

Строковые литералы - LITSTRING

LITSTRING - это примитив, используемый для реализации операторов  ."  И  S"  (которые написаны в формате Forth). См. ниже определение этих операторов.

defcode "LITSTRING",9,,LITSTRING
    lodsl                   # Получить длину строки
    push    %esi            # push адрес начала строки
    push    %eax            # push длину
    addl    %eax,%esi       # пропустить строку
    addl    $3,%esi         # но округлить до следующей 4 байтовой границы
    andl    $~3,%esi
    NEXT

Печать строки - TELL

TELL просто печатает строку. Это более эффективно определять в ассемблере, потому что мы можем сделать это одним из системных вызовов Linux.

defcode "TELL",4,,TELL
    pop     %edx                # param3: длина строки
    pop     %ecx                # param2: адрес строки
    mov     $1, %ebx            # param1: stdout
    mov     $sys_write, %eax    # SYSCALL #4 (write)
    int     $0x80
    NEXT

QUIT

QUIT - первая функция Forth, вызываемая почти сразу после того, как система Forth загружается. Как объяснялось ранее, QUIT никуда не "уходит". Она выполняет некоторую инициализацию (в частности, она очищает стек возвратов), и вызывает INTERPRET в цикле для интерпретации команд.

Причина, по которой он называется QUIT, заключается в том, что вы можете вызвать его из собственных слов Forth, чтобы "выйти" из вашей программы и начать снова работать в режиме приема команд от пользователя.

# QUIT не должна возвращаться (те есть вызывать EXIT).
defword "QUIT",4,,QUIT
    # Положить константу RZ (начальное значение стека возвратов) на стек параметров.
    .int RZ
    # Установить значение, лежащее на стеке параметров, как новое значение вершины стека возвратов
    .int RSPSTORE       # Это очищает стек возвратов
    # Запустить интерпретатор команд                  <-----+
    .int INTERPRET      # Интерпретировать следующее слово  |
    # И навсегда зациклиться                                |
    .int BRANCH,-8      # -----------------------------------

INTERPRET

INTERPRET является REPL (см.: http://en.wikipedia.org/wiki/REPL) внутри Forth.

Этот интерпретатор довольно прост, но помните, что в Forth вы всегда можете переопределить его более мощным!

defcode "INTERPRET",9,,INTERPRET
    call    _WORD           # Возвращает %ecx = длину, %edi = указатель на слово.
    # Есть ли слово в словаре?
    xor     %eax, %eax
    movl    %eax, (interpret_is_lit)    # Это не литерал (или пока не литерал)
    call    _FIND           #           # Возвращает в %eax указатель на заголовок или 0
    test    %eax, %eax      #           # (Совпадение)?
    jz  1f                  #--------+  # ?-Не думаю! Переход вперед к (1)
    # Это словарное слово   #        |  # ?-Да. Найдено совпадающее слово. Продолжаем.
    # Это IMMEDIATE-слово?  #        |  #
    mov     %eax, %edi      #        |  # %edi = указатель на слово
    movb    4(%edi), %al    #        |  # %al = flags+length.
    push    %eax            #        |  # Сохраним его (flags+length) ненадолго
    call    _TCFA           #        |  # Преобразуем entry (в %edi) в указатель на codeword
    pop     %eax            #        |  # Восстановим flags+length
    andb    $F_IMMED, %al   #        |  # (Установлен флаг F_IMMED)?
    mov     %edi, %eax      #        |  # %edi->%eax
    jnz     4f              #--------|-+# ?-Да, переходим сразу к выполнению (4)
    jmp 2f                  #--+     | |# ?-Нет, переходим к проверке режима работы (2)
    # --------------------- #  |     | |# -------------------------------------------------
1:  #                   <------|-----+ |
    # Нет в словаре, будем считать, что это литерал
    incl    (interpret_is_lit)#|       |# Установим флаг
    call    _NUMBER         #  |       |# Возвращает число в in %eax, %ecx > 0 если ошибка
    test    %ecx, %ecx      #  |       |# (Удалось распарсить число)?
    jnz 6f                  #--|-----+ |# ?-Нет, переходим к (6)
    mov     %eax, %ebx      #  |     | |# ?-Да, Перемещаем число в %ebx,
    mov     $LIT, %eax      #  |     | |#     Устанавливаем слово LIT в %eax <ЗАЧЕМ????>
2:  #                   <------+     | |#
    # Проверим в каком мы режиме     | |#
    movl    var_STATE, %edx #        | |#
    test    %edx, %edx      #        | |#     (Мы компилируемся или выполняемся)?
    jz  4f                  #-----+  | |#     ?-Выполняемся. Переходим к (4)
    call    _COMMA          #     |  | |#     ?-Компилируемся. Добавляем словарное определение
    mov     (interpret_is_lit), %ecx#| |#
    test    %ecx, %ecx      #     |  | |#       (Это был литерал)?
    jz      3f              #--+  |  | |#       ?-Нет, переходим к NEXT
    mov     %ebx, %eax      #  |  |  | |#       ?-Да, поэтому за LIT следует число,
    call    _COMMA          #  |  |  | |#            вызываем _COMMA, чтобы скомпилить его
3:  #                   <------+  |  | |#
    NEXT                    #     |  | |# NEXT
    # ---------------------       |  | |# -------------------------------------------------
4:  #                   <---------+<-|-+
    # Выполняемся                    |
    mov     (interpret_is_lit), %ecx#|
    test    %ecx, %ecx      #        |  # (Это литерал)?
    jnz 5f                  #--+     |  # ?-Да, переходим к (5)
    # Не литерал, выполним прямо сейчас. Мы не осуществляем возврата, но
    # codeword в конечном итоге вызовет NEXT, который повторно вернет цикл в QUIT
    jmp     *(%eax)         #  |     |
    # --------------------- #  |     |  # -------------------------------------------------
5:  #                    <-----+     |
    # Выполняем литерал, что означает, что мы push-им его в стек и делаем NEXT
    push    %ebx            #        |
    NEXT                    #        |
6:  #                    <-----------+
    # Мы здесь, если не получилось распарсить число в текущей базе или этого
    # слова нет в словаре. Печатаем сообщение об ошибке и 40 символов контекста.
    mov     $sys_write, %eax#           # SYSCALL #4 (write)
    mov     $stderr, %ebx   #           # param1: stderr
    mov     $errmsg, %ecx   #           # param2: Выводимая строка
    mov     $errmsgend-errmsg, %edx     # param3: Длина выводимой строки
    int     $0x80           #           # SYSCALL
    # Ошибка произошла перед currkey
    mov     (currkey), %ecx #
    mov     %ecx, %edx      #
    sub     $input_buffer, %edx         # %edx = (currkey - buffer) (длина буфера перед currkey)
    cmp     $40, %edx       #           # (if > 40)?
    jle 7f                  #--+        # ?-Нет, печатаем все
    mov     $40, %edx       #  |        # ?-Да, печатать только 40 символов
7:  #                    <-----+
    sub     %edx, %ecx      #           # %ecx = start of area to print, %edx = length
    mov     $sys_write, %eax            # SYSCALL #4 (write)
    int     $0x80           #           # SYSCALL
    # Выведем перевод строки
    mov     $sys_write, %eax            # SYSCALL #4 (write)
    mov     $errmsgnl, %ecx #           # newline
    mov     $1, %edx        #           # Длина
    int     $0x80           #           # SYSCALL
    NEXT                    #           # NEXT
    # ---------------------
    .section .rodata
errmsg:
    .ascii "PARSE ERROR: "
errmsgend:
errmsgnl:
    .ascii "\n"

    .data                   # NB: проще записать в .data section
    .align 4
interpret_is_lit:
    .int 0                  # Флаг литерала

CHAR

CHAR помещает код ASCII первого символа следующего слова в стек. Например, CHAR A кладет 65 в стек.

defcode "CHAR",4,,CHAR
    call    _WORD           # Возвращает %ecx = length, %edi = указатель на слово.
    xor     %eax, %eax
    movb    (%edi), %al     # Получаем первый символ слова
    push    %eax            # Кладем его в стек
    NEXT

EXECUTE

EXECUTE используется для запуска токенов выполнения. См. обсуждение токенов выполнения в коде Forth для получения более подробной информации.

С точки зрения реализации EXECUTE делает следующее:

  • берет указатель на codeword слова, которое нужно выполнить.
  • т.к. этот codeword сам является указателем на процедуру выполнения (такую, как DOCON) - осуществляется переход по нему. Т.е. управление передается этой процедуре.

После перехода на токен его NEXT выйдет из текущего слова.

defcode "EXECUTE",7,,EXECUTE
    pop     %eax            # Получить токен выполнения в %eax
    jmp     *(%eax)         # и выполнить jump на него.

DODOES

Работа этого кода объясняется во второй части

DODOES:
    PUSHRSP %esi            # (с) Сохраняем ESI на стеке возвратов

    pop     %esi            # (b,d) CALL-RETADDR -> ESI

    lea     4(%eax), %eax   # (a) вычислить param-field DEUX
    pushl   %eax            # (a) push его на стек данных

    NEXT                    # (e) вызвать интерпретатор

defconst "DODOES_ADDR",11,,DODOES_ADDR,DODOES

Системные вызовы

SYSCALL0, SYSCALL1, SYSCALL2, SYSCALL3 делают стандартный системный вызов Linux. (См. список номеров системных вызовов). Как видно из названия, эти формы занимают от 0 до 3 параметров syscall, а также номер системного вызова.

В этом Forth SYSCALL0 должен быть последним словом во встроенном (ассемблерном) словаре, потому что мы инициализируем переменную LATEST, чтобы указать на нее. Это означает, что если вы хотите расширить ассемблерную часть, вы должны поместить новые слова перед SYSCALL0 или изменить способ инициализации LATEST.

defcode "SYSCALL3",8,,SYSCALL3
    pop     %eax            # Номер системного вызова (см. <asm/unistd.h>)
    pop     %ebx            # Первый параметр.
    pop     %ecx            # Второй параметр
    pop     %edx            # Третий параметр
    int     $0x80
    push    %eax            # Результат
    NEXT

defcode "SYSCALL2",8,,SYSCALL2
    pop     %eax            # Номер системного вызова (см. <asm/unistd.h>)
    pop     %ebx            # Первый параметр.
    pop     %ecx            # Второй параметр
    int     $0x80
    push    %eax            # Результат
    NEXT

defcode "SYSCALL1",8,,SYSCALL1
    pop     %eax            # Номер системного вызова (см. <asm/unistd.h>)
    pop     %ebx            # Первый параметр.
    int     $0x80
    push    %eax            # Результат
    NEXT

defcode "SYSCALL0",8,,SYSCALL0
    pop     %eax            # Номер системного вызова (см. <asm/unistd.h>)
    int     $0x80
    push    %eax            # Результат
    NEXT

Сегмент стека и буффер ввода

    .bss

    # Стек возвратов Forth
    .set RETURN_STACK_SIZE,8192
    .align 4096
return_stack:
    .space RETURN_STACK_SIZE
return_stack_top:           # Initial top of return stack.

    # Буфер ввода
    .set INPUT_BUFFER_SIZE,4096
    .align 4096
input_buffer:
    .space INPUT_BUFFER_SIZE

    # Буфер данных - cюда указывает HERE
    .set INITIAL_DATA_SEGMENT_SIZE,65536
    .align 4096
data_buffer:
    .space INITIAL_DATA_SEGMENT_SIZE

Tangling

Теперь мы можем переходить к высокоуровневой части. Она лежит в разделе: Forth-часть

А тут осталась только сборка всего кода в один ассемблерный файл:

<<macro_next>>

<<macro_pushrsp>>

<<macro_poprsp>>

<<macro_defword>>

<<macro_defcode>>

<<macro_defvar>>

<<macro_defconst>>
<<flags>>

<<macros>>

<<built_in_vars>>

<<built_in_constants>>

<<asm_docol>>

<<words_for_retstack>>

<<simple_primitives>>

<<mod>>

<<comparison>>

<<exit>>

<<store>>

<<char_store>>

<<data_stack_words>>

<<word_key>>

<<word_emit>>

<<word_word>>

<<word_find>>

<<word_tcfa>>

<<word_tdfa>>

<<word_number>>

<<word_lit>>

<<word_tell>>

<<word_create>>

<<word_comma>>

<<word_rbrac>>

<<word_colon>>

<<word_semicolon>>

<<word_immediate>>

<<word_hidden>>

<<word_tick>>

<<word_interpret>>

<<word_branch>>

<<word_quit>>

<<word_char>>

<<word_execute>>

<<dodoes>>

<<word_syscalls>>

<<asm_entry>>

<<sys_ret_stack_and_input_buffer>>
Яндекс.Метрика
Home