Эта статья объясняет как создать минимальное ядро операционной системы, используя стандарт мультизагрузки. По факту, оно будет просто загружаться и печатать 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 константы,- константа
0x100000000
в вычислении контрольной суммы — это небольшой хак, чтобы избежать предупреждений компилятора.
Мы уже можем собрать данный файл (который я назвал multiboot_header.asm
) используя nasm
.
[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-битные инструкции),mov dword
инструкция помещает 32-битную константу0x2f4b2f4f
в адрес памятиb8000
(это выводитOK
на экран, объяснено будет в следующих статьях),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-го мегабайта, это стандарт расположения для загрузки ядра,- исполняемая часть имеет две секции: в начале
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
пакет).
[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 при добавлении файлов, - операция
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 режим.
Примечания
- Формула из таблицы
-(magic + architecture + header_length)
создает отрицательное значение, которое не влезает в 32 бита. С помощью вычитания из0x100000000
мы оставляем значение положительным без изменения вычтенного значения. В результате без дополнительного знакового бита результат помещается в 32 бита и компилятор счастлив :) - Мы не хотим загружать ядро по офсету 0x0, так как много специфичных областей памяти может быть расположено до метки в 1 мегабайт (для примера, так называемый VGA буфер по адресу
0xb8000
, который мы используем чтобы вывестиOK
на экран).
Автор: Галимов Арсен