Pull to refresh

Минимальный multiboot загрузчик

Reading time 12 min
Views 21K
Original author: Philipp Oppermann

Эта статья объясняет как создать минимальное ядро операционной системы, используя стандарт мультизагрузки. По факту, оно будет просто загружаться и печатать 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.


Установка 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-битные инструкции),
  • 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 пакет).


На `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 в верхнем левом углу. Если у вас это не работает, посмотрите секцию комментариев.


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


  1. BIOS загружает загрузчик (GRUB) из виртуального CD-ROM (ISO).
  2. Загрузчик прочёл исполняемый код ядра и нашёл заголовок мультизагрузки.
  3. Скопировал секцию .boot и .text в память (по адресу 0x100000 и 0x100020).
  4. Переместился к точке входа (0x100020, это можно узнать вызвав objdump -f).
  5. Ядро вывело на экран текст 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 режим.


Примечания


  1. Формула из таблицы -(magic + architecture + header_length) создает отрицательное значение, которое не влезает в 32 бита. С помощью вычитания из 0x100000000 мы оставляем значение положительным без изменения вычтенного значения. В результате без дополнительного знакового бита результат помещается в 32 бита и компилятор счастлив :)
  2. Мы не хотим загружать ядро по офсету 0x0, так как много специфичных областей памяти может быть расположено до метки в 1 мегабайт (для примера, так называемый VGA буфер по адресу 0xb8000, который мы используем чтобы вывести OK на экран).
Tags:
Hubs:
+27
Comments 15
Comments Comments 15

Articles