[Перевод] Нет ветвлений? Нет проблем — Форт-ассемблер
Перевод
Оригинальный текст:
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.
То, что мы хотим сделать, это поставить 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, с контрольной точкой где-то посередине.
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. Обычно ничто не мешает встроить в эти слова больше ума, чтобы автоматически подобрать правильный вариант на основе операндов. Однако в данном конкретном случае у нас нет ветвящихся слов (поскольку они и есть наша цель