Вступление
Привет, Хабр! Мне 16 лет, я студент, учусь на первом курсе колледжа на программиста. Недавно увлёкся низкоуровневым программированием на Ассемблере и C/C++.
И вот, в какой-то момент я решил для саморазвития создать свой простенький загрузчик на ассемблере, который будет загружать ядро написанное на C и на экран будет выводится что-то по типу "Hello World!". Перечитал кучу статей по этой теме на Хабре, и на некоторых других ресурсах. Спустя десяток ошибок у меня всё получилось, и я был искренне счастлив.
Но меня огорчило то, что большая часть подобных статей описывают код загрузчиков для BIOS-MBR которому уже несколько десятков лет. А ведь сравнительно недавно появился новый UEFI-GPT и очевидно что будущее именно за ним, но при этом я не нашëл на Хабре ни одной статьи, подробно описывающей создание подобного простенького UEFI приложения для него! Конечно есть некоторые люди которые писали об этом, но их очень мало, а те материалы что есть, показались мне уж слишком сложными и малопонятными. Именно эта мысль навела меня на идею разобраться в этом самому и написать данную статью.
BIOS

BIOS — это Basic Input Output System, базовая система ввода‑вывода. Это программа низкого уровня, хранящиеся в чипе материнской платы компьютера.
BIOS запускается при включении компьютера и отвечает за пробуждение ��ппаратных компонентов, убеждается в том, что они правильно работают, после чего определяет загрузочное устройство.
Как только BIOS определил загрузочное устройство, он считывает первый дисковой сектор этого устройства в память. Первый сектор диска — это главная загрузочная запись — Masted Boot Record (MBR) размером 512 байт. В MBR расположена программа‑загрузчик, которая уже в свою очередь запускает операционную систему.
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 практически нет ограничения на размер раздела.
Что нам понадобится?
Linux (Я использую Kali Linux запущенный на Virtual Box)
Компилятор GCC
Библиотека GNU-EFI, добавляющая стандартные функции
Знание Си
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.