Первый шаг в написании своей ОСи — создание бинарника, не зависящего от стандартных библиотек, это делает возможным запуск кода без ОС — мы же пишем свою. Оригинал блога разрабатывается на GitHub. Замечания к оригиналу оставляйте на странице Issues репозитория выше, а к переводу — в личке, комментариях или тут. Код, написанный в этой статье, содержится в ветке post-01. Для того, чтобы написать свою ОС, нам нужен код, не зависящий от библиотек или функций другой операционной системы. Это значит, что мы не можем использовать потоки исполнения (threads), файлы, память в куче (heap), сеть, вывод в терминал и так далее. Но это можно преодолеть, написав свою ОС и драйвера. Мы не можем использовать большую часть стандартной библиотеки Rust, но также есть много функций, которые мы можем использовать. Например итераторы, замыкания, сопоставление с образцом, Option и Result, форматирование строк, и конечно же, концепцию владения. Это позволит написать своё ядро в высокоуровневом стиле, не заботясь о неопределённом поведении или безопасности памяти. Эта статья рассказывает, как создать автономный исполняемый файл и почему это необходимо. Если нужен только пример, можно прокрутить до части "Заключение". По умолчанию скомпилированный код собирается с участием стандартной библиотеки, которой нужна ОС для реализации таких функций, как файлы, сеть и т.д. Также она зависит от стандартной библиотеки С: libc, которая еще больше использует фичи ОС. А так как мы пишем программу, которая заместит собой обычную ОС, это нам не подходит. Нужно отключить библиотеки с помощью no_std. Сначала создадим проект с помощью Cargo. Это делается с помощью коммандной строки: Я назвал проект os-in-rust (для избегания путаницы с оригинальным блогом), но можно выбрать любое имя. Флаг --bin говорит, что нужен проект, который будет собираться в исполняемый файл, а не в библиотеку. --edition 2018 значит, что нужно использовать именно редакцию Rust 2018. Cargo сгенерирует такую структуру папок: Файл Cargo.toml содержит конфигурацию: название проекта, автор, версию и зависимости. В src/main.rs содержится код, который выполнится, когда мы запустим скомпилированный бинарник. Компилируется он коммандой cargo build, а исполняемый файл будет в папке target/debug. Сейчас код собирается с использованием стандартной библиотеки. Это поведение можно выключить с помощью атрибута no_std: Если попробовать собрать проект, получим ошибку: Причиной этому является тот факт, что макрос println — часть библиотеки Rust, которую мы больше не используем. Получается, мы не можем ничего выводить. Это имеет смысл, так как для вывода нужна консоль или файл, а для них нужна запущенная ОС. :( Так что удалим вывод и снова попробуем запустить сборку: Атрибут panic_handler говорит языку, что эту функцию нужно вызвать, если произошла паника (вызов panic!()). В стандартных библиотеках она есть, но в среде no_std надо её создать самим: В параметре PanicInfo содержится файл и номер строки, где код запаниковал, а также (опциональное) сообщение об ошибке. Эта функция не должна возвращать выполнение другой, поэтому тип возвращаемого значения — ! (never). Сейчас она ничего не делает, а просто устраивает бесконечный цикл. eh_personality — это "элемент языка", функция или тип, которые необходимы для работы компилятора. Например, типаж Copy — элемент языка, который говорит компилятору, какие типы поддерживают семантику копирования. Если посмотреть на его реализацию, можно увидеть атрибут #[lang = "copy"], который определяет этот типаж как элемент языка. Вообще, можно попробовать создать свою реализацию элементов языка, это нужно использовать только в крайнем случае, потому, что они очень часто меняются и даже не имеют проверки типов! Хорошо, что есть еще один способ. Элемент языка eh_personality помечает функцию, которая используется для реализации "разматывания" стека вызовов. По умолчанию Rust использует это для вызова деструкторов всех переменных на стеке в случае паники, чтобы освободить всю использованную память. Но это сложный процесс, которому требуются библиотеки, специфические для каждой ОС (libunwind на Linux и структурированная обработка исключений на Windows), то мы не будем это использовать. Rust дает возможность просто прерывать исполнение программы в случае паники. Это также исключает необходимые для разматывания данные, уменьшая бинарник. Есть несколько мест, где можно это выключить. Самое простое — Cargo.toml: Это устанавливает стратегию обработки паники в значение abort и для профиля dev (используется при вызове cargo build), и для профиля release (cargo build --release). Теперь нам не нужен eh_personality. Мы пофиксили обе ошибки. Но теперь есть новая: Можно подумать, что функция main вызывается первой при исполнении программы. Но это не так для большинства языков. Много языков программирования имеют свою среду исполнения, которая отвечает за сборку мусора (Java, C#, JavaScript...) или программных потоков исполнения (корутины, горутины в Go). Эту среду нужно инициализировать перед вызовом main с очевидных причин. В типичном исполняемом файле Rust исполнение начинается в библиотеке, которая называется crt0, что настраивает среду для исполнения кода на С. Это, помимо прочего, включает в себя создание стека и укладывание параметров в правильные регистры. Потом среда С вызывает среду Rust в месте, помеченным start, Rust инициализируется, и только потом вызывает main(). Наш код мог бы переопределить этот элемент языка, но тогда он имел бы зависимость от crt0, а это нам не надо. Так что переопределять он будет входную точку crt0. Чтобы сказать компилятору, что мы не используем обычную цепочку вызовов при инициализации, надо добавить атрибут #![no_main]. Мы удалили функцию main(), так как она все равно не вызывается. Вместо нее надо определить функцию _start: Используя атрибут #[no_mangle], мы выключаем искажение имен, чтобы компилятор назвал функцию _start, а не, например, _ZN3blog_os4_start7hb173fedf945531caE. Это используется компилятором для того, чтобы избежать ошибок переопределения. Также надо пометить функцию extern "C", это говорит компилятору, что функцию надо вызывать с помощью конвенции С, а не Rust (которая еще и изменяться, видно, может, так как по ней нету документации). Название такое в функции потому, что это название точки входа по умолчанию на большинстве систем. Вы, наверное, заметили, что она возвращает !, то есть, не заканчивает свое исполнение. Это нужно потому, что эту функцию будет вызывать загрузчик, а если она завершится, то ПК выключится (или перезагрузится). Теперь, если сделать cargo build, мы получим ошибку сборщика. Сборщик — это программа, которая собирает ваш код и библиотеки, которые ему нужны, в единое целое. А так как разные системы имеют свои форматы исполняемых файлов, они имеют свои сборщики, и разные ошибки. Надо сделать так, чтобы они не появлялись, нужно сказать сборщику, что мы не используем стандартные библиотеки. Это можно сделать 2 способами: передавая сборщику параметры, или созданием конфигурации целевой платформы. По умолчанию Rust пробует создать исполняемый файл для вашей платформы. Если вы используете Windows на x86-64, Rust создаст .exe с инструкциями для архитектуры x86-64. Это называется целевая платформа. Для различания платформ Rust (да и многие другие инструменты) использует target triples. Чтобы увидеть эту строку для вашего ПК, надо выполнить rustc --version --verbose: Это вывела данная команда на моей системе (Linux x86-64). Параметр, который нас интересует — host. Он говорит, что: Компилируя для такой платформы, Rust думает, что бинарник будет исполняться на какой-нить операционной системе (в моем случае, Linux) и использует системные библиотеки (libc, libunwind и другие). Чтобы не было ошибок сборщика, надо компилировать под другую целевую платформу. Примером такой платформы является thumbv7em-none-eabihf, что означает встраиваемая система на базе ARM. Детали нас не интересуют, главное то, что там нет ОС (none). Чтобы иметь возможность компиляции под данную платформу, надо добавить её с помощью Rustup: Теперь можно скомпилировать код: Используя флаг --target, мы кросс-компилируем код для другой платформы. Так как написано, что нету операционной системы, сборщик даже не пробует найти библиотеки. Это тот способ, который мы будем использовать. Но вместо thumbv7em-none-eabihf мы создадим кастомное описание платформы для x86-64. Детали будут в следующей статье (которую я пока не перевел), а пока можно почитать оригинал. Если не ошибаюсь, эти статьи также переводил m1rko, но не могу найти эту (да и любую). Код для минимального бинарника, что запустится на голом железе выглядит так: src/main.rs: Cargo.toml: Для сборки — эта команда: Но есть одно но. Это только минимальный пример. Этот бинарник ожидает некоторые вещи, например, что стек будет инициализирован. Если захотите использовать это где-то, надо доработать. кодер-самоучка, пробую себя в переводе