Эта статья объясняет как создать минимальное ядро операционной системы, используя стандарт мультизагрузки. По факту, оно будет просто загружаться и печатать OK на экране. В последующих статьях мы расширим его, используя язык программирования Rust .

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

Обзор

Когда вы включаете компьютер, он загружает BIOS из специальной флэш памяти. BIOS запускает тесты самопроверки и инициализацию аппаратного обеспечения, затем он ищет загрузочные устройства. Если было найдено хотя бы одно, он передаёт контроль загрузчику, который является небольшой частью запускаемого кода, сохранённого в начале устройства хранения. Загрузчик определяет местоположение образа ядра, находящегося на устройстве, и загружает его в память. Ему также необходимо переключить процессор в так называемый защищённый режим, потому что x86 процессоры по умолчанию стартуют в очень ограниченном реальном режиме (чтобы быть совместимыми с программами из 1978).

Мы не будем писать загрузчик, потому что это сам по себе сложный проект (если вы действительно хотите это сделать, почитайте об этом здесь). Вместо этого мы будем использовать один из многих испытанных загрузчиков для загрузки нашего ядра с CD-ROM. Но какой?

Мультизагрузка

К счастью, есть стандарт загрузчика: спецификация мультизагрузки. Наше ядро должно лишь указать, что поддерживает спецификацию и любой совместимый загрузчик сможет загрузить его. Мы будем использовать спецификацию Multiboot 2 (PDF)

вместе с известным загрузчиком GRUB 2.

Чтобы сказать загрузчику о поддержке Multiboot 2 , наше ядро должно начинаться с заголовка мультизагрузки , который имеет следующий формат:

Field Type Value магическое число u32 0xE85250D6 архитектура u32 0 для i386, 4 для MIPS длина заголовка u32 общий размер заголовка включая тэги контрольная сумма u32 -(магическое число + архитектура + длина заголовка) тэги variable завершающий тэг (u16, u16, u32) (0, 0, 8)

В переводе на x86 ассемблер это будет выглядеть так ( Intel синтаксис):

section .multiboot_header header_start: dd 0xe85250d6 ; магическое число (multiboot 2) dd 0 ; архитектура 0 (защищённый режим i386) dd header_end - header_start ; длина заголовка ; контрольная сумма dd 0x100000000 - (0xe85250d6 + 0 + (header_end - header_start)) ; вставьте опциональные `multiboot` тэги здесь ; требуюется завершающий тэг dw 0 ; тип dw 0 ; флаги dd 8 ; размер header_end:

Если вы не знаете x86 ассемблер, то вот небольшая вводная:

заголовок будет записан в секцию, названную .multiboot_header (нам понадобится это позже),

(нам понадобится это позже), header_start и header_end — это метки, которые указывают на месторасположение в памяти, мы используем их, чтобы вычислить длину заголовка,

и — это метки, которые указывают на месторасположение в памяти, мы используем их, чтобы вычислить длину заголовка, dd означает define double (32bit) и dw означает define word (16bit). Они просто выводят указанные 32bit/16bit константы,

означает (32bit) и означает (16bit). Они просто выводят указанные 32bit/16bit константы, константа 0x100000000 в вычислении контрольной суммы — это небольшой хак, чтобы избежать предупреждений компилятора.

Мы уже можем собрать данный файл (который я назвал multiboot_header.asm ) используя nasm .

