Перевод Оригинальный текст: 22 июня 2021 г. · 36 минут чтения Эта статья является частью серии «Начальная загрузка» , в которой я начинаю с 512-байтного начального числа и пытаюсь загрузить реальную систему. Предыдущий пост: Разместить FORTH в 512 байтах Следующий пост: Ветвление: сборка не требуется Набор слов, доступных после загрузки Miniforth, довольно скуден. Один читатель даже заявил , что, поскольку ветвей нет, он не является полным по Тьюрингу и, следовательно, не достоин называться Фортом! Сегодня тот день, когда мы докажем, что они ошибались. В предыдущем посте я описал ядро Miniforth размером с загрузочный сектор со всеми хитроумными оптимизациями, которые мне пришлось использовать, чтобы вместить его. Подводя итог, доступны следующие слова: Большинство из них будут знакомы программистам на Forth, но load и s: могут нуждаться в некоторых комментариях. load ( u -- )является стандартным словом из необязательного набора слов Block — оно загружает блок uи выполняет его как исходный код Forth. 1 Это слово имеет решающее значение для практического использования такого маленького Форта, поскольку, как только вы загрузитесь достаточно далеко, вы можете сохранить свой код на диск, а после перезагрузки возобновить работу с помощью всего лишь 1 load. Однако, чтобы добраться до этой точки, вам нужно написать довольно много кода. Чтобы сделать исходный код доступным в памяти, как только вы сможете его сохранить, я включил s: ( buf -- buf+len ), что, по сути, представляет собой ввод строки — остальная часть входного буфера копируется в buf. Адрес конца буфера остается в стеке, чтобы его можно было использовать s:на следующей строке, и результат будет конкатенирован (соеденён). В этом посте мы начнем с состояния, в котором загружается Miniforth, и: написать ассемблер в стиле Forth, загрузить возможность записи на диск и используйте наш ассемблер для реализации ветвей. Нельзя сказать, что это единственный способ. У меня есть реализация ветвей на чистом Форте поверх Miniforth, и я намерен поговорить о ней подробнее примерно через неделю, а пока рекомендую вам попробовать разобраться с этим самостоятельно. Мне действительно любопытно, сколько существует различных подходов. Тем временем давайте рассмотрим подход, который  не отказывается от большей части производительности во имя чистоты. Для ознакомления и удобства экспериментов код из этого поста доступен на GitHub . При объяснении кода я иногда добавляю комментарии, но, поскольку мы еще не реализовали обработку комментариев, на самом деле их нет в коде. Я решил хранить свой исходный код по адресу 1000, в пространстве между стеками параметра и возврата. Первое, что нам понадобится, — это способ запуска кода, который мы туда поместили. Определено InputPtr, чтобы быть по адресу A02, поэтому давайте определим run, который подставляет значение по нашему выбору по этому адресу: >in— это традиционное имя для указателя входного буфера, поэтому я выбрал его. 2 Чтобы убедиться, что он также доступен при последующих загрузках, я сохраняю этот фрагмент кода в памяти: Это хорошее время для просмотра текущего указателя на исходный буфер с помощью dup u.. Если вы не добавили некоторое пространство для записи, ответ будет 101A, и это адрес, на который мы хотим перейти runпозже, чтобы избежать переопределения >inи run. 3 Написав достаточно кода, чтобы протестировать его, я печатал текущий адрес буфера с помощью u., а затем runновый код из предыдущего напечатанного адреса буфера. Во-первых, важно, чтобы адрес буфера не оставался наверху стека, так как Miniforth загружается с адресами встроенных системных переменных в стеке , и мы хотим получить к ним доступ. На самом деле почти все, что мы хотим сделать, требуют этих переменных, поэтому давайте сначала позаботимся об этом — необходимость копаться в стеке каждый раз, когда вам нужна одна из этих переменных, не работает. Стек начинается так: Обычно мы просто делаем constant disk#, constant hereи так далее. Однако у нас нет constant, или какого-либо способа его определения (пока). literalближе, но нам нужно по крайней мере hereреализовать его и latestпометить как немедленное. Мы можем обойти насущную проблему с помощью [и ], что предполагает следующий порядок действий: Давайте рассмотрим это шаг за шагом, так как это работает довольно сложно. dpозначает указатель данных . Это переменная back here, указатель компиляции, значение которого hereпросто определяется как Когда выполняется код внутри квадратных скобок, наша память выглядит так: То, что мы хотим сделать, это поставить dpтам, где 0в настоящее время. Так как мы запустили swapперед определением нашего слова, адрес dpнаходится на вершине стека. После dup @ 2 -у нас будет указатель на ячейку, содержащую 0, и !мы перезапишем ее. Как видите, the 0не имеет особого значения, мы могли бы использовать любой литерал. Далее определяем cell+и cells. Причина, по которой я делаю это так рано, заключается в том, что одна из вещей, которую я в конечном итоге хотел бы сделать, — это переключиться на 32-битный защищенный режим, чтобы как можно больше кода не зависело от ширины ячейки. Кроме того, поскольку у нас теперь есть dp, давайте напишем allot. Функциональность увеличения переменной может быть выделена в +!: Это позволяет определить c,и ,, которые записывают байт или ячейку соответственно в область компиляции: Далее мы напишем lit,, который добавляет литерал к текущему определению. Для этого нам нужен адрес LITассемблерной процедуры, которая обрабатывает литерал. Мы сохраняем его в 'lit«константе» с помощью трюка, аналогичного тому, что мы сделали для dp: Это позволяет нам легко обрабатывать остальные переменные в стеке: Я вызываю его stвместо state, потому что stateэто должна быть переменная размером с ячейку, где true означает компиляцию, а stпеременная размером с байт, где true означает интерпретацию. Если вы в настроении пошалить, вы можете создать переменные из воздуха, просто упомянув их. Отсутствие проверки ошибок превратит их в число, по сути, дав вам случайный адрес. Например, srcpos u.выходы DA9C. Конечно, вы рискуете тем, что эти адреса будут конфликтовать либо друг с другом, либо с чем-то еще, например со стеком или пространством словаря. Я был не в настроении для шалостей, так что мы сделаем это как следует. Ядро любого определяющего слова будет основано на :, поскольку оно уже анализирует слово и создает словарную статью. Нам просто нужно вернуться в режим интерпретации. [делает это, но это непосредственное слово, и мы postponeпока не можем его определить, поэтому давайте определим наш собственный вариант, который не является немедленным: Нам также понадобится непрямой вариант ;. Единственное, что нужно сделать, это скомпилировать exit. Мы не знаем адрес exit, но можем прочитать его из последнего скомпилированного слова: Например, вот как мы будем использовать его для constant: createопределяет слово, которое помещает адрес сразу после него. Типичное использование Чтобы вычислить адрес, который мы должны скомпилировать, нам нужно добавить 3 cells— по одному для каждого из LIT, фактического значения и EXIT. variable, то просто allotодна ячейка: До сих пор указатели передавались s:и runуправлялись вручную. Однако это простой процесс, поэтому давайте его автоматизируем. srcposбудет содержать текущий конец буфера и checkpointбудет указывать на часть, которая еще не была запущена. Автоматический вариант s:называется s+: Мы также печатаем новый указатель. Это имеет два применения: если вы допустили опечатку и хотите исправить, то можете просто прочитать примерный адрес, где нужно ковыряться; нам нужно убедиться, что ни одно определение не выходит за пределы килобайтной границы, чтобы наш буфер можно было напрямую записать в блоки. Отложенная часть буфера может быть выполнена с помощью doit: Настройка этого сводится к чему-то вроде Эта строка не записывается на диск, так как после перезагрузки точный адрес не пригодится. Обычный синтаксис сборки выглядит так: Если бы мы хотели справиться с этим , нам пришлось бы написать модный синтаксический анализатор, а мы никак не сможем сделать это без ветвей. Вместо этого давайте настроим синтаксис для наших целей — если AT&T разрешено это делать, то и мы тоже. Чтобы быть конкретным, давайте сделаем каждую инструкцию Форт-словом, передав аргументы через стек: Я решил упорядочить аргументы как src dst instr,, с потоком данных слева направо. Это согласуется с тем, как данные передаются в обычном коде Forth, и является точным отражением синтаксиса Intel. После дефиса я включаю типы аргументов в том же порядке — регистр ( r), память ( m) или непосредственный ( i). Наконец, инструкции, которые могут быть размером как в байт, так и в слово, имеют суффикс bили w, как в синтаксисе AT&T. Обратите внимание, что необходимость указывать типы операндов вручную не является фундаментальным ограничением ассемблеров Forth. Обычно ничто не мешает встроить в эти слова больше ума, чтобы автоматически подобрать правильный вариант на основе операндов. Однако в данном конкретном случае у нас нет ветвящихся слов (поскольку они и есть наша цель 😄). Проще всего кодировать инструкции, которые не принимают никаких аргументов. Например, Следующими по простоте являются инструкции, включающие непосредственные аргументы — числовые аргументы, которые идут сразу после кода операции: Некоторые инструкции используют битовое поле в байте кода операции. Например, немедленная загрузка, такая как mov cx, 0x1234`, кодирует регистр младшими 3 битами кода операции: Регистры сопоставляются со следующими числовыми значениями: Вы правильно прочитали, это идет AX CX DX BX. Насколько мне известно, это не потому, что кто-то в Intel забыл свои азбуки, а потому, что этимология этих имен регистров выглядит примерно так: «Аккумулятор , Счетчик , Данные , База » , и тот факт, что они являются первые четыре буквы просто совпадение. Или, во всяком случае, в этом суть. Этот пост на retrocomputing.SE содержит некоторые предположения о том, как это может быть полезно для конструкции оборудования, но не содержит твердых фактов. Соответствующая нумерация для байтовых регистров выглядит так: Таким образом, мы можем закодировать некоторые movs: Их можно использовать так: Обратите внимание, что ответственность за использование 8-битного регистра с movb, и 16-битного регистра с movw. Некоторые другие инструкции, которые закодированы таким образом, incw/decwи push/pop: Самые сложные инструкции, с которыми нам придется иметь дело, используют байт ModR/M . Это механизм кодирования, отвечающий за такие инструкции, как add ax, [bx+si+16], но также и за такие простые, как mov ax, bx. Сам код операции определяет шаблон, например mov r16, r/m16. В данном случае это означает, что адресатом является регистр, а источником является либо регистр, либо память. Байт ModR/M, который следует за кодом операции, определяет детали операндов: Три бита в середине определяют r16часть, а остальные определяют часть r/m16в соответствии с этой таблицей: поле reg/[regs] mod: 00 mod: 01 mod: 10 mod: 11 0    AL/AX 1    CL/CX 2    DL/DX 3    BL/BX 4    АХ/SP 5    CH/BP 6    DH/SI 7    BH/DI Как видите, если в поле mod установлено значение 3, то младшие 3 бита просто кодируют другой регистр в том же порядке, что и раньше. В противном случае мы выбираем одну из восьми возможностей для вычисления адреса с необязательным смещением, которое может быть либо 8, либо 16 бит. Упомянутое смещение идет сразу после байта ModR/M и при необходимости расширяется по знаку до 16 бит. Есть одна неточность: если мы попытаемся закодировать a без какого-либо смещения, вместо этого мы получим жестко закодированный адрес, такой как mov bx, [0x1234], который должен идти после самого байта ModR/M. 4 Если вы помните, именно из-за отсутствия было выгодно переключение стека возврата на использование вместо него.DI Специфическим аспектом этого кодирования является то, что операции между регистрами могут быть закодированы двумя разными способами. Возьмем xor cx, dx, например: В любом случае, давайте это реализуем. Во-первых, вариант от регистрации к регистрации. Я выбрал слово для компиляции такого байта ModR/M , означающее, что в поле rm-r,есть регистр, который также может быть emory, за которым следует еще один регистр. У нас нет битовых смещений, но мы можем обойти это повторным добавлением:r``m``r При использовании rm-r,нам нужно убедиться, что используемый код операции соответствует шаблону r/m16, r16— в противном случае нам потребовались бы swapдва регистра: Варианты памяти для регистрации не намного сложнее. Мы определяем режимы адресации так же, как мы это делали для регистров. режим абсолютного адреса, который следует использовать, собирая адрес вручную после инструкции, например Я также включаю , который конфликтует с , так как слова режима адреса могут использоваться совместно с режимами и . Аналогично rm-r,имеем m-r,: r-m,то же самое, просто поменяйте местами операнды: Нет необходимости определять каждую инструкцию с операндами памяти, movдостаточно просто s: Существует также одно несколько иное использование байта ModR/M. А именно, если инструкции требуется только один операнд (например, not bxили jmp ax), r/mфактически используется только он. В этом случае поле регистра вместо этого повторно используется как дополнительные биты для самого кода операции. В руководстве Intel для этого используется обозначение opcode /regbits. Например, непрямой переход — это FF /4, а непрямой вызов — это FF /2совместное использование основного байта кода операции. Мы можем кодировать такие инструкции, просто введя нужное значение перед вызовом rm-r,. Чтобы на самом деле собрать примитивное слово, нам также понадобится способ создания его заголовка. Самый простой способ сделать это — вызвать normal :, а затем перемотать dpна три байта назад, чтобы удалить вызов DOCOL: Чтобы закончить такое определение, мы компилируем NEXT: Обратите внимание, что next,не определяется с помощью :code— это эквивалент макроса ассемблера. В качестве примера простого собранного примитива рассмотрим реализацию 1+: На самом деле этого достаточно, чтобы записать нашу работу на диск. Как и в случае с load, нам нужно создать пакет адресов дисков, а затем вызвать int 0x13. Одно примитивное слово может служить как для чтения, так и для записи, поскольку единственная разница заключается в значении, которое AXвам нужно. Крайне важно сохранить SI— я имел неудовольствие узнать это эмпирически . Обратите внимание, что мы сохраняем возвращаемое значение AXback в стеке. Это связано с тем, что ненулевое AHзначение сигнализирует об ошибке. Чтобы заполнить packetданные, я использую вариант, ,который записывает в контролируемое место вместо here: Для чтения мы используем AH = 0x42, как и раньше. Запись использует AH = 0x43, но в этом случае значение определяет, ALхотим ли мы, чтобы BIOS проверял запись — мы делаем это, поэтому я установил для него значение 0x02. Было бы неплохо убедиться, что наш новый код собран правильно, прежде чем запускать его. В идеале мы бы написали небольшую утилиту hexdump, но у нас пока нет никакого способа зациклиться. Однако есть способ обойти это — просто введите нужное слово несколько раз подряд: Еще одна хорошая проверка работоспособности — убедиться, что ничего, чего мы не ожидали, не было помещено в стек — обычно это происходит из-за того, что неопределенные слова плохо преобразуются в числа. Это можно сделать следующим образом dup u.: пустой стек приведет к ответу E0E, происходящему из-за безобидного опустошения стека, которое мы только что вызвали. Одним из примеров обнаруженной ошибки является опечатка, когда я набрал movb-it,вместо movb-ir,. Первый доступ к диску, который я попробовал, был 0 4000 read-block u. 41fe @ u.. Это показывает AA55магическое число в конце загрузочного сектора. Затем я записал свой исходный код в блоки 1 и 2 и прочитал их в отдельный буфер, чтобы убедиться, что он работает. Оглядываясь назад, возможно, было бы неплохо прочитать блок, отличный от 0, перед попыткой записи, чтобы убедиться, что предоставление LBA работает правильно. К счастью, эта конкретная ошибка была чисто гипотетической. Я также записал те же данные в блоки 0x101 и 0x102. Таким образом, я могу восстановиться, если когда-нибудь сломаю загрузку с обычными номерами блоков. Прежде чем мы займемся реализацией ветвей, нам понадобится еще одна инструкция — условный переход. В x86 смещения перехода кодируются как значение со знаком относительно текущего указателя инструкции. Существуют разные кодировки для разной разрядности этого значения, но нам понадобится только более короткая 8-битная. Чтобы быть конкретным, значение относится к концу инструкции перехода, так что оно соответствует количеству пропущенных байтов в случае перехода вперед: Чтобы составить дистанции прыжков, я использую две пары слов — одну для прыжков вперед, а другую для прыжков назад: Правило состоит в том, что стрелки показывают направление прыжка, и стрелки должны быть «внутри» — другими словами, если вы получили два слова рядом друг с другом, эти стрелки должны подойти как влитые. Два слова просто общаются через стек. Например, j<просто запомнит текущее местоположение: Затем это используется функцией <j, которая вычитает текущую позицию и компилирует смещение: Для прыжков вперед мы составляем фиктивное смещение, чтобы переписать его позже: Наконец, сами инструкции по прыжкам. Некоторые прыжки имеют несколько названий. Например, поскольку флаг переноса устанавливается, когда при вычитании требуется заимствование, a jcведет себя точно так же, как беззнаковое сравнение jump, если below. То же самое относится к jeи jz, но это достаточно интуитивно для меня, поэтому я не чувствовал необходимости определять оба имени. При компиляции в определение ветки выглядят так: По соглашению имена слов, которые компилируются в определение, но не используются напрямую, заключены в круглые скобки, поэтому наши ветки называются (branch), что является безусловным, и (0branch), что выталкивает значение из стека и выполняет ветки, если оно равно нулю. Мы можем прочитать цель перехода из последовательности указателей с помощью lodsw: В случае условного перехода важно всегда читать (или пропускать) цель перехода, независимо от того, верно ли условие перехода. Для обработки вычисления смещения ветвления я использую набор слов, очень похожий на те, которые используются для переходов. Однако реализация проще, поскольку кодировка не относится к текущей позиции: Чтобы логика, которая компилирует ifs, действительно выполнялась во время компиляции, нам нужно пометить эти слова как немедленные. Для этого мы используем immediate, который устанавливает непосредственный флаг последнего скомпилированного слова: Нам также понадобится compile, который при вызове как compile xдобавляется xк слову, которое компилируется в данный момент. На самом деле нам не нужно делать его непосредственным словом, которое само по себе анализирует следующее слово, достаточно просто прочитать адрес xlike litили do it:(branch) ifэто просто условная прямая ветвь: elseнемного сложнее. Нам нужно скомпилировать безусловную ветвь, переходящую к then, а также разрешить ifпереход к точке сразу после безусловного перехода. Мне нравится использовать для этого слова манипулирования стеком возврата, поскольку стрелки совпадают со стрелками, используемыми >brи облегчают чтение кода: Далее нам понадобятся циклы. Во-первых, простой begin ... againбесконечный цикл: begin ... untilне намного сложнее — просто используйте условный переход в конце: Наконец, уникальный цикл Форта begin ... while ... repeat, где условие цикла находится в середине цикла: Ветвление не так уж полезно без возможности сравнивать вещи. Одной из общих проблем среди всех слов сравнения является превращение флага процессора в логическое значение Forth. Вспомним, что в Форте все биты логических значений либо установлены, либо не установлены: Я решил сгенерировать эти логические значения, сначала установив axзначение 0, а затем, возможно, уменьшив его на основе результата сравнения. Этот код можно разложить следующим образом: Затем, чтобы определить сравнение, вам просто нужно скомпилировать переход, который будет выполнен, когда результат должен быть ложным: Для более сложных условий у нас есть типичные логические операторы. Пока мы используем правильно построенные булевы значения, нет необходимости выделять отдельный набор чисто логических операторов — побитовые работают нормально. До сих пор многие слова, которые могли бы улучшить рабочий процесс редактирования кода в памяти, просто невозможно было определить, поскольку они по своей сути используют цикл. Это меняется сейчас. Во-первых, давайте определим type, который печатает строку: В моем текущем рабочем процессе последний определенный блок завершается нулевым байтом. Поиск этого места необходим для добавления элементов в этот блок. Вот что seekделает: Затем он используется для appendingустановки srcposи checkpointсоответствующим образом: Еще одна полезная вещь — возможность показать содержимое блока и быстро оценить адрес конкретной точки в коде — поскольку у нас нет настоящего редактора кода, это первый шаг любого патчинга. Я решил отображать блоки в обычном формате 64 на 16, хотя мои блоки никак не форматируются, а токены часто охватывают такие разрывы строк. Вместо номеров строк я печатаю адрес, с которого начинается каждая строка. Это и проще реализовать, и полезнее. Во-первых, у нас есть show-line, который показывает одну строку: Затем он вызывается 16 раз в цикле с помощью list. Поскольку у нас еще нет do- loop, это довольно сложно. Иногда нам нужно переместить некоторый код. Для этого нам понадобится move. Это похоже на C memmoveв том, что он копирует данные с правого конца, так что результат правильный, даже если источник и место назначения перекрываются. Мы реализуем это с помощью инструкции x86 rep movsb, которая, по сути, состоит memcpyиз одной инструкции. Давайте пока обучим ассемблер остальным строковым инструкциям: Затем он используется cmove, который копирует данные вперед. 5 Эта оболочка несколько длинная, так как нам нужно сохранить значения siи di. Далее нам понадобится cmove>, который начинается с конца буферов и копируется в обратном направлении. Стрелка >указывает, что это правильное слово для использования, если данные должны быть перемещены по более высокому адресу, т. е. вправо. Чтобы запустить такую копию, мы запускаем ее rep movsbс включенным флагом направления x86. Это предполагает, что регистры будут содержать адреса концов буферов , поэтому мы запускаем саму копию в (cmove>), а потом фактическое cmove>отвечает за вычисление правильного адреса. Наконец, moveсравнивает два адреса и выбирает подходящее направление копирования: Реализация fillпохожа, и она полезна для стирания любых остатков после moves. Еще одна проблема при редактировании кода — определить, было ли уже написано конкретное слово. Для этого я написал words, который выводит список слов, известных системе. Напомним структуру словаря: Заголовки слов содержат имя в виде строки с подсчетом, что означает, что первый байт хранит длину. countберет указатель на такую подсчитанную строку и превращает ее в типичное addr lenпредставление: Учитывая указатель на заголовок словаря, >nameизвлечет из него имя: Нам также нужно проверить скрытый флаг, так как в противном случае мы столкнемся с именами мусора, введенными нашими трюками со сжатием . Нашими последними вспомогательными словами являются space, которое просто печатает пробел, и #bl(обозначает пробел ), представляющее собой константу, в которой хранится ASCII-значение пробела. 6 Все это затем используется функцией words-in, которая принимает указатель на первое слово в списке. Это облегчит адаптацию, как только наш Форт получит поддержку словарей. 7 Со временем мне нужно будет заменить внешний интерпретатор codegolf на другой, написанный на Forth. Это позволит мне добавить такие вещи, как правильная обработка неопределенных слов, знакомая okподсказка, а также словари и обработка исключений. 8 Первым шагом к этому является parse ( delim -- addr len )слово, которое будет анализировать ввод до тех пор, пока не встретится указанный символ-разделитель. Для обычного синтаксического анализа это будет пробел, но если мы установим его равным ), у нас, наконец, появятся комментарии. parseхранит разделитель в переменной, чтобы его могли использовать вспомогательные слова. Разбор может завершиться из-за того, что мы нашли разделитель или из-за того, что у нас закончились входные данные, что обозначается байтом NULL. После завершения цикла разбора у нас будет указатель на разделитель. Если это настоящий разделитель, мы хотим удалить его из ввода — в конце концов, он )не существует как слово. Однако, если мы остановились из-за того, что ввод закончился, то мы не должны продвигаться дальше нулевого терминатора. +sepобрабатывает это и продвигает указатель ввода только в том случае, если он не указывает на нулевой байт. Цикл parseудерживает два указателя в стеке. Один не двигается и отмечает начало жетона. Другой продвигается в каждой итерации. В конце мы сохраняем перемещенный указатель в >in, а затем вычисляем длину, вычитая два указателя. Это работает, когда мы хотим проанализировать комментарий, но для анализа слова нам действительно нужно сначала пропустить начальные пробелы. Это обрабатывается skip, который также принимает разделитель в качестве аргумента и продвигается вперед >in, чтобы больше не указывать на разделитель. tokenсочетает в себе два. Затем он используется функцией char, которая возвращает первый символ следующего токена — мы можем использовать его как символьные литералы. Однако, чтобы включить такой литерал в скомпилированное слово, нам нужно : Наконец, (это непосредственное слово, которое запускает )синтаксический анализ с разделителями и отбрасывает результат. Как только загрузочный сектор Miniforth был готов, я решил выполнить всю свою разработку и тестирование на компьютере с «голым железом». В общем, я не жалею об этом решении, но мне потребовалось несколько попыток, чтобы сохранить код на диске. Однако я сфотографировал код, поэтому повторная попытка потребовала повторного ввода около килобайта исходного кода. Несколько попыток были испорчены опечатками, сделанными во время этой транскрипции, но помимо этого у меня было две ошибки, о которых стоит упомянуть. В первый раз я попытался реализовать ветки перед записью на сам диск. Сам код ветвления был в порядке, но мой тест на зацикливание - нет. Попробуйте найти ошибку: Показать подсказку Код вылетел вот так: Показать решение Потребляет 0=счетчик циклов в стеке, что означает, что каждая итерация опускается дальше в стек. Поскольку защиты от этого нет, код будет выталкивать что-то до тех пор, пока не начнет перезаписывать код, достаточно важный, чтобы вызвать сбой. Другая ошибка была в int13самом слове. Как я упоминал ранее, я забыл сохранить SI, который является указателем выполнения для многопоточного кода. Он рухнул, как только запустился next. По иронии судьбы, если бы я не был так осторожен и не выполнил write-blockсвою первую операцию, большая часть кода была бы сохранена 🙃 Помимо ошибок, есть еще одна проблема с запуском на «голом железе»: публикация кода на GitHub. Я решил эту проблему, написав скрипт, splitdisk.pyкоторый извлекает код из образа диска. Поскольку я загружаюсь с USB-накопителя, получение кода не займет много времени. Поскольку там нет разрывов строк, я написал эвристику, чтобы разбить его на строку для каждого определения. Благодаря Python difflibон даже сохраняет любые корректировки форматирования, сделанные вручную при извлечении обновленной версии блока. Теперь, когда у нас есть ветвления, многие вещи становятся доступными в качестве потенциального следующего шага. Одной из важных целей является написание текстового редактора, но некоторые другие улучшения нашего Форта, вероятно, должны быть сделаны первыми. Между тем, я рекомендую вам попробовать выполнить начальную загрузку поверх Miniforth вплоть до ветвления  _без_использования каких-либо дополнительных собранных примитивов. Не то чтобы в таком ограничении себя есть смысл, но это интересная проблема. Я объясню свое решение через неделю, а также любые существенно отличающиеся решения, найденные такими читателями, как вы. Я создал отдельную ветку для обсуждения этой проблемы, поэтому, пожалуйста, избегайте спойлеров в комментариях под статьей 🙂 Возможно, вам понравятся и другие мои посты . Если вы хотите получать уведомления о новых, вы можете подписаться на меня в Твиттере или подписаться на RSS-канал . Я хотел бы поблагодарить моих спонсоров GitHub за их поддержку: Michalina Sidor и Tijn Kersjes. 1 Есть небольшое отличие от стандартного поведения в том, что my loadпросто перенаправляет входной указатель, и блок будет выполняться только один раз, непосредственно перед тем, как выполнение достигнет REPL верхнего уровня.↩ 2 Теперь до меня дошло, что причина, по которой его обычно называют, >inзаключается в том, что обычно это смещение ( >), которое добавляется к отдельному базовому адресу входного буфера. Ну что ж.↩ 3 Впрочем, это не было бы катастрофой. Повторный запуск такого кода является нормальным явлением после исправления ошибки.↩ 4 Если бы это было что-то вроде mov [0x1234], 0x5678, то использовался бы точный порядок: код операции, ModR/M, адрес, немедленный.↩ 5 Это не лучшее название, так как это не то, что программист на C назвал бы "движением", но это то, что использует стандарт Forth, так что я буду придерживаться его.↩ 6 Теперь до меня доходит, что я мог бы определить это constantвместо этого. Я, вероятно, изменю это, когда загрузлю подходящий текстовый редактор.↩ 7 Словари Forth, также известные как списки слов, представляют собой способ разделения слов на несколько отдельных «словарей», которые можно динамически добавлять и удалять из порядка поиска.↩ 8 Точный аспект, который меня интересует, - это установка обработчика верхнего уровня для исключений, которые не перехвачены. Вероятно, есть способ заставить этот конкретный аспект работать без нового внешнего интерпретатора, но другие преимущества все еще существуют, поэтому нет особого смысла пытаться понять это.↩