4E4th + TI LaunchPad. В начале было Слово.

После нескольких экспериментов и вводных слов (раз, два) пора переходить к рассмотрению внутренних механизмов нашей учебной форт-системы 4e4th. Иначе, как мне кажется, народу скоро наскучит зажигать светодиоды из терминала и забавляться вывернутой наизнанку логикой стековой машины.

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

В связи с этим подумалось, что те люди, которым интересна данная оригинальная технология программирования родом из 70-х, могли бы сами потихоньку копать в интересующем направлении, но для этого нужно уяснить несколько базовых принципов и основ построения forth.

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

Таким образом, сегодня рассмотрим строение основополагающего компонента любой forth-системы — слова.


Структура слова forth
В прошлый раз я попытался описать слово dump, которое выводило бы на экран заданную область памяти в виде hex-кодов и ascii-символов.
Проглядывая файл hilvl430G2553.s43 в репозитории, заметил следующее определение:

;X DUMP  adr n  --   dump memory
;  OVER + SWAP DO
;    CR I 4 U.R SPACE SPACE
;    I $10 + I DO I C@ 3 U.R LOOP  SPACE SPACE
;    I $10 + I DO I C@ $7F AND $7E MIN BL MAX EMIT LOOP
;  10 +LOOP ;
    HEADER  DUMP,4,'DUMP',DOCOLON
        DW OVER,PLUS,SWAP,xdo
LDUMP1: DW CR,II,lit,4,UDOTR,SPACE,SPACE
        DW II,lit,10h,PLUS,II,xdo
LDUMP2: DW II,CFETCH,lit,3,UDOTR,xloop
        DEST  LDUMP2
        DW SPACE,SPACE
        DW II,lit,10h,PLUS,II,xdo
LDUMP3: DW II,CFETCH,lit,7Fh,ANDD,lit,7Eh,MIN,BLANK,MAX,EMIT,xloop
        DEST  LDUMP3
        DW lit,10h,xplusloop
        DEST  LDUMP1
        DW EXIT

Это как раз слово dump, которое я пытался изобрести. Оно просто лежало в памяти и ждало, когда же его вызовут :)
Очень интересно видеть, как организовано слово forth на ассемблере.
Как видим, оно мало отличается от своего натурального определения (в шапке комментария). Отличия в основном заключаются в замене запрещенных синтаксисом символов в макроименах (UDOTR вместо U.R, PLUS вместо + и т.д.).
Это слово высокого уровня, состоящее из ссылок на более простые слова.

Возьмем на заметку ещё одно интересное слово, мелькнувшее тут же:
U.R    u n --           display u unsigned in n width

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

;X U.R    u n --           display u unsigned in n width
;   >R  <# 0 #S #>  R> OVER - 0 MAX SPACES  TYPE ;
    HEADER  UDOTR,3,'U.R',DOCOLON
        DW TOR,LESSNUM,lit,0,NUMS,NUMGREATER
        DW RFROM,OVER,MINUS,lit,0,MAX,SPACES,TYP,EXIT


Итак, начинаем трепанацию, слабонервных и излишне впечатлительных просим подождать за перегородкой :)

Макрос HEADER описывает заголовок слова.
Находим его, как и другие системные макросы, в файле CF430G2553forth.h.

// ; HEADER CONSTRUCTION MACROS

HEADER  MACRO   asmname,length,litname,action
        PUBLIC  asmname
        DW      link
        DB      0FFh       ; not immediate
link    SET     $
        DB      length
        DB      litname
        EVEN
        IF      'action'='DOCODE'
asmname: DW     $+2
        ELSE
asmname: DW      action
        ENDIF
        ENDM

Разберем, что же он положит в память.
Сначала будет байт 0xFF, означающий «not immediate», то есть «нормальное» слово, в отличие от «immediate», то есть немедленного исполнения (уже знакомые нам DO, IF, BEGIN и др.).

Дальше идет символьное имя слова litname, выровненное по границе машинного слова.
А вот дальше — непосредственно «кишки» нашего слова. Причем, как видно, содержимое будет зависеть от типа слова — низкого уровня (DOCODE) или других. Также, ссылка asmname будет указывать на разные области слова.

