Ещё полгода назад я всерьёз считал, что собственная операционная система — слишком большой проект для одного человека, который может заниматься ею только в свободное от основной работы время. Для старта нужно было одновременно разбираться в AArch64, MMU, исключениях, прерываниях, планировании, ELF, CPIO — и при этом как-то отлаживать код в среде, которой ещё толком нет.

Сейчас мой экспериментальный проект genrt загружает high-half-ядро в QEMU, запускает вытесняющий планировщик, пользовательские процессы на EL0 и интерактивную командную оболочку из initramfs. В этой статье я расскажу, как пришёл к такому результату, где именно мне помогли ИИ-инструменты и почему это всё равно не было историей про «написал промпт — получил ОС». Во второй половине статьи разберу архитектуру genrt, последовательность загрузки и ограничения, которые пока не позволяют называть систему полноценной RTOS.

Что получилось за 4 месяца

Начну с результата. genrt — небольшая экспериментальная операционная система для AArch64. Ядро написано преимущественно на Rust, ранняя загрузка и переключение контекста — на AArch64-ассемблере, а пользовательские программы — на freestanding C.

На момент релиза v0.1.0-alpha.2 в проекте около 13 тысяч строк кода ядра, 4,7 тысячи строк инфраструктуры и тестов и ещё примерно 600 строк userspace. Само по себе количество строк мало о чём говорит, но масштаб уже достаточный, чтобы пройти полный путь от собственной точки входа до запуска отдельных ELF-программ.

Пока целевая платформа у проекта одна: одноядерная виртуальная машина QEMU virt с Cortex-A72 и GICv2. Сейчас genrt умеет:

  • загружаться на AArch64 и переходить из физически адресуемого boot-кода в high-half-ядро;

  • строить отдельные виртуальные адресные пространства ядра и пользовательских процессов;

  • вытесняюще планировать потоки по алгоритму Round Robin;

  • обрабатывать таймерные прерывания, исключения из EL0 и ввод с UART;

  • монтировать read-only initramfs в формате CPIO newc;

  • загружать статические ELF-файлы и запускать их на EL0;

  • выполнять цепочку fork → execve → waitpid;

  • запускать интерактивную командную оболочку и небольшие утилиты echo, cat, ls и pwd;

  • проходить воспроизводимые QEMU-тесты локально и в CI.

Долгосрочное направление проекта — эксперименты в области жёсткого реального времени. Но считать текущую версию genrt полноценной RTOS было бы преждевременно: пока нет приоритетного планирования, измеренных верхних границ задержек и даже поддержки реальной аппаратной платформы. Это цель развития, а не характеристика текущего релиза.

Пример загрузки и работы в shell
$ cargo xtask run-aarch64
...
[INFO ] memory: switched to runtime kernel page tables; TTBR0 cleared
[INFO ] initramfs: mounted 8 files, 3 directories
[INFO ] sched: irq-return preemptive switching initialized
[INFO ] init: spawning first EL0 process
[INFO ] init: loading /init from initramfs

genrt shell
> ls
bin
etc
hello.txt
init
readme.txt
> cat /etc/banner
genrt initramfs
> cd bin
> pwd
/bin
> ls
cat
echo
ls
pwd
> exit
[INFO ] init: user process exited code=0

Почему я вообще взялся за свою ОС

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

Тогда же появилась мысль когда-нибудь написать собственную ОС. Правда, довольно долго она оставалась именно мыслью. Причины были приблизительно следующими:

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

  • ошибки в ядре трудно локализовать, особенно пока нет привычных средств диагностики;

  • сделать проект основной работой я не мог;

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

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

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

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

Как ИИ встроился в разработку

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

Чат как интерактивный учебник и собеседник по архитектуре

Больше всего времени чат сэкономил мне там, где обычный поиск быстро превращается в десятки вкладок. Например, при изучении AArch64 я мог начать с общего вопроса про уровни исключений, затем перейти к конкретным регистрам, таблице векторов, устройству GICv2 или последовательности включения MMU — и уточнять каждое непонятное место, не теряя общий контекст.

