Pull to refresh

Создание своего UEFI приложения

Level of difficultyMedium
Reading time5 min
Views43K

Вступление

Привет, Хабр! Мне 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.

Tags:
Hubs:
Total votes 72: ↑71 and ↓1+84
Comments80

Articles