Создание своего UEFI загрузчика

Вступление

Привет Хабр! Мне 16 лет, я студент, учусь на первом курсе колледжа на программиста. Недавно увлёкся низкоуровневым программированием на Ассемблере и C/C++.

И вот, в какой-то момент я решил для саморазвития создать свой простенький загрузчик на ассемблере, который будет загружать ядро написанное на C и на экран будет выводится что-то по типу «Hello World!». Перечитал кучу статей по этой теме на Хабре, и на некоторых других ресурсах. Спустя десяток ошибок у меня всё получилось, и я был искренне счастлив.

Но меня огорчило то, что большая часть подобных статей описывают код загрузчиков для BIOS-MBR которому уже несколько десятков лет. А ведь сравнительно недавно появился новый UEFI-GPT и очевидно что будущее именно за ним, но при этом я не нашëл на Хабре ни одной статьи, подробно описывающей создание подобного простенького UEFI загрузчика для него! Конечно есть некоторые люди которые что-то писали о об этом, но их очень мало, а те что есть, показались мне уж слишком сложными и малопонятными. Именно эта мысль навела меня на идею разобраться в этом самому и написать данную статью.

Дисклеймер

Эта статья написана новичком для новичков. Я всего-навсего попытался самостоятельно разобраться в данной теме и поделится своим опытом тут, я НЕ дипломированный специалист, и НЕ хочу его из себя строить. По этому прошу простить мне возможные ошибки или неточности которые я мог допустить.

BIOS

BIOS

BIOS

BIOS — это Basic Input Output System, базовая система ввода‑вывода. Это программа низкого уровня, хранящиеся в чипе материнской платы компьютера.

BIOS запускается при включении компьютера и отвечает за пробуждение аппаратных компонентов, убеждается в том, что они правильно работают, после чего определяет загрузочное устройство.
Как только BIOS определил загрузочное устройство, он считывает первый дисковой сектор этого устройства в память. Первый сектор диска — это главная загрузочная запись — Masted Boot Record (MBR) размером 512 байт. В MBR расположена программа‑загрузчик, которая уже в свою очередь запускает операционную систему.

Недостатки BIOS

BIOS существует уже очень давно и очень мало эволюционировал. Он практически не изменялся со времëн создания, в отличии от других компьютерных технологий. По этому спустя время начали вылезать различные проблемы, такие как:

  1. Ограничение загрузки с дисков не более 2 ТБ

  2. Загрузчик не может быть больше 512 байт

  3. BIOS должен работать в 16-битном режиме процессора и ему доступен всего 1 Мб памяти.

  4. Проблемы с одновременной инициализацией нескольких устройств, что ведëт к замедлению процесса загрузки, во время которого инициализируются все аппаратные интерфейсы и устройства.

UEFI

UEFI

UEFI

UEFI — это унифицированный расширяемый интерфейс прошивки (Unified Extensible Firmware Interface), является более продвинутым интерфейсом, чем BIOS. Он может анализировать файловую систему и даже сам загружать файлы. UEFI не имеет процедуры загрузки с помощью MBR, вместо этого он использует GPT.

Как загружаются UEFI-загрузчики?

UEFI определяет диски с известными файловыми системами, и ищет на них по адресу /EFI/BOOT/ файл с расширением .efi, который называется bootX.efi где Х — это платформа, для которой написан загрузчик. Вот собственно и всё.

GPT (GUID)

GPT — это более новый стандарт для определения структуры разделов на диске. Это часть стандарта UEFI, то есть систему на основе UEFI можно установить только на диск использующий GPT.
GPT допускает создание неограниченного количества разделов, хотя некоторые операционные системы могут ограничивать их число 128 разделами. Также в GPT практически нет ограничения на размер раздела.

Что нам понадобится?

  1. Linux (Я использую Kali Linux запущенный на Virtual Box)

  2. Компилятор GCC

  3. GNU-EFI (Гайд по установке с OSDev тык)

  4. Знание Си

  5. QEMU (Виртуальная машина для тестирования)

Начало

Для начала создадим рабочую директорию под названием gnu-efi-dir и зайдём в неё:

mkdir gnu-efi-dir
cd gnu-efi-dir

Установим и скомпилируем GNU-EFI:

git clone https://git.code.sf.net/p/gnu-efi/code gnu-efi
cd gnu-efi
make

Теперь пришло время написания самой программы. Создаём файл, я его назову boot.c и начинаем писать код! Для начала хватит загрузчика который ничего не загружает выводит на экран «Hello World!»

#include 
#include 

EFI_STATUS 
EFIAPI

efi_main (EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE *SystemTable) {
  InitializeLib(ImageHandle, SystemTable);

  Print(L"Hello World!\n");

  return EFI_SUCCESS;
}

Сборка

Теперь всё это дело нам нужно скомпилировать, слинковать и сделать из этого EFI файл. Что бы не прописывать все команды вручную я создал Makefile:

