Назад в машинный зал: как собрать эмулятор PDP-11/70 с телетайпом, блэкджеком и без Rust — собери себе загрузчик сам


Представьте компьютер, который после включения не делает вообще ничего. Буквально. В 1975 году это было нормой: постоянная память стоила дорого, и операторы вручную щелкали тумблерами, чтобы оживить машину.
Как я докатился до рекурсивной компиляции?
Если вы попали сюда случайно, вот краткая хронология моего помешательства:
В начале была магия (Часть 1): Мы просто запустили PDP-11 в браузере. Играли в Moon Lander, слушали стрекот принтера. Читать первую часть →
Затем пришла суровая проза (Часть 2): Пришлось объяснять читателям, почему WebView ненавидит CRT-эффекты, а Tauri заставляет танцевать с бубном вокруг драйверов Windows. Читать вторую часть →
А потом… (Текущая статья): Я понял, что собирать код хостовым кросс-компилятором — это моветон. Пусть эмулятор мучается сам.
Но что, если пойти дальше обычной эмуляции? Зачем собирать загрузчик для ретро-компьютера на современном ПК через кросс-компиляторы, если внутри самого эмулятора крутится настоящая операционная система RT-11 с аутентичным компилятором от DEC?
В этой статье мы провернем инженерный инсепшен: заставим эмулятор PDP-11/70 в браузере собрать свой собственный бутлоадер «родным» инструментарием полувековой давности. Будет чистый ретро-хардкор: передача файлов через виртуальные перфоленты, битва за каноничный консольный промпт @ вместо исторической ереси и суровая экономика золотого века IT. Змея, кусающая себя за хвост — но в самом лучшем смысле.
В первой статье я рассказывал, как yaPDP — мой эмулятор PDP-11/70 в браузере — начал «помогать себя разрабатывать»: я взял boot.mac, скормил его host-сторонним macro11.exe и получил свежий бутлоадер. Работало, но оставался осадок: ассемблер-то был неродной, эмуляция DEC-инструментария на хосте. Как-то это… не по-канону.
А потом я понял: стоп. У меня же в браузере крутится настоящая PDP-11/70. На ней можно запустить настоящий RT-11. А на RT-11 живёт настоящий DEC MACRO-11. Зачем мне host-ассемблер, если у меня в кармане целый машинный зал?
Так родилась идея, от которой у любого ретро-энтузиаста загораются глаза: собрать новый бутлоадер оригинальным инструментарием DEC, запущенным внутри эмулятора, который этот самый бутлоадер и загружает. Змея, кусающая себя за хвост, — но в хорошем смысле.
Канон против «Boot>»
Сначала — зачем вообще пересобирать загрузчик. Доставшийся мне в наследство bootstrap (вместе с ядром эмулятора) был… скажем так, творческим. Это был монстр на 2008 слов: сам загрузчик, прилинкованный ODT11 (DEC On-line Debugging Tool), демонстрационные процедуры LIGHTS и CHASER для мигания лампочками на панели, MMU-демо и приглашение Boot>.
И вот тут — главная ересь. Настоящие загрузчики DEC никогда не печатали Boot>. Канонический промпт bootstrap-ромов PDP-11 — это лаконичное @. Именно его ждал оператор, именно под него написана вся документация той эпохи.
Кстати, забавный факт: приведя загрузчик к канону, я впервые увидел на экране два @ подряд (@boot rk0 → @unix) и на секунду задумался — не смутит ли это пользователя. Не смутит: это ровно та картина, которую видел оператор настоящей машины. «Boot>» — это как если бы в книжке по MS-DOS переименовали C:> в «Готов к работе!».
Почему именно @?
Символ `@`, известный сегодня как «собака», имеет долгую историю. В торговле он использовался для обозначения предлога *at* («по [цене]»), что отражено, например, в записи: «3 lbs. bananas @ $.50/lb.» — три фунта бананов по цене $0,5 за фунт. В операционной системе **RT‑11**, разработанной компанией DEC для PDP‑11, этот символ стал стандартом для приглашения загрузчика. Он был лаконичным, легко распознаваемым на экране и не занимал много места в памяти, что было критично в те времена.
Когда Деннис Ричи и Кен Томпсон переносили Unix на PDP‑11, они сохранили многие элементы интерфейса от RT‑11.
Для Кена Томпсона, который стремился сэкономить каждый байт в крошечном 512-байтном первичном загрузчике, символ @ был идеален с точки зрения ассемблера PDP-11.
В восьмеричной системе счисления (которая была родной для PDP-11) ASCII-код символа @ равен 0100 (в шестнадцатеричной — 0x40).
Круглое число 0100 в ассемблере обрабатывать и проверять гораздо проще и «дешевле» по объему кода, чем, например, знак доллара $ (044) или процента % (045).
Интересно, что была и “аппаратная” причина: телетайпы подключались к PDP-11 через последовательные интерфейсы (токовая петля 20 мА). При включении компьютера, перезагрузке или сбросе линии в линию связи часто проскакивал электрический шум — «мусорные» байты.
Код 0100 (@) имеет очень специфическую битовую маску в ASCII (ровно один установленный бит в седьмом регистре: 01000000 в двоичной системе).
На фоне случайных шумов, которые могли принимать значения 0177 (ручной сброс/DEL) или 0000 (NULL), символ @ был максимально четким сигналом готовности, который сложно перепутать с помехами.
Кстати, В самих Unix V1–V5 символ @ имел фундаментальное значение — он использовался как управляющий символ Line Kill (стереть всю строку). Если пользователь ошибался при вводе команды, он нажимал @, и драйвер терминала понимал, что всю текущую строку нужно аннулировать и начать ввод заново.
Выведя @ на экран, загрузчик как бы транслировал пользователю: «Вся память очищена, предыдущее состояние сброшено, перед вами абсолютно чистая строка — вводите имя ядра».
План канонизации:
Промпт @ вместо Boot>
Команды только BOOT и HELP — без LIGHTS/CHASER/ODT
ODT11 — отдельным модулем (он ещё пригодится, я его сохранил)
Пустой баннер — тихий старт, без пафосных надписей
Размер: 2008 слов → 673 слова
История человеческой лени (и почему мы здесь)
Сейчас трудно представить, что включённый компьютер сразу после старта… не делал ничего, но это было именно так.
В те времена сделать память небольшого объёма было можно, но она была банально дорога, а часто и ненадёжна.
Первое решение — набор переключателей или диодная матрица.
Минимальный загрузчик с перфоленты Model 33 ASR занимает от 12 до 16 слов. Это 256 переключателей плюс обвязка. Вся эта дорогая плата почти не использовалась — компьютер стоил как крыло самолета, его старались не перезагружать без нужды.
Время техника, который изредка вбивает пару десятков слов с инженерного пульта и заправляет перфоленту со вторичным загрузчиком, стоило дешевле для бизнеса. Позже появились ёмкие микросхемы ROM/PROM (платы BM873, M9312), которые решали ту же задачу аппаратно, используя арбитраж шины UNIBUS.
Но операторы 70-х всё равно ленились. Они автоматизировали ввод второго промпта двумя путями:
Патч исходника: правили цикл ожидания ввода в файле boot.s Unix V5/V6. Если строка пустая — подставлять имя ядра unix.
Аппаратный обман: модифицировали вторичный загрузчик так, чтобы он читал состояние тумблеров Switch Register (адрес 177570). Определенная комбинация битов означала автозапуск файла /unix.
Гимн лени
Честно говоря, мне эта идея нравится настолько, что я её реализую в будущем. Когда-нибудь.
Позже в модифицированный исходник загрузчика стали добавлять тайм-аут, после которого автоматически “нажимался Enter” и процесс загрузки стартовал сам.
Это уже напоминает современные нам загрузчики (привет, GRUB!).
Перфолента — мост между эпохами
Осталась техническая проблема: как передавать файлы между хостом и гостевым RT-11?
В 1975 году ответ был один — перфолента. И я уже добавил в эмулятор честный перфоратор и перфосчитыватель.
Ленты, экспортированные из эмулятора, можно заправить и в симулированный, и в настоящий телетайп. Значит, и в обратную сторону — тоже.
Получился конвейер, до смешного похожий на то, что делали операторы полвека назад:
Исходник boot.mac на хосте превращается в ленту (файл .ptap)
Лента «заправляется» в перфосчитыватель эмулятора
Гостевая RT-11 читает её: COPY PC: BOOT.MAC
Дальше всё делает настоящий DEC-инструментарий — MACRO-11 V04.00C и LINK V06.01H
Готовый .SAV выводится на перфоратор
Лента экспортируется на хост и превращается в bootcode.js
Мой терминальный клиент получил команды нового поколения: :mount, :export, :raw. Связь с внешним миром — через дырочки.
headless-term.js — консольный мост
Вся магия управляется одной утилитой — tools/headless-term.js. По сути это мост между моим терминалом и консолью гостевой RT-11. Строки, начинающиеся с :, утилита забирает себе:
:mount <file>— заправить перфоленту:rewind— перемотать ленту:export <file>— вынуть накопленное с перфоратора:wait <marker>— ждать маркер:raw <hex>— отправить сырые байты (например, :raw 03 — Ctrl+C)
Промпт распознаётся хитро: точка в начале строки плюс ~300 мс тишины. Оператор 1975 года оценил бы эту автопаузу.
Конвейер сборки
Вот полный листинг того самого скрипта, который я скормил утилите пайпом:
:mount boot.mac.ptap
COPY PC: BOOT.MAC
R MACRO
DK:BOOT.OBJ,DK:BOOT.LST=DK:BOOT.MAC
R LINK
DK:BOOT.SAV=DK:BOOT/B:120000
COPY DK:BOOT.SAV PC:
:export boot.sav.bin
Красота в том, что ассемблирует и линкует не эмуляция, а оригинальные бинарники DEC. Я даже получил настоящий листинг .LST — и по нему проверил все 17 адресных целей макроса LDA. PRTAST=120514, INPAST=120620, EOL=120273 — красота, а не код.
Скрипт tools/bin2bootdump.js превращает бинарник в массив восьмеричных слов для памяти по адресу 0120000.
Естественно, чтобы не вводить эту последовательность каждый раз руками, есть команда npm run rebuild-boot, которая вызывает достаточно немудрящий скрипт tools/rebuild-bootcode.js, автоматизирующий операцию. tools/headless-term.js в основном используется для интерактивной работы и исследований.
Грабли, на которые я наступил
Разумеется, без них никуда. Три истории, достойные мемуаров.
Перфоратор резал байты до 7 бит. Первый экспортированный .SAV упорно не декодировался: вместо C1 15 я видел 41 15. Оказалось, хук экспорта ленты делал byte & 0x7f — для красоты визуала телетайпа Model 33 ASR. Но физически-то лента 8-дорожечная! Фикс — одна строчка (byte & 0xff), зато теперь понятно, почему наши предки трепетно относились к переключателю чётности.
Формат .SAV от LINK V06.01H. Оказалось, это чистый образ памяти без заголовка: смещение в файле = адрес в памяти. Никакого 6-словного заголовка — вот тебе и «документация» сорокалетней давности.
ODT11 и символы с точкой. Когда я вынес ODT11 в отдельный модуль, ассемблер начал выдавать загадочное EQ 0 на символе ODT11S. Виновник найден — forward-ссылки на символы с точкой в имени (вроде s.t), которые MACRO-11 прощал не всегда. Знание, за которое я заплатил парой часов и которое теперь живет в macro-asm/README-odt11.md.
И напоследок — мелочь, но показательная: при склейке шапки файла я умудрился получить дважды var bootcode=[. node --check поймал мгновенно, но осадочек остался: даже в 2026-м синтаксические ошибки случаются из-за tail -n +2.
Боевой приём
Новый загрузчик прошёл полный цикл проверки: юнит-тесты (35 файлов), e2e-снапшоты, и — самое вкусное — живой прогон. Вот он, канон, на экране:
@
VALID COMMANDS ARE BOOT OR HELP
@boot rk0
@unix
login: root
Видите эти два @? Это не дубликат и не баг. Это 1975 год собственной персоной: первый @ — загрузчик DEC ждёт команду, второй — Unix V5 спрашивает имя ядра. Всё как на настоящей машине.
А по дороге я заодно:
починил перфоратор — теперь из эмулятора можно вывозить любые бинарники;
выяснил, что в RT-11 V04 .TTYOUT — это EMT 341, а не EMT 1;
получил инструмент bin2bootdump.js для любых образов PDP-11.
Итог
2008 слов → 673. Промпт @ вместо Boot>. ODT11 — отдельным модулем. Тихий старт без баннера. Всё собрано оригинальным DEC MACRO-11 V04.00C, запущенным внутри эмулятора, а наружу вывезено через перфоленту — как и положено.
Теперь эмулятор собирает свой загрузчик не «почти по-настоящему», а совсем по-настоящему: тем же инструментарием, теми же руками и тем же носителем, что и полвека назад. Единственная уступка современности — перфоленту из эмулятора не нужно нести через весь машинный зал: она сама становится файлом на диске.
Если хотите попробовать прямо сейчас — перейдите по ссылке https://amesk.github.io/yaPDP/pdp11.html, нажмите “Bootstrap now!” и увидите канонический @.
Исходники: github.com/amesk/yaPDP — macro-asm/boot.mac, macro-asm/README-odt11.md, tools/bin2bootdump.js, tools/headless-term.js, tools/rebuild-bootcode.js. Сайт проекта: https://amesk.github.io/yaPDP.
Путеводитель по циклу эмулятора yaPDP
Чтобы вы не заблудились в машинном зале семидесятых, вот хронология проекта:
Часть 1. Магия запуска «Эмулятор PDP-11/70 в браузере: запуск UNIX V5, телетайп и Lunar Lander» ➡️ Перейти к первой части
Здесь мы впервые оживили старую железку прямо в Chrome.
Часть 2. Суровая правда фронтенда «Как построить прокуренную серверную 70-х внутри браузера: Tauri, WASM и SharedWorker» ➡️ Перейти ко второй части
Архитектурные компромиссы, ад WebView и борьба за миллисекунды звука.
Часть 3. Змея кусает хвост (Текущая статья) «Эмулятор, который собрал свой загрузчик сам: оригинальный MACRO-11 внутри RT-11» ➡️ Вы уже здесь
Рекурсивная компиляция через виртуальную перфоленту и канонический промпт @.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.