Ветвление: сборка не требуется

29 июня 2021 г. · 7 минут чтения

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

Предыдущий пост:
Нет веток? Нет проблем — Форт-ассемблер

Следующее сообщение:
Как Forth реализует исключения

В прошлый раз мы начали с barebones ядра Miniforth , 1 и реализовали ветки, написав дополнительные слова-примитивы на ассемблере. Из прагматических соображений именно этим путем я и буду следовать дальше, но я заметил, что в чистом Форте также можно реализовывать ветки. Я считаю, что этот подход довольно интересен, так что давайте отклонимся и посмотрим поближе.

Соответствующие 2 доступных примитива:

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

Кроме того, у нас есть реализации следующего из [предыдущего поста](https://habr.com/ru/articles/739012/ :

  • системные переменные latest, st, base, dp,disk#

  • доступ к пространству словаря: here, allot, ,,c,

  • определяющие слова create, variable,constant

  • немного разного: +!, cell+, cells, compile, immediate,lit,

Вы можете запустить Miniforth, следуя инструкциям в репозитории GitHub . Если вы хотите попробовать реализовать ветки pure-Forth самостоятельно, сейчас самое время прекратить чтение. В противном случае мы будем изучать ветки на purityветке .

Безусловные ветви

Когда (branch)or (0branch)компилируется в слово, за ним сразу же следует адрес цели ветки:

Диаграмма демонстрирует стратегию компиляции структуры if-else.  IF компилируется в две ячейки, где первая — (0branch), а вторая — цель перехода, которая указывает сразу после ELSE.  Перед этой целью перехода ELSE вводит безусловное (ответвление) в позицию THEN.
Диаграмма демонстрирует стратегию компиляции структуры if-else. IF компилируется в две ячейки, где первая — (0branch), а вторая — цель перехода, которая указывает сразу после ELSE. Перед этой целью перехода ELSE вводит безусловное (ответвление) в позицию THEN.

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

: (branch)
  r>  \ вывести адрес возврата, который указывает на ячейку, содержащую цель перехода
  @   \ получить цель перехода
  >r  \ сделать это новым обратным адресом
;

Условные ветки

Ясно, что сложность условного перехода сводится к выбору между двумя возможными значениями адреса возврата. Это было бы довольно просто, если бы у нас было andи 0=— поскольку в Форте trueустановлены все биты, мы можем andс помощью логического флага выбирать между значением по нашему выбору и 0,3 .

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

: (0branch)
  0=              ( должен перейти )
  r> dup cell+    ( должен пкркйти addr-of-offset retaddr-if-false )
  >r @ and r>     ( offset|0 retaddr-if-false )
  + >r
;

К сожалению, у нас нет andили 0=. Тем не менее, это все еще полезная отправная точка. Можем ли мы как-то реализовать эти слова?

Сдвиг битов вокруг

Было бы неплохо, если бы мы могли извлекать отдельные биты из слова. Если бы у нас это было, мы могли бы реализовывать побитовые функции, сдвигая входные данные, вычисляя то, что нам нужно, по крупицам и сдвигая результат:

Сдвиг влево достаточно прост, так как это просто умножение на два:

: 2* dup + ;

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

variable x
: lobyte x ! x c@ ;
: hibyte x ! x 1 + c@ ;

По сути, это 8-битный сдвиг вправо. Давайте используем это, чтобы проверить, равно ли число нулю. Нам нужно ИЛИ все биты вместе, но у нас нет ни того, orни другого. Однако сложение несколько похоже, поэтому давайте посчитаем биты в значении.

s ( c v -- c' v' )обрабатывает одну итерацию «цикла» — он немного сдвигается за пределы 8-битного значения vи добавляет его к счетчику c.

: s 2* dup >r hibyte + r> lobyte ;

Выполнение этого 8 раз будет подсчитывать биты в байте, вот что делает nb( number of its):b

: nb 0 swap s s s s s s s s drop ;

nbw( nчисло bего в wорде) делает то же самое для полного 16-битного значения, вызывая nbдля каждой половины:

: nbw dup hibyte nb swap lobyte nb + ;

Как нам превратить это в сравнение с нулем? Повторяем nbнесколько раз:

  • после nbwу вас будет значение не более 16,

  • после nbw nbу вас будет значение не более 4,

  • после nbw nb nbу вас будет значение не более 2,

  • после nbw nb nb nbу вас будет значение либо 0, либо 1.

: 1bit nbw nb nb nb ;

Выбор между значениями

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

create bb 2 cells allot
: (0branch)
  r> dup              \ две копии
  @ bb !              \ bb[0] = адрес возврата, если на стеке 0
  cell+ bb cell+ !    \ bb[1] = адрес возврата, если на стеке есть что-то еще
  1bit cells bb + @ >r
;

Другие решения: компромисс между временем и памятью

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

Например, мы могли бы подготовить 256-байтовую таблицу поиска для файлов 1bit. Поскольку у нас нет никакого способа зациклиться, нам нужно будет повторить все вручную. Поскольку 255 = 3 × 5 × 17, это может выглядеть так:

: x 1 c, ;      \ записать 1 один
: y x x x ;     \  записать 3
: z y y y y y ; \ записать 15
create tab 0 c,
z z z z z z z z z z z z z z z z z     \ 17 times
: 1bit-half tab + c@ ;
: 1bit dup hibyte 1bit-half swap lobyte 1bit-half + 1bit-half ;

И это всё?

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

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

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

Понравилась эта статья?

Возможно, вам понравятся и другие мои посты . Если вы хотите получать уведомления о новых, вы можете подписаться на меня в Твиттере или подписаться на RSS-канал .

Я хотел бы поблагодарить моих спонсоров GitHub за их поддержку: Michalina Sidor и Tijn Kersjes.


1

Слово «ядро» используется здесь в смысле языковой реализации, а не операционной системы. Если вы знаете термин получше этого, пожалуйста, дайте мне знать, так как в какой-то момент мне придется говорить об обоих вещах одновременно...

2

Я пропускаю loadи s:, так как они не помогут, и их описание выходит за рамки этого поста. Я описал их в предыдущем посте , если вам интересно.

3

Этот подход, кажется, независимо изобретался как минимум трижды: мной , Цезарем Блюмом и Полом Слей .

4

Напомним, что x86 имеет прямой порядок следования байтов, и поэтому значение like 1234хранится как 34 12в памяти.

5

Кажется, я научился этой технике у M/o/Vfuscator .

6

Что ж, поскольку память конечна, все, что мы на самом деле когда-либо создавали, — это просто очень большая конечная машина. Я полагаю, что более близким понятием будет LBA-полнота, если мы будем педантичными, но я не стал бы надеяться на полностью формальное определение, которое отражает то, что мы обычно подразумеваем под «полным по Тьюрингу», когда говорим о вещах, которые действительно существуют.

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


  1. arteast
    02.06.2023 06:20
    +3

    Клепаете переводы, не приходя в сознание?

    • "bранчо b" - это вот что? Как можно было такое написать? Ответ - никак, это машинный перевод.

    • "Ассемблер четвертого стиля" - как, переводя статью о Forth, можно было так перевести Forth-style assembler? Ответ - никак, это машинный перевод.

    • следующая строчка "Обычный синтаксис сборки выглядит так:" - сборки?! Мы про ассемблер говорим, а не сборочный автомат.

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


    1. arteast
      02.06.2023 06:20

      Отвечу сам себе по теме "по теме" - потому, что смысл статьи был именно в том, чтобы не применять ассемблер... Читать по диагонали вредно.

      Что, кстати, показывает еще раз про перевод. Сравните "Ветвление: сборка не требуется" и "Branches: no assembly required".


      1. forthuse Автор
        02.06.2023 06:20

        смысл статьи был именно в том, чтобы не применять ассемблер...

        Вот есть классическая статья как на Форт можно сделать ассемблер.
        Brad Rodriguez: Как написать свой (кросс-)ассемблер