run: boot.o boot.so boot.efi
	make clean

boot.o:
	gcc -I gnu-efi/inc -fpic -ffreestanding -fno-stack-protector -fno-stack-check -fshort-wchar -mno-red-zone -maccumulate-outgoing-args -c boot.c -o boot.o

boot.so:
	ld -shared -Bsymbolic -L gnu-efi/x86_64/lib -L gnu-efi/x86_64/gnuefi -T gnu-efi/gnuefi/elf_x86_64_efi.lds gnu-efi/x86_64/gnuefi/crt0-efi-x86_64.o boot.o -o boot.so -lgnuefi -lefi

boot.efi:
	objcopy -j .text -j .sdata -j .data -j .rodata -j .dynamic -j .dynsym  -j .rel -j .rela -j .rel.* -j .rela.* -j .reloc --target efi-app-x86_64 --subsystem=10 boot.so boot.efi

clean:
	rm *.o *.so

Теперь нам остаётся лишь написать команду make и мы получим итоговый файл boot.efi.

Подготовка к запуску

Как я уже сказал выше для запуска нашего EFI приложения мы будем использовать виртуальную машину QEMU. Так же нам понадобится OVMF. Устанавливаем всё это:

sudo apt install qemu-kvm qemu
sudo apt install ovmf

Ещё нам понадобятся файлы OVMF_CODE.fd и OVMF_VARS-1024×768.fd. Их можно скачать отсюда. Установим их с помощью wget в отдельную директорию:

mkdir ovmf
cd ovmf
wget https://github.com/kholia/OSX-KVM/blob/master/OVMF_CODE.fd
wget https://github.com/kholia/OSX-KVM/blob/master/OVMF_VARS-1024x768.fd

Сразу создадим ещё одну директорию build в которой будет собираться наше приложение:

mkdir build

Всё почти готово! Давайте напишем небольшой скрипт на Python Build.py (его я взял из вот этой статьи) который будет создавать все нужные директории в папке build, копировать туда наш файл и запускать QEMU:

import argparse
import os
import shutil
import sys
import subprocess as sp
from pathlib import Path

ARCH = "x86_64"
TARGET = ARCH + "-none-efi"
CONFIG = "debug"
QEMU = "qemu-system-" + ARCH

WORKSPACE_DIR = Path(__file__).resolve().parents[0]
BUILD_DIR = WORKSPACE_DIR / "build"

OVMF_FW = WORKSPACE_DIR / "ovmf" / "OVMF_CODE.fd"
OVMF_VARS = WORKSPACE_DIR / "ovmf" / "OVMF_VARS-1024x768.fd"

def build():
    boot_dir = BUILD_DIR / "EFI" / "BOOT"
    boot_dir.mkdir(parents=True, exist_ok=True)
    
    built_file = "boot.efi"
    output_file = boot_dir / "BootX64.efi"
    shutil.copy2(built_file, output_file)

    startup_file = open(BUILD_DIR / "startup.nsh", "w")
    startup_file.write("\EFI\BOOT\BOOTX64.EFI")
    startup_file.close()

def run():
    qemu_flags = [
        # Disable default devices
        # QEMU by default enables a ton of devices which slow down boot.
        "-nodefaults",
    
        # Use a standard VGA for graphics
        "-vga", "std",
    
        # Use a modern machine, with acceleration if possible.
        "-machine", "q35,accel=kvm:tcg",
    
        # Allocate some memory
        "-m", "128M",
    
        # Set up OVMF
        "-drive", f"if=pflash,format=raw,readonly,file={OVMF_FW}",
        "-drive", f"if=pflash,format=raw,file={OVMF_VARS}",
    
        # Mount a local directory as a FAT partition
        "-drive", f"format=raw,file=fat:rw:{BUILD_DIR}",
    
        # Enable serial
        #
        # Connect the serial port to the host. OVMF is kind enough to connect
        # the UEFI stdout and stdin to that port too.
        "-serial", "stdio",
    
        # Setup monitor
        "-monitor", "vc:1024x768",
      ]

    sp.run([QEMU] + qemu_flags).check_returncode()

def main():
    if len(sys.argv) < 2:
        print("Error! Unknown command.")
        print("Example: python3.11 Build.py [build/run]")

        return False
        
    if sys.argv[1] == "build":
        build()
    elif sys.argv[1] == "run":
        run()
    else:
        print("Error! Unknown command.")
        print("Example: python3.11 Build.py [build/run]")

if __name__ == "__main__":
    main()

Запуск

Всё готово! Собираем и запускаем наше EFI приложение:

python Build.py build
python Build.py run

Итоговый результат

Итоговый результат

Заключение

В этой статье мы рассмотрели как создать простой UEFI-загрузчик и протестировали его на виртуальной машине QEMU. Все файлы (кроме gnu-efi, он у меня почему-то криво загрузился) проекта вы можете посмотреть на моём GitHub.

© Habrahabr.ru