Привет Мир! Limine, BIOS/UEFI

Материал из SynapseOS wiki
Перейти к навигации Перейти к поиску

Наша цель - создать простой загрузочный образ операционной системы, который будет выводить на экран сообщение "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


Ссылки