Привет Мир! Limine, BIOS/UEFI
Наша цель - создать простой загрузочный образ операционной системы, который будет выводить на экран сообщение "Hello World". Для этого мы будем использовать язык программирования C и ассемблер GAS, а также различные инструменты для создания бинарных файлов и образов дисков.
Для начала, мы рассмотрим, как установить и настроить загрузчик Limine и как использовать протоколы Multiboot 1 и Multiboot 2 для передачи информации о загружаемой операционной системе. Затем мы напишем код загрузчика и ядра операционной системы, который будет выводить на экран сообщение "Hello World" при запуске.
Зависимости
Для начала установите
sudo apt install gcc xorriso grub-common git mtools qemu-system-x86
Limine
Создайте папку isodir и src.
Потом установите Limine:
#!/bin/bash
mkdir -p isodir
git clone https://github.com/limine-bootloader/limine.git --branch=v7.x-binary --depth=1
cd limine
make
cp limine-bios-cd.bin ../isodir/
cp limine-uefi-cd.bin ../isodir/
cp limine-bios.sys ../isodir/
cp limine-bios-pxe.bin ../isodir/
cd ..
Далее запишем в файл isodir/limine.cfg настройки загрузки:
:MyOS multiboot1
PROTOCOL=multiboot
KERNEL_CMDLINE=Hello World!
KERNEL_PATH=boot:///myos.elf
Multiboot и вызов главной функции ядра
Приступим к написанию кода.
Создайте файл src/boot.s:
.code32
/*
Этот код объявляет секцию .multiboot_header, содержащую информацию о заголовке протокола Multiboot 1.
Он предоставляет загрузчику информацию о типе заголовка, адресе точки входа, и других параметрах.
На метке _start, находится точка входа нашей операционной системы.
Однако, поскольку мы определяем этот адрес с помощью символа, определенного в нашем коде, _start может отличаться в зависимости от нашего C-кода.
Другая важная часть - это контрольная сумма. Обычно она проверяется загрузчиком на корректность при загрузке.
Если контрольная сумма неверна, загрузчик будет считать измененным заголовок, и загрузка с диска не будет успешной.
*/
.section .multiboot
.align 4 # Выравниваем секцию по 4-байтной границе
multiboot:
.long 0x1BADB002 # заголовок boot magic
.long 0x00 # флаги
.long -(0x1BADB002 + 0x00)# контрольная сумма (магическое число + флаги) * -1
.long _start # адрес точки входа
.long 0x00000000 # резервный адрес для стека
.long 0x00 # количество областей памяти
.long 0x00 # указатель области памяти
.long 0x00 # длина имени загрузчика
.long 0x00 # указатель на имя загрузчика
.long 0x00 # указатель на командную строку
.long 0x00 # модули
.long 0x00 # флаг графического режима
.long 0x00 # высота экрана
.long 0x00 # ширина экрана
.long 0x00 # bpp экрана
/*
Метка "stack_bottom" указывает на начало стека, который выделяется в секции ".bss".
Используя директиву ".skip", выделяется 4 килобайта памяти для стека.
Метка "stack_top" указывает на вершину стека, которая находится в конце выделенной памяти.
Указатель стека устанавливается на вершину стека с помощью инструкции "mov" в секции text.
Это важно, так как стек используется для передачи параметров функций, адреса возврата и хранения локальных переменных.
*/
.section .bss
.align 16 # Выравниваем секцию по 16-байтной границе
# Объявляем точку начала стека и выделяем на него 4 килобайта
stack_bottom:
.skip 1024 * 4 # 4 килобайт на стек
stack_top:
/*
Вызываем код на C
*/
.section .text
.extern kernel_main
.global _start
# Начинаем исполнять код с метки _start
_start:
cli # Отключаем прерывания
mov $stack_top, %esp # Начинаем стек в stack_top
call kernel_main # Вызываем функцию kernel_main
cli # Отключаем прерывания в случае ошибки в kernel_main
_halt:
hlt # Останавливаем процессор
jmp _halt # Возвращаемся на метку _halt на случай прерывания
Ядро на C
Далее код ядра src/kernel.c:
/**
* @brief Указатель на видеопамять
*
* Этот указатель используется для доступа к видеопамяти, которая используется для вывода текста на экран.
* Он указывает на начало видеопамяти, которая находится по адресу 0xB8000.
*/
volatile unsigned short *video_memory = (unsigned short*)0xB8000;
/**
* @brief Функция для вывода строки на экран
*
* Эта функция используется для вывода строки на экран. Она принимает указатель на строку и заменяет символы в видеопамяти
* на соответствующие символы из строки.
*
* @param str Указатель на строку для вывода на экран
*/
void print(const char *str) {
// FF - цвет текста(белый)
// 00 - цвет фона(черный)
const short color = 0xFF00;
for (int i = 0; str[i] != '\0'; i++) {
video_memory[i] = (video_memory[i] & color) | str[i]; // Изменяем символы в видеопамяти на символы из строки
}
}
/**
* @brief Точка входа в ядро операционной системы
*
* Эта функция является точкой входа в ядро операционной системы, которая вызывается загрузчиком операционной системы.
* Она выводит сообщение "Hello World!" на экран.
* Затем она входит в бесконечный цикл ожидания прерываний.
*
*/
void kernel_main() {
const char *message = "Hello World!"; // Создаем указатель на строку "Hello World!"
print(message); // Выводим сообщение на экран
for (;;) { // Заходим в бесконечный цикл ожидания прерываний
asm volatile("hlt"); // Ожидаем прерывания
}
}
И скрипт линковки src/link.ld:
OUTPUT_FORMAT("elf32-i386") /* Формат вывода ELF 32 бита для архитектуры x86 */
ENTRY(_start) /* Указывает на точку входа программы */
SECTIONS {
. = 1M; /* Устанавливает адрес загрузки ядра на 1 МБ */
.text BLOCK(4K) : ALIGN(4K) {
*(.multiboot) /* Секция multiboot */
*(.text) /* Секция кода */
}
.rodata BLOCK(4K) : ALIGN(4K) {
*(.rodata) /* Секция только для чтения */
}
.data BLOCK(4K) : ALIGN(4K) {
*(.data) /* Секция данных */
}
.bss BLOCK(4K) : ALIGN(4K) {
*(COMMON) /* Секция неинициализированных данных */
*(.bss)
}
/DISCARD/ : {
*(.comment) /* не хранить в итоговом файле */
}
}
/*
Этот скрипт линковки используется для компоновки загрузчика операционной системы. Вот пояснения для каждой строки:
- ENTRY(_start) указывает на точку входа в программу, которая должна быть помечена меткой "_start". Это гарантирует, что программа начнет свое выполнение с этой точки.
- SECTIONS { ... } - группирует различные секции в один блок.
- . = 1M; устанавливает адрес загрузки на 1 МБ. Это необходимо для того, чтобы загрузчик операционной системы не мешал другим программам, которые могут быть загружены в память.
- .text BLOCK(4K) : ALIGN(4K) {
*(.text)
} - это секция заголовка multiboot, которая содержит информацию для загрузчика
- .text BLOCK(4K) : ALIGN(4K) {
*(.text)
} - это секция кода, которая содержит код операционной системы. ALIGN (4K) выравнивает секцию по границе страницы (4 КБ), что обеспечивает лучшую производительность, а *(.text) включает в себя код самой операционной системы.
- .rodata BLOCK(4K) : ALIGN(4K) {
*(.rodata)
} - это секция только для чтения, которая содержит данные, которые не могут быть изменены во время выполнения программы.
- .data BLOCK(4K) : ALIGN(4K) {
*(.data)
} - это секция данных, которая содержит данные, которые могут быть изменены во время выполнения программы.
- .bss BLOCK(4K) : ALIGN(4K) {
*(COMMON) *(.bss)
} - это секция неинициализированных данных, которая содержит данные, которые не были явно инициализированы в программе. *(COMMON) включает общие неинициализированные данные, а *(.bss) включает все остальные неинициализированные данные.
*/
Сборка
Скрипт сборки build.sh:
#!/bin/bash
mkdir -p bin
gcc -m32 -ffreestanding -O0 -Wall -Wextra -c src/boot.s -o bin/boot.o
gcc -m32 -ffreestanding -O0 -Wall -Wextra -c src/kernel.c -o bin/kernel.o
gcc -m32 -ffreestanding -O0 -T src/link.ld -o isodir/myos.elf -nostdlib bin/boot.o bin/kernel.o
if grub-file --is-x86-multiboot isodir/myos.elf; then
echo Multiboot confirmed
else
echo The file is not multiboot
fi
if grub-file --is-x86-multiboot2 isodir/myos.elf; then
echo Multiboot2 confirmed
else
echo The file is not multiboot2
fi
xorriso -as mkisofs -b limine-cd.bin -no-emul-boot -boot-load-size 4 -boot-info-table --efi-boot limine-cd-efi.bin -efi-boot-part --efi-boot-image --protective-msdos-label isodir -o MyOS-limine.iso
Запуск
Далее вы можете запустить ваш ISO образ используя эмулятор qemu:
#!/bin/bash
qemu-system-i386 -cdrom MyOS-limine.iso
Или вы можете запустить ваше ядро напрямую:
#!/bin/bash
qemu-system-i386 -kernel isodir/myos.elf