К черту архитектуру x86, или как я в 14 лет написал простую ОС под rv64
Введение и дисклеймер
Сразу скажу - я не считаю себя втором Терри Дэвисом и тем более не хочу делать аналог Виндовс. Изначально я все это затеял только ради прикола и изучения ассемблера.
А, и если что - мне 14, но я не против любой критики. И да, я тоже как и большинство местами использовал ИИ. Я не сидел читал мануалы часами, просто гуглил или просил нейронку пояснить.
Так а в чем же проблема x86_64?
Я так скажу - я пробовал писать под эту архитектуру. Мне не понравилось, всякие режимы (которые за меня правда переключал загрузчик), да и местный ассемблер мне было лень изучать. Просто захотел писать под более новую архитектуру. Не буду говорить, что RISC-V лучшая, но я просто выбрал ее. Прикол в том, что инструкции короткие, да и в целом достаточно перспективно. Хотя и тут были проблемы.
Был драйвер, стала ОС
Я ничего с нуля не писал.
Все началось с пыток ИИ, когда я пытался выдавить из него то обращение по конкретному адресу для доступа к графике, то опрос портов, то еще что-то такое. Мне стало лень и я пошел искать что-то плюс-минус готовое. И я нашел его - ramfb. По сути я получил готовый указатель на фреймбуфер для вывода на экран... И все. Но и на этом большое спасибо авторам драйвера. Дальше началась моя ОС.
А где хранить данные?
А тут первая проблема - как таковой кучи у меня еще нету. Есть стек, который за меня выделил автор драйвера. Есть всякие глобальные переменные. Но что если мне нужно много памяти (например, под картинки) и я не знаю точный размер? И в добавок, кто вообще рисует напрямую на экран? Тут нужен некий массив пикселей, который будет лежать в памяти, а по нашей команде быстро перенесется на экран (называется backbuffer). Под кучу взял 64 мб из памяти, так как Qemu, где запускается моя ОС, выделяет ей аж 128 мб, часть уже занята ядром и стеком, но бОльшая часть пустует. Сделал простенький список свободных и занятых блоков памяти. Это когда ты делишь память на куски, каждый помечаешь как занятый или свободный. Нужно мало памяти - откусываем от большого куска блок. Блок освободился, а рядом есть другой свободный блок - соединяем в один большой.
Сразу же добавил функцию, чтобы узнать сколько памяти занято. Сейчас, например, занято около 3-4 мб. Ну это учитывая, что backbuffer и занял почти 3.5 мб.
В оперативной памяти что-то типа [ядро] -> [стек, 64 KB] -> [куча, 64 MB] -> [сам экран Qemu, привязанный к ram напрямую, ±3.51 MB]
Такая разная многопоточность
Как я думаю большинство знает, если у процессора одно ядро, одновременно на нем может выполняться только одна задача-поток. Но мы же может останавливать задачу, запускать другую, останавливать, запускать еще другую и так много раз в секунду. Тогда будет казаться, будто бы у нас несколько задач выполняются одновременно.
Многопоточность кооперативная
Так мы можем сказать потокам, чтобы если один из них решит, что стоит сделать паузу, сам вызвал yield() и отдал процессор другому потоку. И я это сделал. Все что нужно было - сохранить s-регистры процессора (те, в которых лежат всякие важные значения), указатель на личный стек потока (размер стека задается при создании) и указатель на "дорогу назад" ra. То есть yield() сохраняет эту информацию потока А, затем вместо нее загружает информацию потока Б. И так по кругу.
Кстати, стеки самих потоков я выделял в той самой куче.Но у такого варианта многопоточности есть проблема - зависает один поток, остальные перестают выполняться. Да и в целом писать yield() каждый раз сложно.
Многопоточность вытесняющая
Поэтому я и решил переписать все на вытеснение. Для начала я сделал так - каждые 100к тиков процессора сохраняются все регистры и пинается функция обработки прерываний. Таймер, по которому будет работать многопоточность - это тоже прерывание. Так же эта функция ловит ошибки по типу kernel panic. Это было сложнее - так как потоки не добровольно отдают процессор, приходится сохранять вообще все 32 регистра. Но криво-косо я написал это и функцию sleep(), во время которой функция простаивает, ее никто не трогает.
А тут еще и ИИ начал выдавать бред, пока я тестировал у меня... начинал выполняться основной код, все было хорошо, а потоки - не выполнялись и залезали на другую память, судя по логу. Я сам не вижу ошибку в коде, ИИ тоже. Он то предложит заменить какую-то часть функции, то еще что-то такое. Наконец я вспомнил про замечательную штуку - gdb. Запустил, посмотрел... Оказалось так - цикл отрисовки экрана был слишком долгим, во время него вызывалось переключение потоков, а их еще не было кроме главного. В итоге я подумал что смысла создавать потоки в конце, после отрисовки, нет, и перенес их в самое начало. Удивительно, но все заработало.
Немного про режимы
Как выяснилось, в RISC-V есть 3 режима (точнее 4, включая гипервизор, но тут он мне точно не нужен) - M-mode с полными привелегиями, S-mode для ядра ОС (я думал что и мое его использует), ну и U-mode для пользователей, но до этого мои ручки еще не дошли.
Ну и по пути я случайно осознал, что мое ядро выполняется аж в М-режиме:
Вызываю я ecall из потока. Смотрю - kernel panic. В логах какой-то код "11". Оказалось, это и есть ecall - в M-mode он 11, а в S-mode (в котором, как я думал, работало ядро, и который я пытался выловить) это 9. Ну когда я нашел, уже быстро исправил.
Визуальчик
Но в это время на сам экран эмулятора Qemu... Ничего и не выводилось. Совсем. Просто белый экран. Хотелось красоты.Тогда я вспомнил, что день назад прикрутил qoi.h к моему коду. QOI - формат изображений. Выбрал я его, так как декодер легкий и его удобно использовать в любой ОС, где есть базовые функции.
Я аккуратно, разумеется, согласно лицензии, взял из pop os, которой когда-то пользовался, картинку для фона. Конвертировал ее в .qoi, а так как моя ос еще не умела читать с диска, перевел картинку в заголовочный файл .h. Весит правда много, но влезает.
Дальше я нашел шрифт (Liberation Mono от Red Hat), перегнал и его в .h. Честно, визуал меня не сильно интересовал, поэтому быстро попросил ИИ переделать функции вывода в консоль в функции вывода на экран. Проверил, просмотрел что и как сделано, получилось нормально для старта. Правда пока что в графической консоли нет нормальной перемотки и много чего другого.
Вывод
Конечно, еще нету даже ввода с клавиатуры, мыши, файловой системы (даже простой). Но я считаю, что вышло неплохо, пусть и далеко от уровня реальной ОС типа Линукса.
Понятно, что код не очень-то красивый, достаточно простой. Статью я выкладываю больше просто чтобы разобраться самому в том, что я написал.
Если я не забью на код, хотел бы написать еще и MMU и переход в U-mode.
Для тех кому очень интересно - можете глянуть исходный код mqos, там же будут инструкции по запуску.