В нашем случае имеем слово высокого уровня, которое состоит из ссылок на другие слова.
При этом после заголовка располагается адрес процедуры DOCOLON, осуществляющей вход в новое слово:

; DOCOLON enters a new high-level thread (colon definition.)
; (internal code fragment, not a Forth word)
        PUBLIC DOCOLON
DOCOLON: 
        PUSH IP         ; 3 save old IP on return stack
        MOV W,IP        ; 1 set new IP to PFA
        NEXT            ; 4

Данный код сохраняет на стеке возвратов текущий адрес инструкции вызывающего слова, а указатель инструкций устанавливает на первую адресную ссылку в нашем слове. Слово EXIT, стоящее в конце любого слова, выполняет обратную операцию для возобновления потока инструкций вызывающего слова. А вот внутри слова переход осуществляется с помощью слова NEXT, которое фактически осуществляет JMP на адрес следующего слова. Данный механизм реализует так называемый шитый код, используемый в подавляющем большинстве форт-систем.

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

Нашел удачную картинку, поясняющую происходящее:



Теперь ещё один нюанс. Мне пришлось читать IAR_AssemblerReference, чтобы его окончательно понять.
В самом начале макроса HEADER стоит инструкция DW link, а потом идет строка link SET $, которая присваивает указателю link значение адреса поля length текущего слова. Таким образом, перед каждым следующим словом ставится ссылка на начало предыдущего и автоматически формируется связный список словаря. Где бы слово ни находилось, оно увязано одной «ниточкой» со всеми остальными. Именно это свойство используется при интерпретации и компиляции для поиска слова по имени.

Разобравшись в общих чертах, что нам предстоит увидеть, начинаем непосредственно вскрытие. В этом нам поможет волшебное слово ' (апостроф), которое найдет адрес слова по его имени. Точнее, адрес его кодовой секции. Чтобы увидеть все слово целиком, из этого адреса мы должны вычесть длину литерального поля имени (учитывая выравнивание по машинному слову) и ещё два — поле длины имени и служебный байт.

Пробуем:

>hex \ переходим в режим отображения hex
ok
>' u.r \ адрес кодовой секции
ok
>6 - \ адрес начала слова
ok
>30 dump \ дамп

