[Перевод] Нет ветвлений? Нет проблем — Форт-ассемблер

Перевод
Оригинальный текст:
22 июня 2021 г. · 36 минут чтения

Эта статья является частью серии «Начальная загрузка» , в которой я начинаю с 512-байтного начального числа и пытаюсь загрузить реальную систему.

Предыдущий пост:
Разместить FORTH в 512 байтах
Следующий пост:
Ветвление: сборка не требуется

Набор слов, доступных после загрузки Miniforth, довольно скуден. Один читатель даже заявил , что, поскольку ветвей нет, он не является полным по Тьюрингу и, следовательно, не достоин называться Фортом! Сегодня тот день, когда мы докажем, что они ошибались.

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

+ - ! @ c! c@ dup drop swap emit u. >r r> [ ] : ; load s:

Большинство из них будут знакомы программистам на Forth, но loadмогут s:нуждаться в некоторых комментариях. load ( u -- )является стандартным словом из необязательного набора слов Block — оно загружает блок uи выполняет его как исходный код Forth. 1 Это слово имеет решающее значение для практического использования такого маленького Форта, поскольку, как только вы загрузитесь достаточно далеко, вы можете сохранить свой код на диск, а после перезагрузки возобновить работу с помощью всего лишь 1 load.

Однако, чтобы добраться до этой точки, вам нужно написать довольно много кода. Чтобы сделать исходный код доступным в памяти, как только вы сможете его сохранить, я включил s: ( buf -- buf+len ), что, по сути, представляет собой ввод строки — остальная часть входного буфера копируется в buf. Адрес конца буфера остается в стеке, чтобы его можно было использовать s:на следующей строке, и результат будет конкатенирован.

В этом посте мы начнем с состояния, в котором загружается Miniforth, и:

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

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

s:- рабочий процесс

Я решил хранить свой исходный код по адресу 1000, в пространстве между стеками параметра и возврата. Первое, что нам понадобится, — это способ запуска кода, который мы туда поместили. Определено InputPtr, чтобы быть по адресу A02, поэтому давайте определим run, который подставляет значение по нашему выбору по этому адресу:

: >in A02 ;  : run >in ! ;

>in— это традиционное имя для указателя входного буфера, поэтому я выбрал его. 2 Чтобы убедиться, что он также доступен при последующих загрузках, я сохраняю этот фрагмент кода в памяти:

1000 s: : >in A02 ;  : run >in ! ;

Это хорошее время для просмотра текущего указателя на исходный буфер с помощью dup u.. Если вы не добавили некоторое пространство для записи, ответ будет 101A, и это адрес, на который мы хотим перейти runпозже, чтобы избежать переопределения >inи run. 3

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

Системные переменные

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

latest st base dp disk#

Обычно мы просто делаем constant disk#, constant hereи так далее. Однако у нас нет constant, или какого-либо способа его определения (пока). literalближе, но нам нужно по крайней мере hereреализовать его и latestпометить как немедленное. Мы можем обойти насущную проблему с помощью [и ], что предполагает следующий порядок действий:

swap : dp 0 [ dup @ 2 - ! ] ;

Давайте рассмотрим это шаг за шагом, так как это работает довольно сложно. dpозначает указатель данных . Это переменная back here, указатель компиляции, значение которого hereпросто определяется как

: here dp @ ;

Когда выполняется код внутри квадратных скобок, наша память выглядит так:

Сначала идет заголовок словаря, содержащий поле ссылки и имя.  Затем вызов DOCOL, LIT и 0. HERE указывает на самый конец, после 0.

Сначала идет заголовок словаря, содержащий поле ссылки и имя. Затем вызов DOCOL, LIT и 0. HERE указывает на самый конец, после 0.

То, что мы хотим сделать, это поставить dpтам, где 0в настоящее время. Так как мы запустили swapперед определением нашего слова, адрес dpнаходится на вершине стека. После dup @ 2 -у нас будет указатель на ячейку, содержащую 0, и !мы перезапишем ее. Как видите, the 0не имеет особого значения, мы могли бы использовать любой литерал.

Далее определяем cell+и cells. Причина, по которой я делаю это так рано, заключается в том, что одна из вещей, которую я в конечном итоге хотел бы сделать, — это переключиться на 32-битный защищенный режим, чтобы как можно больше кода не зависело от ширины ячейки.

: cell+ 2 + ;
: cells dup + ;

Кроме того, поскольку у нас теперь есть dp, давайте напишем allot. Функциональность увеличения переменной может быть выделена в +!:

: +! ( u addr -- ) dup >r @ + r> ! ;
: allot ( len -- ) dp +! ;