Обычно я использовал чат, чтобы:

  • разбирать архитектуру AArch64 — от системных регистров до MMU, GICv2 и обработки исключений;

  • сравнивать несколько вариантов устройства новой подсистемы и заранее обсуждать их слабые места;

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

  • делить большой замысел на небольшие этапы;

  • превращать выбранное решение в техническое задание с инвариантами и критериями приёмки;

  • анализировать результаты отладки, когда наблюдаемое поведение не совпадало с моей моделью системы.

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

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

Агенты как разработчики, тестировщики и ревьюеры

Для работы непосредственно с репозиторием я использовал Codex. Со временем у меня сложилось несколько ролей:

Роль

Что делает

Исследователь

Находит реальные точки входа, вызовы и зависимости между затронутыми подсистемами

Архитектор

Формулирует границы решения, инварианты, варианты реализации и необходимые ADR

Разработчик

Вносит изменения в основной код по согласованному техническому заданию

Тестировщик

Проектирует проверки, добавляет тесты и разбирает сбои

Ревьюер

Ищет дефекты, нарушения инвариантов, архитектурных границ и стиля проекта

Это не означает, что пять агентов одновременно правят одни и те же файлы. Такой режим быстро привёл бы к конфликтам и размыванию ответственности. Исследование и проектирование можно выполнять параллельно, но основной код меняет один разработчик. После этого результат отдельно смотрят тестировщик и ревьюер.

Типичный цикл выглядит примерно так:

идея этапа
    ↓
изучение теории и существующего кода
    ↓
архитектурное решение и критерии приёмки
    ↓
реализация одним агентом
    ↓
format / lint / build / QEMU-контракты
    ↓
независимое ревью
    ↓
исправления → повторная проверка → merge

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

Какие решения я не делегирую

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

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

Иными словами, ИИ расширил объём работы, который я могу сделать один, но не снял с меня ответственность за результат.

Что пришлось построить вокруг агентов

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

Поэтому часть инфраструктуры genrt посвящена не самой ОС, а тому, чтобы следующий исполнитель — человек или агент — мог восстановить правила проекта из репозитория.

Правила в AGENTS.md

В корневом AGENTS.md лежат общие правила работы:

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

  • какие архитектурные и RT-инварианты нельзя нарушать;

  • какими командами собирать и проверять проект;

  • как разделяется работа между агентами;

  • когда нужно обновлять документацию и ADR;

  • что считается завершённой задачей.

Для отдельных частей дерева правила уточняются во вложенных файлах. Например, kernel/AGENTS.md описывает допустимые контексты выполнения, запрещает аллокации в IRQ и требует держать доступ к системным регистрам, архитектурным инструкциям и MMIO внутри AArch64-слоя.

Такая вложенность оказалась удобной. Агент, который меняет echo, не обязан загружать в контекст все детали планировщика. Но при работе с IRQ-обработчиком локальные ограничения ядра уже нельзя случайно пропустить.

Skills как готовые рабочие процедуры

AGENTS.md отвечает на вопрос «какие правила здесь действуют», а skills — «как обычно выполнять такой класс задач». В описании агентного процесса сейчас зафиксировано шесть таких процедур:

  • genrt-change-workflow — путь нетривиального изменения от исследования до закрытия задачи;

  • genrt-qemu-test — добавление и изменение QEMU-контрактов и тестового машинного протокола;

  • genrt-verify — выбор проверок в зависимости от риска изменения;

  • genrt-review — независимое ревью с упором на конкретные дефекты и доказательства;

  • genrt-adr — создание и замещение архитектурных решений;

  • genrt-docs-sync — поиск документации, которую нужно обновить вместе с кодом.

По сути, skill — это короткая воспроизводимая инструкция или чек-лист. Это не отдельная роль и не обязательный ритуал для каждой правки. Например, изменение только документации не требует полного запуска QEMU, а изменение syscall ABI должно одновременно затронуть userspace-заголовки, документацию и интеграционные контракты.

Память проекта вместо памяти конкретного чата

