Вступление

Привет, Хабр! Мне 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 расположена программа‑загрузчик, которая уже в свою очередь запускает операционную систему.

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, добавляющая стандартные функции

  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 <efi.h>
#include <efilib.h>

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. Это реализация UEFI которую будет использовать QEMU, так как по стандарту в нём её нет. Устанавливаем всё это:

sudo apt install qemu-kvm qemu
sudo apt install ovmf

Ещё нам понадобятся файлы OVMF_CODE.fd и OVMF_VARS-1024x768.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.