Установка nasm на `archlinux` [loomaclin@loomaclin ~]$ yaourt nasm 1 extra/nasm 2.13.02-1 An 80x86 assembler designed for portability and modularity 2 extra/yasm 1.3.0-2 A rewrite of NASM to allow for multiple syntax supported (NASM, TASM, GAS, etc.) 3 aur/intel2gas 1.3.3-7 (3) (0.20) Converts assembly language files between NASM and GNU assembler syntax 4 aur/nasm-git 20150726-1 (1) (0.00) 80x86 assembler designed for portability and modularity 5 aur/sasm 3.9.0-1 (18) (0.61) Simple crossplatform IDE for NASM, MASM, GAS, FASM assembly languages 6 aur/yasm-git 1.3.0.r30.g6caf1518-1 (0) (0.00) A complete rewrite of the NASM assembler under the BSD License ==> Enter n° of packages to be installed (e.g., 1 2 3 or 1-3) ==> --------------------------------------------------------- ==> 1 [sudo] password for loomaclin: resolving dependencies... looking for conflicting packages... Packages (1) nasm-2.13.02-1 Total Download Size: 0.34 MiB Total Installed Size: 2.65 MiB :: Proceed with installation? [Y/n] :: Retrieving packages... nasm-2.13.02-1-x86_64 346.0 KiB 1123K/s 00:00 [#############################################################################] 100% (1/1) checking keys in keyring [#############################################################################] 100% (1/1) checking package integrity [#############################################################################] 100% (1/1) loading package files [#############################################################################] 100% (1/1) checking for file conflicts [#############################################################################] 100% (1/1) checking available disk space [#############################################################################] 100% :: Processing package changes... (1/1) installing nasm [#############################################################################] 100% :: Running post-transaction hooks... (1/1) Arming ConditionNeedsUpdate... [loomaclin@loomaclin ~]$ nasm --version NASM version 2.13.02 compiled on Dec 10 2017 [loomaclin@loomaclin ~]$

Следующая команда произведёт плоский двоичный файл, результирующий файл будет содержать 24 байта (в little endian , если вы работаете на x86 машине):

[loomaclin@loomaclin ~]$ cd IdeaProjects/ [loomaclin@loomaclin IdeaProjects]$ mkdir a_minimal_multiboot_kernel [loomaclin@loomaclin IdeaProjects]$ cd a_minimal_multiboot_kernel/ [loomaclin@loomaclin a_minimal_multiboot_kernel]$ nano multiboot_header.asm [loomaclin@loomaclin a_minimal_multiboot_kernel]$ nasm multiboot_header.asm [loomaclin@loomaclin a_minimal_multiboot_kernel]$ hexdump -x multiboot_header 0000000 50d6 e852 0000 0000 0018 0000 af12 17ad 0000010 0000 0000 0008 0000 0000018 [loomaclin@loomaclin a_minimal_multiboot_kernel]$

Загрузочный код

Чтобы загрузить наше ядро, мы должны добавить код, который сможет вызвать загрузчик. Давайте создадим файл boot.asm :

global start section .text bits 32 start: ; печатает `OK` на экране mov dword [0xb8000], 0x2f4b2f4f hlt

Здесь есть несколько новых команд:

global экспортирует метки (делает их публичными). Метка start будет входной точкой в наше ядро, она должна быть публичной,

экспортирует метки (делает их публичными). Метка будет входной точкой в наше ядро, она должна быть публичной, .text секция — это секция по умолчанию для исполняемого кода,

секция — это секция по умолчанию для исполняемого кода, bits 32 говорит о том, что следующие строки — это 32-битные инструкции. Это необходимо потому что процессор ещё находится в защищённом режиме, когда GRUB запускает наше ядро. Когда переключимся в Long mode в следующей статье, сможем запускать bits 64 (64-битные инструкции),

говорит о том, что следующие строки — это 32-битные инструкции. Это необходимо потому что процессор ещё находится в защищённом режиме, когда запускает наше ядро. Когда переключимся в Long mode в следующей статье, сможем запускать (64-битные инструкции), mov dword инструкция помещает 32-битную константу 0x2f4b2f4f в адрес памяти b8000 (это выводит OK на экран, объяснено будет в следующих статьях),

инструкция помещает 32-битную константу в адрес памяти (это выводит на экран, объяснено будет в следующих статьях), hlt — это инструкция, которая говорит процессору остановить выполнение команд.

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

[loomaclin@loomaclin a_minimal_multiboot_kernel]$ nano boot.asm [loomaclin@loomaclin a_minimal_multiboot_kernel]$ nasm boot.asm [loomaclin@loomaclin a_minimal_multiboot_kernel]$ hexdump -x boot 0000000 05c7 8000 000b 2f4f 2f4b 00f4 000000b [loomaclin@loomaclin a_minimal_multiboot_kernel]$ ndisasm -b 32 boot 00000000 C70500800B004F2F mov dword [dword 0xb8000],0x2f4b2f4f -4B2F 0000000A F4 hlt [loomaclin@loomaclin a_minimal_multiboot_kernel]$

Создание исполняемого файла

Чтобы загрузить наш исполняемый файл позже через GRUB , он должен быть исполняемым ELF файлом. Поэтому необходимо с помощью nasm создать ELF объектные файлы вместо простых бинарников. Для этого мы просто добавляем в аргументы -f elf64 .

Для создания самого ELF исполняемого кода мы должны связать объектные файлы. Будем использовать кастомный скрипт для связывания, называемый linker.ld :

ENTRY(start) SECTIONS { . = 1M; .boot : { /* в начале оставим заголовк мультизагрузки */ *(.multiboot_header) } .text : { *(.text) } }

Переведём что написано на человеческий язык:

start — это точка входа, загрузчик перейдёт к этой метке после загрузки ядра,

— это точка входа, загрузчик перейдёт к этой метке после загрузки ядра, . = 1M; уставливает адрес загрузки первой секции с 1-го мегабайта, это стандарт расположения для загрузки ядра,

уставливает адрес загрузки первой секции с 1-го мегабайта, это стандарт расположения для загрузки ядра, исполняемая часть имеет две секции: в начале boot и .text после,

и после, конечная секция .text будет содержать в себе все входящие секции .text ,

будет содержать в себе все входящие секции , секции, именованные как .multiboot_header , будут добавлены в первую выходную секцию ( .boot ), чтобы они располагались в начале исполняемого кода. Это необходимо, потому что GRUB ожидает найти заголовок мультизагрузки в начале файла.

Давайте создадим ELF объектные файлы и слинкуем их, используя вышеуказанный линкер скрипт:

[loomaclin@loomaclin a_minimal_multiboot_kernel]$ nasm -f elf64 multiboot_header.asm [loomaclin@loomaclin a_minimal_multiboot_kernel]$ nasm -f elf64 boot.asm [loomaclin@loomaclin a_minimal_multiboot_kernel]$ ld -n -o kernel.bin -T linker.ld multiboot_header.o boot.o [loomaclin@loomaclin a_minimal_multiboot_kernel]$

Очень важно передать -n (или --nmagic ) флаг линкеру, который отключает автоматическое выравнивание секций в исполняемом файле. В противном случае линкер может выравнить страницу секции .boot в исполняемом файле. Если это произойдёт, GRUB не сможет найти заголовок мультизагрузки, потому что он будет находиться уже не в начале.

Воспользуемся командой objdump для того, чтобы вывести секции сгенерированного исполняемого файла и проверить, что .boot секция имеет наименьшее смещение в файле:

[loomaclin@loomaclin a_minimal_multiboot_kernel]$ objdump -h kernel.bin kernel.bin: file format elf64-x86-64 Sections: Idx Name Size VMA LMA File off Algn 0 .boot 00000018 0000000000100000 0000000000100000 00000080 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 1 .text 0000000b 0000000000100020 0000000000100020 000000a0 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE [loomaclin@loomaclin a_minimal_multiboot_kernel]$

Примечание: команды ld и objdump платформо-зависимы. Если вы работаете не на x86_64 архитектуре, вы нуждаетесь в кросс компиляции binutils. После этого воспользуйтесь x86_64‑elf‑ld и x86_64‑elf‑objdump вместо ld и objdump соответственно.

Создание ISO-образа

Все персональные компьютеры, работающие на базе BIOS , знают, как загружаться с CD-ROM, так что нам необходимо создать загружаемый образ CD-ROM, содержащий наше ядро и файлы загрузчика GRUB в единственном файле, называемом ISO. Создайте следующую структуру директорий и скопируйте kernel.bin в директорию boot :

isofiles └── boot ├── grub │ └── grub.cfg └── kernel.bin

grub.cfg указывает имя файла нашего ядра и совместимость с multiboot 2 . Выглядит это так:

set timeout=0 set default=0 menuentry "my os" { multiboot2 /boot/kernel.bin boot }

Исполняем команды:

[loomaclin@loomaclin a_minimal_multiboot_kernel]$ mkdir isofiles [loomaclin@loomaclin a_minimal_multiboot_kernel]$ mkdir isofiles/boot [loomaclin@loomaclin a_minimal_multiboot_kernel]$ mkdir isofiles/boot/grub [loomaclin@loomaclin a_minimal_multiboot_kernel]$ cp kernel.bin isofiles/boot/ [loomaclin@loomaclin a_minimal_multiboot_kernel]$ nano grub.cfg [loomaclin@loomaclin a_minimal_multiboot_kernel]$ cp grub.cfg isofiles/boot/grub/

Теперь мы можем создать загружаемый образ, используя следующую команду:

[loomaclin@loomaclin a_minimal_multiboot_kernel]$ grub-mkrescue -o os.iso isofiles xorriso 1.4.8 : RockRidge filesystem manipulator, libburnia project. Drive current: -outdev 'stdio:os.iso' Media current: stdio file, overwriteable Media status : is blank Media summary: 0 sessions, 0 data blocks, 0 data, 7675m free Added to ISO image: directory '/'='/tmp/grub.jN4u6m' xorriso : UPDATE : 898 files added in 1 seconds Added to ISO image: directory '/'='/home/loomaclin/IdeaProjects/a_minimal_multiboot_kernel/isofiles' xorriso : UPDATE : 902 files added in 1 seconds xorriso : NOTE : Copying to System Area: 512 bytes from file '/usr/lib/grub/i386-pc/boot_hybrid.img' ISO image produced: 9920 sectors Written to medium : 9920 sectors at LBA 0 Writing to 'stdio:os.iso' completed successfully.

Примечание: вызов grub-mkrescue может вызвать проблемы на некоторых платформах. Если она у вас не сработала, попробуйте следующие шаги:

запустить команду с --verbose ,

, удостовериться, что библиотека xorriso установлена ( xorriso или libisoburn пакет).



На `Archlinux пришлось поставить `libisoburn` [loomaclin@loomaclin a_minimal_multiboot_kernel]$ yaourt xorriso

1 extra/libisoburn 1.4.8-2

frontend for libraries libburn and libisofs

==> Enter n° of packages to be installed (e.g., 1 2 3 or 1-3)

==> — ==> 1

[sudo] password for loomaclin:

resolving dependencies…

looking for conflicting packages...

Packages (3) libburn-1.4.8-1 libisofs-1.4.8-1 libisoburn-1.4.8-2

Total Download Size: 1.15 MiB

Total Installed Size: 3.09 MiB

:: Proceed with installation? [Y/n]

:: Retrieving packages…

libburn-1.4.8-1-x86_64 259.7 KiB 911K/s 00:00 [#############################################################################] 100%

libisofs-1.4.8-1-x86_64 237.8 KiB 2.04M/s 00:00 [#############################################################################] 100%

libisoburn-1.4.8-2-x86_64 683.8 KiB 2.34M/s 00:00 [#############################################################################] 100%

(3/3) checking keys in keyring [#############################################################################] 100%

(3/3) checking package integrity [#############################################################################] 100%

(3/3) loading package files [#############################################################################] 100%

(3/3) checking for file conflicts [#############################################################################] 100%

(3/3) checking available disk space [#############################################################################] 100%

:: Processing package changes…

(1/3) installing libburn [#############################################################################] 100%

(2/3) installing libisofs [#############################################################################] 100%

(3/3) installing libisoburn

если вы используете EFI-систему, grub-mkrescue попробует создать EFI образ по умолчанию. Вы можете задать аргумент -d /usr/lib/grub/i386-pc , чтобы избавиться от этого поведения, или установить пакет mtools и получить работающий EFI образ

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

Загрузка

Пришло время загрузить нашу ОС. Для этого воспользуемся QEMU:

[loomaclin@loomaclin a_minimal_multiboot_kernel]$ qemu-system-x86_64 -cdrom os.iso (qemu-system-x86_64:10878): Gtk-WARNING **: Allocating size to GtkScrollbar 0x7f2337e5a280 without calling gtk_widget_get_preferred_width/height(). How does the code know the size to allocate? (qemu-system-x86_64:10878): Gtk-WARNING **: Allocating size to GtkScrollbar 0x7f2337e5a480 without calling gtk_widget_get_preferred_width/height(). How does the code know the size to allocate? (qemu-system-x86_64:10878): Gtk-WARNING **: Allocating size to GtkScrollbar 0x7f2337e5a680 without calling gtk_widget_get_preferred_width/height(). How does the code know the size to allocate?

Появится окно эмулятора:



Обратите внимание на зелёный текст OK в верхнем левом углу. Если у вас это не работает, посмотрите секцию комментариев.

Резюмируем, что произошло:

BIOS загружает загрузчик (GRUB) из виртуального CD-ROM (ISO). Загрузчик прочёл исполняемый код ядра и нашёл заголовок мультизагрузки. Скопировал секцию .boot и .text в память (по адресу 0x100000 и 0x100020 ). Переместился к точке входа ( 0x100020 , это можно узнать вызвав objdump -f ). Ядро вывело на экран текст OK зелёным цветом и остановило процессор.

Вы также можете протестировать это на настоящем железе. Необходимо записать получившийся образ на диск или USB накопитель и загрузиться с него.

Автоматизация сборки

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

… ├── Makefile └── src └── arch └── x86_64 ├── multiboot_header.asm ├── boot.asm ├── linker.ld └── grub.cfg

Создаём:

[loomaclin@loomaclin a_minimal_multiboot_kernel]$ mkdir -p src/arch/x86_64 [loomaclin@loomaclin a_minimal_multiboot_kernel]$ cp multiboot_header.asm src/arch/x86_64/ [loomaclin@loomaclin a_minimal_multiboot_kernel]$ cp boot.asm src/arch/x86_64/ [loomaclin@loomaclin a_minimal_multiboot_kernel]$ cp linker.ld src/arch/x86_64/ [loomaclin@loomaclin a_minimal_multiboot_kernel]$ cp grub.cfg src/arch/x86_64/ [loomaclin@loomaclin a_minimal_multiboot_kernel]$ nano Makefile

Makefile должен иметь следующий вид:

arch ?= x86_64 kernel := build/kernel-$(arch).bin iso := build/os-$(arch).iso linker_script := src/arch/$(arch)/linker.ld grub_cfg := src/arch/$(arch)/grub.cfg assembly_source_files := $(wildcard src/arch/$(arch)/*.asm) assembly_object_files := $(patsubst src/arch/$(arch)/%.asm, \ build/arch/$(arch)/%.o, $(assembly_source_files)) .PHONY: all clean run iso all: $(kernel) clean: @rm -r build run: $(iso) @qemu-system-x86_64 -cdrom $(iso) iso: $(iso) $(iso): $(kernel) $(grub_cfg) @mkdir -p build/isofiles/boot/grub @cp $(kernel) build/isofiles/boot/kernel.bin @cp $(grub_cfg) build/isofiles/boot/grub @grub-mkrescue -o $(iso) build/isofiles 2> /dev/null @rm -r build/isofiles $(kernel): $(assembly_object_files) $(linker_script) @ld -n -T $(linker_script) -o $(kernel) $(assembly_object_files) # compile assembly files build/arch/$(arch)/%.o: src/arch/$(arch)/%.asm @mkdir -p $(shell dirname $@) @nasm -felf64 $< -o $@

Некоторые комментарии (если вы не работали до этого с make , посмотрите makefile туториал):

$(wildcard src/arch/$(arch)/*.asm) выбирает все файлы ассемблера в директории src/arch/$(arch) , так что вам не нужно обновлять Makefile при добавлении файлов,

, так что вам не нужно обновлять Makefile при добавлении файлов, операция patsubst для assembly_object_files просто переводит src/arch/$(arch)/XYZ.asm в build/arch/$(arch)/XYZ.o ,

для просто переводит в , таргеты сборки $< и $@ это автоматически выводимые переменные,

и это автоматически выводимые переменные, если вы используете кросс-комплированные binutils просто замените ld на x86_64-elf-ld .

Теперь мы можем вызвать make и все обновлённые файлы ассемблера будут скомпилированы и скомпонованы. Команда make iso также создаёт ISO образ, а make run в дополнение запускает QEMU.

Что дальше?

В следующей статье мы создадим таблицу страниц и проведем некоторую конфигурацию процессора для переключения в 64-битный long-mode режим.

Примечания