Всё, что должно пережить отдельную сессию с моделью, хранится в репозитории:

  • memory/current-state.md описывает уже реализованные возможности и текущие границы;

  • memory/invariants.md собирает сквозные инварианты;

  • memory/decisions/ содержит ADR и историю архитектурных решений;

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

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

Одна точка входа для сборки и тестов

Единой точкой входа в сборку, запуск, тестирование и выпуск артефактов служит xtask:

# Проверить форматирование, lint, тесты инструментов и обычную сборку
cargo xtask check

# Показать список QEMU-контрактов и запустить их
cargo xtask test-aarch64 --list
cargo xtask test-aarch64

# Выполнить полный набор локальных и CI-проверок
cargo xtask ci

# Собрать образ и запустить интерактивную командную оболочку
cargo xtask run-aarch64

QEMU поднимает одноядерную AArch64-платформу без сети и графики. Ядро, DTB и initramfs загружаются как отдельные артефакты:

Упрощённая команда запуска QEMU
qemu-system-aarch64 \
  -machine virt,gic-version=2 \
  -cpu cortex-a72 \
  -smp 1 \
  -display none \
  -monitor none \
  -nic none \
  -serial stdio \
  -no-reboot \
  -kernel target/aarch64-unknown-none-softfloat/debug/genrt-aarch64.elf \
  -device loader,file=target/aarch64-unknown-none-softfloat/debug/qemu-virt.dtb,addr=0x40000000 \
  -device loader,file=target/aarch64-unknown-none-softfloat/debug/initramfs.cpio,addr=0x47000000,force-raw=on

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

Четыре QEMU-контракта

Проверять систему по произвольным строкам обычного лога оказалось ненадёжно: текст сообщения может измениться, хотя поведение останется прежним. Поэтому в тестовых образах используется отдельный машинный протокол GTRT/1, а релизные артефакты дополнительно проверяются на отсутствие его маркеров.

Сейчас есть четыре интеграционных контракта:

Контракт

Что он проверяет

kernel-contract

Планировщик, таймеры, ожидания, mailbox, аллокаторы и гонки между событием и тайм-аутом внутри специального тестового ядра

user-fault

Классификацию исключения из EL0 и завершение только процесса-источника без падения ядра

userspace-contract

Цепочку fork → execve → waitpid, файловые и процессные системные вызовы и запуск обычных утилит echo, cat, ls и pwd

shell-contract

Релизную командную оболочку, разбор argv, наследование cwd, UART RX и восстановление после неизвестной команды или ненулевого статуса дочернего процесса

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

Машинный протокол, состав тестовых образов и раннер на стороне хоста подробнее описаны в docs/testing.md.

Теперь о самой ОС

Агентная инфраструктура — не цель проекта сама по себе. Теперь о том, что получилось внутри ОС. Архитектурно genrt разделена на AArch64-зависимый слой, общее ядро и freestanding userspace. Более формальное описание текущих возможностей хранится в memory/current-state.md.

Основные подсистемы genrt и связи между ними
Основные подсистемы genrt

AArch64-слой

Архитектурный слой отвечает за раннюю загрузку, таблицу векторов исключений, вход и выход из trap-контекста, настройку MMU и доступ к системным регистрам. Там же находятся низкоуровневые части драйверов GICv2, PL011 и ARM Generic Timer.

Я старался не разносить архитектурные детали по общему коду ядра. Планировщик, менеджер памяти или подсистема процессов должны оперировать своими абстракциями, а не напрямую читать TTBR0_EL1 или программировать регистр таймера. Для единственной платформы это может казаться лишним слоем. Зато уже сейчас понятно, какой код придётся менять/дорабатывать при переносе на другую AArch64-плату.

Планировщик, время и ожидания

Планировщик пока довольно простой: одноядерный Round Robin без приоритетов. Единственная планируемая сущность — поток, который может находиться в состояниях Ready, Running, Blocked или Exited.

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

Вытеснение выполняется на возврате из IRQ. ARM Generic Timer работает в режиме one-shot: ядро каждый раз программирует его на ближайшее событие — окончание кванта, пробуждение после sleep или тайм-аут ожидания. События времени лежат в заранее выделенной deadline queue на основе минимальной кучи; сам IRQ-путь не меняет размер контейнеров и не выделяет память.

