- PVSM.RU - https://www.pvsm.ru -
Эта статья объясняет как создать минимальное ядро операционной системы, используя стандарт мультизагрузки. По факту, оно будет просто загружаться и печатать OK
на экране. В последующих статьях мы расширим его, используя язык программирования Rust
.
Я попытался объяснить всё в деталях и оставить код максимально простым, насколько это возможно. Если у вас возникли вопросы, предложения или какие-либо проблемы, пожалуйста, оставьте комментарий или создайте таску [1] на GitHub
. Исходный код доступен в репозитории [2].
Когда вы включаете компьютер, он загружает BIOS [3] из специальной флэш памяти. BIOS
запускает тесты самопроверки и инициализацию аппаратного обеспечения, затем он ищет загрузочные устройства. Если было найдено хотя бы одно, он передаёт контроль загрузчику, который является небольшой частью запускаемого кода, сохранённого в начале устройства хранения. Загрузчик определяет местоположение образа ядра, находящегося на устройстве, и загружает его в память. Ему также необходимо переключить процессор в так называемый защищённый режим [4], потому что x86 процессоры по умолчанию стартуют в очень ограниченном реальном режиме [5] (чтобы быть совместимыми с программами из 1978).
Мы не будем писать загрузчик, потому что это сам по себе сложный проект (если вы действительно хотите это сделать, почитайте об этом здесь [6]). Вместо этого мы будем использовать один из многих испытанных загрузчиков [7] для загрузки нашего ядра с CD-ROM. Но какой?
К счастью, есть стандарт загрузчика: спецификация мультизагрузки [8]. Наше ядро должно лишь указать, что поддерживает спецификацию и любой совместимый загрузчик сможет загрузить его. Мы будем использовать спецификацию Multiboot 2
(PDF [9])
вместе с известным загрузчиком GRUB 2 [10].
Чтобы сказать загрузчику о поддержке 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-битные инструкции. Это необходимо потому что процессор ещё находится в защищённом режиме [4], когда GRUB
запускает наше ядро. Когда переключимся в Long mode [11] в следующей статье, сможем запускать bits 64
(64-битные инструкции),mov dword
инструкция помещает 32-битную константу 0x2f4b2f4f
в адрес памяти b8000
(это выводит OK
на экран, объяснено будет в следующих статьях),hlt
— это инструкция, которая говорит процессору остановить выполнение команд.После сборки, просмотра и дизассемблирования мы можем увидеть опкоды [12] процессора в действии:
[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
исполняемого кода мы должны связать объектные файлы. Будем использовать кастомный скрипт для связывания [13], называемый 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 [14]. После этого воспользуйтесьx86_64‑elf‑ld
иx86_64‑elf‑objdump
вместоld
иobjdump
соответственно.
Все персональные компьютеры, работающие на базе BIOS
, знают, как загружаться с CD-ROM, так что нам необходимо создать загружаемый образ CD-ROM, содержащий наше ядро и файлы загрузчика GRUB
в единственном файле, называемом ISO [15]. Создайте следующую структуру директорий и скопируйте 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
grub-mkrescue
попробует создать EFI
образ по умолчанию. Вы можете задать аргумент -d /usr/lib/grub/i386-pc
, чтобы избавиться от этого поведения, или установить пакет mtools
и получить работающий EFI
образgrub2-mkrescue
.Пришло время загрузить нашу ОС. Для этого воспользуемся QEMU [16]:
[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
в верхнем левом углу. Если у вас это не работает, посмотрите секцию комментариев.
Резюмируем, что произошло:
.boot
и .text
в память (по адресу 0x100000
и 0x100020
).0x100020
, это можно узнать вызвав objdump -f
).OK
зелёным цветом и остановило процессор.Вы также можете протестировать это на настоящем железе. Необходимо записать получившийся образ на диск или USB накопитель и загрузиться с него.
Сейчас необходимо вызывать 4 команды в правильном порядке каждый раз, когда мы меняем файл. Это плохо. Давайте автоматизируем этот процесс, с помощью Makefile [17]. Но для начала мы должны создать подходящую структуру директорий чтобы отделить архитектурно-зависимые файлы:
…
├── 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 туториал [17]):
src/arch/$(arch)
, так что вам не нужно обновлять Makefile при добавлении файлов,patsubst
для assembly_object_files
просто переводит src/arch/$(arch)/XYZ.asm
в build/arch/$(arch)/XYZ.o
,$<
и $@
это автоматически выводимые переменные [18],ld
на x86_64-elf-ld
.Теперь мы можем вызвать make
и все обновлённые файлы ассемблера будут скомпилированы и скомпонованы. Команда make iso
также создаёт ISO образ, а make run
в дополнение запускает QEMU.
В следующей статье [19] мы создадим таблицу страниц и проведем некоторую конфигурацию процессора для переключения в 64-битный long-mode [11] режим.
-(magic + architecture + header_length)
создает отрицательное значение, которое не влезает в 32 бита. С помощью вычитания из 0x100000000
мы оставляем значение положительным без изменения вычтенного значения. В результате без дополнительного знакового бита результат помещается в 32 бита и компилятор счастлив :)0xb8000
, который мы используем чтобы вывести OK
на экран).Автор: Галимов Арсен
Источник [20]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/275888
Ссылки в тексте:
[1] создайте таску: https://github.com/phil-opp/blog_os/issues
[2] репозитории: https://github.com/phil-opp/blog_os/tree/post_1/src/arch/x86_64
[3] BIOS: https://en.wikipedia.org/wiki/BIOS
[4] защищённый режим: https://en.wikipedia.org/wiki/Protected_mode
[5] реальном режиме: https://wiki.osdev.org/Real_Mode
[6] почитайте об этом здесь: https://wiki.osdev.org/Rolling_Your_Own_Bootloader
[7] многих испытанных загрузчиков: https://en.wikipedia.org/wiki/Comparison_of_boot_loaders
[8] спецификация мультизагрузки: https://en.wikipedia.org/wiki/Multiboot_Specification
[9] PDF: http://nongnu.askapache.com/grub/phcoder/multiboot.pdf
[10] GRUB 2: https://wiki.osdev.org/GRUB_2
[11] Long mode: https://en.wikipedia.org/wiki/Long_mode
[12] опкоды: https://en.wikipedia.org/wiki/Opcode
[13] скрипт для связывания: https://sourceware.org/binutils/docs/ld/Scripts.html
[14] кросс компиляции binutils: https://os.phil-opp.com/cross-compile-binutils/
[15] ISO: https://en.wikipedia.org/wiki/ISO_image
[16] QEMU: https://en.wikipedia.org/wiki/QEMU
[17] Makefile: http://mrbook.org/blog/tutorials/make/
[18] автоматически выводимые переменные: https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html
[19] следующей статье: https://os.phil-opp.com/entering-longmode/
[20] Источник: https://habrahabr.ru/post/351568/?utm_source=habrahabr&utm_medium=rss&utm_campaign=351568
Нажмите здесь для печати.