Это позволяет определить c,и ,, которые записывают байт или ячейку соответственно в область компиляции:

: c, here c! 1 allot ;
: , here ! 2 allot ;

Далее мы напишем lit,, который добавляет литерал к текущему определению. Для этого нам нужен адрес LITассемблерной процедуры, которая обрабатывает литерал. Мы сохраняем его в 'lit«константе» с помощью трюка, аналогичного тому, что мы сделали для dp:

: 'lit 0 [ here 4 - @ here 2 - ! ] ;
: lit, 'lit , , ;

Это позволяет нам легко обрабатывать остальные переменные в стеке:

: disk# [ lit, ] ;
: base [ lit, ] ;
: st [ lit, ] ;
: latest [ lit, ] ;

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

Пользовательские переменные

Если вы в настроении пошалить, вы можете создать переменные из воздуха, просто упомянув их. Отсутствие проверки ошибок превратит их в число, по сути, дав вам случайный адрес. Например, srcpos u.выходы DA9C. Конечно, вы рискуете тем, что эти адреса будут конфликтовать либо друг с другом, либо с чем-то еще, например со стеком или пространством словаря.

Я был не в настроении для шалостей, так что мы сделаем это как следует. Ядро любого определяющего слова будет основано на :, поскольку оно уже анализирует слово и создает словарную статью. Нам просто нужно вернуться в режим интерпретации. [делает это, но это непосредственное слово, и мы postponeпока не можем его определить, поэтому давайте определим наш собственный вариант, который не является немедленным:

: [[ 1 st c! ;

Нам также понадобится непрямой вариант ;. Единственное, что нужно сделать, это скомпилировать exit. Мы не знаем адрес exit, но можем прочитать его из последнего скомпилированного слова:

here 2 - @ : 'exit [ lit, ] ;

Например, вот как мы будем использовать его для constant:

: constant \ example: 42 constant the-answer
  : [[ lit, 'exit ,
;

createопределяет слово, которое помещает адрес сразу после него. Типичное использование

create some-array 10 cells allot

Чтобы вычислить адрес, который мы должны скомпилировать, нам нужно добавить 3 cells— по одному для каждого из LIT, фактического значения и EXIT.

: create : [[ here 3 cells + lit, 'exit , ;

variable, то просто allotодна ячейка:

: variable create 1 cells allot ;

Улучшениеs:

До сих пор указатели передавались s:и runуправлялись вручную. Однако это простой процесс, поэтому давайте его автоматизируем. srcposбудет содержать текущий конец буфера и checkpointбудет указывать на часть, которая еще не была запущена.

То есть в типичной ситуации исходный код начинался бы с 1000, заканчивался бы на srcpos, с контрольной точкой где-то посередине.

То есть в типичной ситуации исходный код начинался бы с 1000, заканчивался бы на srcpos, с контрольной точкой где-то посередине.

variable checkpoint
variable srcpos

Автоматический вариант s:называется s+:

: s+ ( -- ) srcpos @ s: dup u. srcpos ! ;

Мы также печатаем новый указатель. Это имеет два применения:

  • если вы допустили опечатку и хотите исправить, то можете просто прочитать примерный адрес, где нужно ковыряться;

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

Отложенная часть буфера может быть выполнена с помощью doit:

: move-checkpoint ( -- ) srcpos @ checkpoint ! ;
: doit ( -- ) checkpoint @ run move-checkpoint ;

Настройка этого сводится к чему-то вроде

1234 srcpos ! move-checkpoint

Эта строка не записывается на диск, так как после перезагрузки точный адрес не пригодится.

Ассемблер четвертого стиля

Обычный синтаксис сборки выглядит так:

    mov ax, bx

Если бы мы хотели справиться с этим, нам пришлось бы написать модный синтаксический анализатор, а мы никак не сможем сделать это без ветвей. Вместо этого давайте настроим синтаксис для наших целей — если AT&T разрешено это делать, то и мы тоже. Чтобы быть конкретным, давайте сделаем каждую инструкцию Форт-словом, передав аргументы через стек:

    bx ax movw-rr,

Я решил упорядочить аргументы как src dst instr,, с потоком данных слева направо. Это согласуется с тем, как данные передаются в обычном коде Forth, и является точным отражением синтаксиса Intel. После дефиса я включаю типы аргументов в том же порядке — регистр (r), память (m) или непосредственный (i). Наконец, инструкции, которые могут быть размером как в байт, так и в слово, имеют суффикс bили w, как в синтаксисе AT&T.

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

© Habrahabr.ru