Для блокирующих операций используется WaitToken, в котором есть поколение потока и номер конкретного ожидания. Это защищает от неприятного класса ошибок: поздний тайм-аут или повторное пробуждение не должны воздействовать на уже следующее ожидание потока.

В ядре также есть блокирующий mailbox с тайм-аутом. Пока он недоступен из userspace, но используется для проверки общих механизмов блокировки и пробуждения.

Управление памятью

Во время ранней загрузки AArch64-код строит временные таблицы страниц, которых достаточно для включения MMU и перехода в high-half. После запуска аллокатора физических страниц ядро создаёт постоянные отображения TTBR1, переключается на них и очищает TTBR0 до запуска первого пользовательского процесса.

У каждого процесса собственное TTBR0-пространство. При fork пользовательская память копируется сразу. Copy-on-write и demand paging в проекте пока нет — это дороже в момент создания процесса, зато в текущей модели не переносит скрытое копирование и выделение памяти на более поздний page fault.

Размер кучи ядра задаётся при загрузке. Аллокации допустимы во время bootstrap и в обычном контексте потока, но запрещены в IRQ, ядре планировщика, обработчиках событий времени и на пути передачи trap frame между обработчиком исключения и планировщиком.

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

Процессы, ELF и системные вызовы

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

ELF loader принимает статический AArch64 ELF, проверяет заголовки, отображает загружаемые сегменты, строит пользовательский стек и передаёт управление на EL0 через ERET. Динамического загрузчика, ELF interpreter и libc в системе нет.

Syscall ABI точнее называть Linux-подобным и POSIX-ориентированным, а не POSIX-совместимым. В текущем диспетчере реализованы open, read, write, close, fork, execve, waitpid, getdents64, chdir, getcwd и exit, но только в тех вариантах, которые сейчас нужны проекту.

Например, getdents64 — Linux-специфичный интерфейс, waitpid принимает только конкретный положительный PID при options == 0, а запись в обычные файлы пока не поддерживается. Совпадение номеров и сигнатур отдельных вызовов ещё не делает систему POSIX-совместимой — и в документации я стараюсь не создавать такого впечатления.

Initramfs и userspace

QEMU загружает несжатый CPIO-архив newc в заранее зарезервированный диапазон физической памяти. При старте ядро один раз разбирает архив и строит индекс файлов и каталогов в read-only ramfs.

Полноценной VFS здесь пока нет. Нет writable-файлов, mount API, символьных ссылок, блочных устройств, page cache и большинства привычных метаданных. Файловая подсистема поддерживает абсолютные и относительные пути, текущий каталог, последовательное чтение и Linux-подобные записи dirent64. У каждого процесса есть таблица на 32 файловых дескриптора; первые три заняты stdin, stdout и stderr.

В обычном образе /init — это интерактивная командная оболочка. В /bin лежат отдельные статически слинкованные программы:

  • echo выводит аргументы в stdout;

  • cat читает файл;

  • ls перечисляет содержимое каталога;

  • pwd печатает текущий каталог.

Это, разумеется, сильно урезанные версии привычных утилит. cd реализована внутри командной оболочки (built-in utility): если запустить её как отдельный дочерний процесс, изменится каталог только этого процесса, а после его завершения родительская оболочка останется на прежнем месте.

Ввод приходит через ограниченный кольцевой буфер, который заполняет IRQ-драйвер PL011. Полноценной TTY-подсистемы и line discipline пока нет, поэтому интерфейс остаётся намеренно простым.

Как система доходит от _start до оболочки >

Вся цепочка загрузки в сокращённом виде выглядит так:

xtask → QEMU → _start → boot page tables → high-half
      → kernel_main → scheduler → /init → shell