F8D2   FF  3 75 2E 72  0 58 E0 70 E1 1C EF 3A E0  0  0  ~ u.r X`pa o:`
F8E2   66 EF 7C EF 80 E1 2E E1 EE E3 3A E0  0  0 FC EB  fo|o a.anc:`  |k
F8F2   E6 EC 94 ED 50 E0 D3 F8 FF  4 64 75 6D 70 58 E0  fl mP`Sx~ dumpX` ok
>

Здесь обратный слеш — это слово комментария до конца строки, аналогично // в си.
В символьной колонке дампа явно видим имя нашего слова и специфическую тильду перед ним, а следом за именем идут адреса ссылок. Первая — E058 — на процедуру DOCOLON, следующие — на составляющие слова. И в самом конце, перед заголовком слова dump, видим ссылку F8D3 — как раз на начало нашего слова u.r

Посмотрим теперь строение другого слова, низкого уровня. Например, SWAP.
Исходник на ассемблере

;C SWAP     x1 x2 -- x2 x1    swap top two items
        HEADER  SWAP,4,'SWAP',DOCODE
        MOV     @PSP,W          ; 2
        MOV     TOS,0(PSP)      ; 4
        MOV     W,TOS           ; 1
        NEXT                    ; 4

и отображение в памяти

> ' swap 6 - 20 dump

E112   FF  4 73 77 61 70 1A E1 26 44 84 47  0  0  7 46  ~ swap a&D G   F
E122   36 45 30 46 13 E1 FF  4 6F 76 65 72 30 E1 26 44  6E0F a~ over0a&D ok
>

Теперь видим, что ссылка после имени слова указывает непосредственно на следующий адрес Е11А, в котором находится первая машинная инструкция MOV @PSP,W. Ну и опять же, последняя ссылка перед следующим словом over указывает на начало текущего слова.

Из того, что мы только что выяснили, следует несколько важных выводов.

  • Все слова в словаре автоматически увязаны в связный список, и поиск слова по имени осуществляется последовательным движением по этой цепочке до первого совпадения имени.
  • Заголовки с именами нужны только на этапе компиляции и интерпретации, в скомпилированном слове используются исключительно кодовые секции и ссылки на слова.
  • Можно безболезненно «обезглавить» большинство системных слов, которые пользователь напрямую не вызывает и которые будут так же работать в составе других слов, как и изначально. Освобождается место в памяти, занимаемое ненужным текстом.
    У этого пункта есть ещё одна сторона. Можно специально удалить заголовки тех слов, которые пользователь не должен использовать по соображениям безопасности. В самом деле, зачем обычному пользователю иметь неограниченный доступ к любой периферии и памяти.
  • Для работы шитого кода в рантайме необходимы слова и макросы DOCOLON и EXIT для переходов по уровням высокоуровневых слов и NEXT для переходов «по горизонтали».
    То есть непосредственно виртуальная машина может работать практически на пустом месте. Таким образом, миф о «громоздком интерпретаторе», необходимом для работы форта, безжалостно разрушен.

Рассмотрим ещё два очень важных объекта — это константы и переменные.

> hex
 ok
> 1234 constant my_c1
 ok
> : my_c2 2345 ;
 ok
> 3456 user my_c3
 ok
> C000 30 dump

C000   61 FD FF  5 6D 79 5F 63 31 FF 8A E0 34 12  3 C0  a}~ my_c1~ `4  @
C010   FF  5 6D 79 5F 63 32 FF 58 E0 3A E0 45 23 50 E0  ~ my_c2~X`:`E#P`
C020   11 C0 FF  5 6D 79 5F 63 33 FF B2 E0 56 34 FF FF   @~ my_c3~2`V4~~ ok
>

Мы задали три константы тремя разными способами.
Первый — с помощью специального слова constant.
Второй — создали новое слово, которое просто помещает нашу константу на стек.
Третий — с помощью слова user. //отставить… user задает что-то другое, пока не понял что..

Каждое из определений представляет из себя слово, которое кладет константу на стек.
Если делать это прямым определением, получается наибольшая длина, если стандартным словом constant — средняя, и наименьшая при использовании слова user.

Переменная задается словом variable, которое резервирует одну ячейку в ОЗУ и определяет адрес в виде именованной константы. То есть, вызывая имя переменной, мы фактически кладем ее адрес на стек.

Теперь об обещанных массивах и структурах.
Массив — это та же переменная, но только большей длины. То есть при определении массива мы должны застолбить в ОЗУ не одну ячейку, а несколько.
Посмотрим, как размещается переменная:

;C VARIABLE   --            define a Forth VARIABLE
;   CREATE CELL ALLOT ;

В случае массива нужно просто заменить слово CELL на CELLS и предварить определение длиной массива в ячейках.

: array ( len -- )
    create cells allot ;

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

> 10 array MyArray
 ok
> MyArray .
966  ok
> 10 array MyArray1
 ok
> MyArray1 .
986  ok
>

Как видите, все очень просто.
Первое определение отцепило от пользовательского ОЗУ 20 байт (10 ячеек по 2 байта), что мы и видим, вызывая адрес второго массива.
Естественно, второй массив занял ровно столько же.

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

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

Можно, например, ввести типизацию переменных (что совершенно нетипично для форта) и задавать переменные не в виде константного адреса, а структуры со всеми необходимыми свойствами. Скажете, что это повлечет накладные расходы? Разумеется. Но это будут расходы компилирующей части, никак не влияющие на размер и быстродействие целевых, рабочих слов. Именно поэтому многие «взрослые» форт-системы разделены на HOST и TARGET. Все дополнительное окружение компилируется на хосте, а в программу таргета складываются компактные конечные слова, и даже без заголовков, если это целевая прошивка без интерактивных возможностей.

Ещё одна возможность, реализуемая с помощью деления на HOST/TARGET, заключается в том, что можно сделать распределенную систему, в которой на таргете остается минимальное ядро из пары десятков инструкций, а все остальное компилируется на хосте и исполняется в интерактивном режиме. Причем, со стороны это будет выглядеть так же, как будто программа полностью залита в целевой кристалл.
Зачем это нужно? Вариантов множество. Для отладки. Для написания программы «снизу вверх», когда каждую минимальную процедуру можно отлаживать прямо на лету вне зависимости от других. Для работы программы, рассчитанной на размещение в «толстых» кристаллах, на макетке типа LaunchPad. Может, и бред, просто привожу возможные варианты. Один из вариантов (который мы с вами, кстати, пользуем в данный момент) — это использование урезанных по длине выходной прошивки демо-версий компиляторов. 4e4th спокойно компилируется (точнее, ассемблируется) в IAR Kickstart, не требующем лицензии.

На сегодня пока все. Хотел ещё разместить несколько практических примеров, но пока нет ни времени, ни сил.
Пусть будет голая теория.
А следующее занятие посвятим практике на основании вновь приобретенных знаний (и конструктивных комментариев).
  • +1
  • 12 ноября 2012, 10:51
  • MrYuran

Комментарии (25)

RSS свернуть / развернуть
Приложите к статье бинарик для заливки в кристал — а то неохота тянуть, ставить и компилить в IAR…
0
  • avatar
  • wowa
  • 12 ноября 2012, 17:51
Так он там же в репах лежит, вот.
«Сохранить как..», иначе просто текст прошивки откроется
0
О… а я прозевал… Я ихспутал с асемблерными файлами…
0
Понял почему… Я сначала стянул для FR чипа… Там был прямо и бинарик… А у G я нестянул поддиректории… и поэтому подумал что его там нет :)
0
Хотя, у меня прошит немецкий вариант, причем _se
Чем отличается от "не_se", пока не понял, не считая уже выявленных разногласий S? и S2?
0
То, что я в чуть выше дал — это оригинальный CamelForth, прототип.
Прошил — вроде то же самое, только не выдает приглашение в виде скобочки и ок выводит в той же строке, без переносов/
0
Я не могу понять, как слова переопределяются, ведь из слова можно вызвать прежнее это же слово. Получается, что все определения одного слова остаются в памяти? В gforth (пока на нем экспериментирую) почему-то нет слова forget.
0
Тут все хитрО :)
Если просто переопределить через:, то на самом деле в словарь впишется новое слово с новым определением. Поскольку слов поиск идет с конца по цепочке, то все новые ссылки на это слово будут использовать новое определение, в то время как старые уже скомпилированные слова будут ссылаться на старый вариант.
Чтобы переопределить слово насовсем, используются специальные методики. Например, классический вариант CREATE DOES>
Слово CREATE создает новое слово в словаре (словарную статью), а DOES> дает ссылку на реализацию. Однако, в нашем случае (размещение в флеш-памяти) такой вариант не прокатит. То есть, механизм есть, он работает, но одноразово. CREATE создает новое слово с FFFF в поле адреса, а DOES> может однократно вписать адрес ссылки на тело.
В RAM-based системах никаких проблем нет.
0
Надо будет не забыть включить этот момент в лабораторный практикум :)
0
Только что проделал фокус:

: blink >r 2dup cset r> ms cclr ; ok 
: test green 200 blink ; ok 

test ok \ мигнул зелёный

: old_test test ; ok
: test red 200 blink ; ok 

test ok \ мигнул красный

old_test ok \ опять зелёный :)
0
Т.е. чтобы избавиться от прежней версии, нужно ее стереть (через FORGET) сначала?
Начнешь чью-то форт-машину смотреть, и будешь постоянно путаться.
0
Если необходимо часто менять необходимую последовательность действий слова, то оно обычно определяется с помощью DEFER name
и далее например
' слово IS name
установит новый вектор поведения для слова name
0
Спасибо за разъяснения, кажется разобрался как это работает. Получается же не удобно что-то менять в программе, все слова ссылаются уже на старое слово, и никак не поменять его поведение что-ли? Ведь заранее не знаешь, какое слово придется изменить в будущем.
0
Если рассматривать встроенный вариант Форт системы, и знать её устройство,
то зачастую всегда имеется возможность (если другие варианты исчерпаны) изменить поведение любого слова (при сохранении баланса вход/выход) заменив непосредственно указатель на новое «тело» программного кода в заголовке слова. Но это уже ближе к хаку кода. хотя и допустимо. Встроив Форт в «любую» программу можно «допилить» необходимый функционал и сервис уже самостоятельно.
0
Форт ничем не ограничивает варианты своей перестройки. Всё зависит от самого разработчика, а Форт подобно пластилину, примет необходимую
форму и наполнение.
0
Это да, Форт позволяет на самом низком уровне работать, но я сужу с точки зрения текущего опыта программирования, и мне отсутствие таких механизмов при разработке кажется сильно непривычным. Как же фортеры программы разрабатывают, или ранее определенные слова уже не меняют никогда? Интересен сам процесс разработки программы, раз уж он отличен от такового на других языках программирования.

Похоже что буду писать свою версию Форт-а, где будет немного другая идеология, а всего то нужно, это написать новое слово ":", которое будет переопределять слово полностью, не дублируя его в списке слов. Хотя, может и поменяю свои привычки, надо только понять полностью Форт, каким образом на нем писать программы. Ведь на Форте довольно сложные вещи делались, например Canon Cat, я уже скачал его исходники, буду смотреть, учиться как надо делать программы.
0
Какого уровня абстракции не хватает? В Форте существует возможность управлять контекстом выбора (например одних и тех же слов из разных словариков), и размещать новое слово также в необходимом месте (слова работы со словариками).
Может пока Вам не совсем удобно в рамках простого терминала работать со встроенной Форт системой? или ещё не «обкатали» данный подход на себе.
0
Слова конечно меняются при редактировании и программа или её часть может перезаливаться в контроллер. И если есть какие то неудобные моменты в предлоенном варианте работы с Форт их и надо, прежде всего, улучшать.
0
Получается что в самом форте программу не разрабатывают, а лишь отлаживают код?

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

А то видел пример, рисуем диаграмму в терминале символами #, а потом задание переделать вывод другими символами, переписав слово вывода мы ничего не добьемся, ведь все остальные слова уже ссылаются на старый код. Мне было бы интереснее разрабатывать сверху вниз, написав временные заглушки мелких слов, и потом просто переопределив их.
0
Для полноценной разработки весь код должен «легко» обозреваться и редактроваться.
Т.к. Форт слове есть поле указатель на тело кода (а сам Форт список слов в нужном словарике), то «ничто» не ограничивает возможность вставить, для использования, новый вариант слова, сохранив при этом все ссылки на использование введённого слова. Только при этом, без сборки «мусора» память в контроллере (не ресурсоёмком) может «быстро» растрачиваться.
Может помочь определение векторных DEFER слов
0
Терминал форта как раз нравится, но не понимаю как в нем разрабатывать программу, в тех же бейсиках всегда можно переопределить ранее написанное при необходимости, а как это делается в форте? Или нужно программу хранить во внешней памяти, и работать с ней не из терминала, а из внешнего текстового редактора?
0
А какая ссылка на исходники Canon Cat?
Одна из крупных Форт разработок — это, например, OpenBios (Биос для компьютеров на Форт, применяется например в OLPC компьютерах)
0
Если необходим функционал ООП, то расширений на выбор много сделано от простых до усложнённых. Даже в рамках Bash реализовали Форт и к нему надстройку ООП www.forthfreak.net/index.cgi?BashForth
0
Обычно, прежние определения слов, это базис построения новых.
В gforth, как и во многих Форт системах слова FORGET нет, как не совсем приемлемое по действию
в Форт системе. Вместо него определили слово MARKER name При исполнении
name забудутся слова определённые после этого маркёра.
Слово, пока не определено, не включено в список поиска. В Форт слова с одним именем может быть определено в разных словариках, а программист сам задаёт нужную последовательность поиска и в акой словарь добавлять новое слово.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.