Полноценного загрузчика здесь нет: его роль частично выполняют xtask и фиксированная конфигурация QEMU. Последовательность получается такой:

  1. xtask собирает kernel ELF и userspace, получает нормализованный DTB для QEMU virt и упаковывает initramfs.

  2. QEMU размещает DTB по физическому адресу 0x4000_0000, точку входа ядра — по 0x4008_0000, а initramfs — по 0x4700_0000.

  3. Низко расположенный trampoline _start оставляет активным только boot CPU, подготавливает физический bootstrap stack и вызывает Rust-код до включения MMU.

  4. Ранний Rust-код читает DTB по фиксированному адресу, строит временные таблицы TTBR0/TTBR1, включает MMU и переходит в high-half со смещением 0xffff_0000_0000_0000.

  5. rust_entry устанавливает VBAR_EL1, восстанавливает сведения о платформе и инициализирует UART, GICv2 и ARM Generic Timer.

  6. kernel_main запускает frame allocator и фиксированную кучу, создаёт постоянные TTBR1-таблицы, очищает TTBR0, монтирует initramfs и подготавливает планировщик.

  7. Планировщик запускает kernel_init_thread. Уже из этого потока ядро читает /init, создаёт для него TTBR0-пространство и начальный EL0-контекст, а затем блокируется в ожидании завершения процесса.

  8. Планировщик выбирает пользовательский поток, активирует его TTBR0 и через ERET передаёт управление в /init. Командная оболочка запускает внешние программы по цепочке fork → execve → waitpid.

Где система пока заканчивается

genrt уже проходит путь от boot-кода до интерактивного пользовательского окружения, но остаётся исследовательским проектом. Самые заметные ограничения сейчас такие:

  • только один CPU: нет SMP-планирования, IPI, TLB shootdown и межъядерных блокировок;

  • только QEMU virt: реальная ARM-плата пока не поддерживается;

  • read-only initramfs вместо VFS, writable-файловой системы и постоянного хранилища;

  • небольшой syscall ABI без сигналов, сокетов, mmap и многих привычных POSIX-интерфейсов;

  • один пользовательский поток на процесс и полное копирование памяти при fork;

  • нет libc, динамического загрузчика, TLS и стандартной userspace-среды;

  • Round Robin без приоритетов, priority inheritance и измеренной верхней границы latency;

  • нет сохранения FP/SIMD-контекста, а само ядро собирается для soft-float target;

  • фиксированное количество процессов, потоков, файловых дескрипторов и событий времени;

  • UART вместо TTY и полноценного терминала.

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

Как запустить genrt

Статья описывает релиз v0.1.0-alpha.2. Для воспроизведения лучше переключиться именно на этот тег, а не использовать меняющуюся ветку main:

git clone https://github.com/redeemed-sis/genrt.git
cd genrt
git checkout v0.1.0-alpha.2
./scripts/setup/install-deps.sh
cargo xtask run-aarch64

После появления приглашения > можно выполнить ls, pwd, cat /etc/banner, echo hello и exit.

Полный набор проверок запускается отдельно:

cargo xtask ci
Демонстрация работы с релизной genrt
Демонстрация работы с релизной genrt

Что я вынес из этих четырех месяцев

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

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

При этом расширение возможностей не уменьшает ответственность. Решение о том, что строить, какие компромиссы принимать и почему тестам можно доверять, всё равно остаётся за инженером.

genrt ещё далека от практического применения: ей не хватает SMP, постоянного хранилища, развитого ABI, RT-политик и поддержки реального оборудования. Но это уже не набор разрозненных экспериментов. Собственный boot-код приводит к запуску изолированного userspace, командная оболочка действительно запускает отдельные ELF-программы, а изменения проходят через сквозные тестовые контракты.

Исходный код находится в репозитории genrt, а состояние, описанное в статье, зафиксировано в релизе v0.1.0-alpha.2. Буду рад технической критике, найденным ошибкам и обсуждению архитектурных решений в GitHub.

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


  1. VladislavKalinkin
    30.07.2026 10:09

    Поздравляю с работой. Очень неплохо. Тоже как-то пробовал через chatgpt ос на расте делать (не Codex, а именно диалог с thinking 5.5). Делал сначала под arm64, потом добавил risc-v, затем удалил arm и только risc. В конце концов перестал делать не дойдя до результата как у вас. Спасибо что напомнили своей статьей об этом!

    Типо вот начало запуска.
    Типо вот начало запуска.