Пишем свой загрузчик
Наша цель - написать простой загрузчик для UEFI систем, который сможет запускать ELF-ядра. Для этого мы будем использовать язык C (gcc) и библиотеку GNU-EFI. Для начала, скачаем GNU-EFI:
git clone https://tvoygit.ru/vi_is_lonely/gnu-efi.git --depth 1 # Скачиваем исходный код библиотеки
cd gnu-efi # переходим в директорию библиотеки
make # компилируем библиотеку
Теперь у нас есть такие файлы, как crt0-efi-x86_64.o, libefi.a и libgnuefi.a. Для удобства предлагаю создать в папке проекта папку lib и поместить эти файлы в неё. Так же создадим папку src с нашим исходным кодом. В папку lds поместим скрипт компоновщика из поставки GNU-EFI, под названием elf_x86_64_efi.lds. В папке inc у нас будут заголовки, а в папку gnu-efi переместим содержимое папки gnu-efi/inc. Так же скачайте OVMF и поместить файлы OVMF_CODE.4m.fd и OVMF_VARS.4m.fd в папку ovmf. Напишем простой Makefile:
SOURCES = $(shell find src/*.c) # список всех исходных файлов
OBJECTS = $(patsubst src/%.c,/tmp/build-obj/%.o,$(SOURCES)) # будем хранить объектные файлы в ОЗУ для более быстрой сборки
OPTLEVEL = -O2 -s # предлагаю вынести эти флаги в отдельную переменную
CFLAGS = $(OPTLEVEL) -Iinc -Ignu-efi -fpic -ffreestanding -c \
-fno-stack-protector -fno-stack-check -Wno-pointer-arith -Wno-volatile \
-Wno-return-type -Wno-narrowing -Wno-multichar \
-fshort-wchar -mno-red-zone -maccumulate-outgoing-args \
-mno-red-zone -maccumulate-outgoing-args -fno-exceptions -fno-rtti -std=gnu23
LDFLAGS = -shared -Bsymbolic -L ./lib -flto -s -T lds/elf_x86_64_efi.lds lib/crt0-efi-x86_64.o
OCFLAGS = -j .text -j .sdata -j .data -j .rodata \ # флаги для конвертации ELF в EFI Application (PE)
-j .dynamic -j .dynsym -j .rel -j .rela \
-j .rel.* -j .rela.* -j .reloc --target \
efi-app-x86_64 --subsystem=10
IMG = /tmp/viboot-image # здесь будет располагаться папка, имитирующая наш образ
OVMF_FW = ./ovmf/OVMF_CODE.4m.fd
OVMF_VARS = ./ovmf/OVMF_VARS.4m.fd
QEMUFLAGS = -nodefaults -vga std -m 96M -smp 2 -serial stdio -monitor vc:800x600 \ # флаги для виртуальной машины QEmu
-drive if=pflash,format=raw,readonly=on,file=${OVMF_FW} \
-drive if=pflash,format=raw,file=${OVMF_VARS} \
-drive format=raw,file=fat:rw:${IMG}
all: build image qemu
build: clean bootx64.efi
bootx64.efi: $(OBJECTS)
@echo " * Linking"
@ld $(LDFLAGS) $^ -o /tmp/boot.so -lefi -lgnuefi
@strip -s /tmp/bootx64.so
@echo " * Objcopying"
@objcopy $(OCFLAGS) /tmp/boot.so $@
@strip -s $@
@echo " * Cleaning up"
@rm -rf $(OBJECTS)
/tmp/build-obj/%.o: src/%.cc
@echo " [$$] Compiling $<"
@mkdir -p $(dir $@) || true
@gcc $(CFLAGS) -I$(GNUEFIINC) -o $@ $^
clean:
@echo " [$$] Cleaning up"
@rm -rf $(OBJECTS) bootx64.efi $(IMG) || true
image:
@echo " [$$] Creating image"
@rm -rf $(IMG) || true
@mkdir -p $(IMG)/EFI/BOOT
@cp bootx64.efi $(IMG)
qemu:
@echo " [$$] Running QEMU"
@qemu-system-x86_64 $(QEMUFLAGS)
Так же, вам ничто не мешает добавлять в цель image копирование любых других файлов, в том числе ядра. давайте обусловимся, что ваше ядро - статический ELF64, позиционезависимый (-fpic). Итак, напишем основу для нашего загрузчика:
#include <efi.h>
#include <efilib.h>
EFI_STATUS EFIAPI efi_main(EFI_HANDLE Image, EFI_SYSTEM_TABLE *SysTab) {
InitializeLib(Image, SysTab);
Print(L"Hello, world!\n");
return EFI_SUCCESS;
}
Для вызова любой функции UEFI, вы можете использовать макрос uefi_call_wrapper(указатель_на_функцию, количество_аргументов, аргументы...), однако это может быть немного неудобно, поэтому можете взять мой собственный макрос, оборачивающий уже предоставленный:
#define EFI(f, vargs...) uefi_call_wrapper((void*)f, __VA_NARG__(vargs), vargs)
// использоваие: EFI(функция, аргументы...)
При этом, если вы используете С++ вместо С, с этим макросом у вас не будет возникать предупреждений permissive.
Советы и рекомендации
Помимо функций UEFI, библиотека GNU-EFI поставляет собственный набор функций, которые оборачивают UEFI. Например, если вы хотите вывести на экран строку, то вот так это будет выгядеть с использованием функций UEFI:
EFI_STATUS EFIAPI efi_main(EFI_HANDLE Image, EFI_SYSTEM_TABLE *SysTab) {
InitializeLib(Image, SysTab);
EFI(SysTab->ConOut->OutputString, SysTab->ConOut, L"Hello, World!\r\n");
return EFI_SUCCESS;
}
Как вы могли заметить, в предыдущем примере уже используется оборачивающая функция Print. Зачастую лучше использовать их, поскольку становится меньше шанс ошибки в коде и сам код становится чище и лаконичнее.
Так же, я сделал набор простых, но удобных макросов. Вот они:
/// @brief обёртка вызова UEFI
#define EFI(f, vargs...) uefi_call_wrapper((void*)f, __VA_NARG__(vargs), vargs)
/// @brief ассерт UEFI
#define EFI_ASSERT(cond, fmt, ...) if (!(cond)) EFI_FAT(fmt __VA_OPT__(,) __VA_ARGS__)
/// @brief аварийная остановка работы
#define EFI_STOP() asm volatile ("cli\nhlt\njmp . - 2")
#if EFI_DEBUG
/// @brief отладочный вывод UEFI
#define EFI_DBG(fmt, ...) Print(L" <*> " fmt L"\r\n" __VA_OPT__(,) __VA_ARGS__)
/// @brief вывод об ошибке UEFI
#define EFI_ERR(fmt, ...) Print(L" <!> " fmt L"\r\n" __VA_OPT__(,) __VA_ARGS__)
/// @brief вывод о фатальной ошибке UEFI и аварийная остановка работы
#define EFI_FAT(fmt, ...) do { Print(L" <X> " fmt L"\r\n" __VA_OPT__(,) __VA_ARGS__); asm volatile ("cli\nhlt\njmp . - 2"); } while(0)
/// @brief вывод о предупреждении
#define EFI_WRN(fmt, ...) Print(L" <#> " fmt L"\r\n" __VA_OPT__(,) __VA_ARGS__)
/// @brief простой вывод UEFI
#define EFI_PRN(fmt, ...) Print(fmt __VA_OPT__(,) __VA_ARGS__)
/// @brief вывод UEFI с переносом строки
#define EFI_LN(fmt, ...) Print(fmt L"\r\n" __VA_OPT__(,) __VA_ARGS__)
#else
#define EFI_DBG(fmt, ...)
#define EFI_ERR(fmt, ...)
#define EFI_FAT(fmt, ...) asm volatile ("cli\nhlt\njmp . - 2")
#define EFI_WRN(fmt, ...)
#define EFI_PRN(fmt, ...)
#define EFI_LN(fmt, ...)
#endif
Среди всего прочего могу показать функции-обёртки, упрощающие работу с файловой системой в среде UEFI:
/// @brief корневой раздел EFI
static EFI_FILE_HANDLE g_RD;
/// @brief Загрузить файл в ОЗУ
/// @param FileName Путь к файлу для загрузки
/// @param FileData Указатель на указатель на буфер. В него будет выведен адрес буфера
/// @param FileDataLength Указатель на длину файла. В него будет выведен размер буфера
/// @return статус завершения функции
EFI_STATUS LoadFile(const CHAR16 *FileName, CHAR8 **FileData, UINTN *FileDataLength) {
EFI_STATUS status;
EFI_FILE_HANDLE FileHandle;
EFI_FILE_INFO *FileInfo;
UINT64 ReadSize;
UINTN BufferSize;
UINT8 *Buffer = NULL;
EFI_ASSERT(g_RD || FileName, L"LoadFile: Неверные параметры");
status = EFI(g_RD->Open, g_RD, &FileHandle, FileName,
EFI_FILE_MODE_READ, EFI_FILE_READ_ONLY | EFI_FILE_HIDDEN | EFI_FILE_SYSTEM);
EFI_ASSERT(!status, L"Open: %r", status);
FileInfo = LibFileInfo(FileHandle);
if (FileInfo == NULL) {
EFI(FileHandle->Close, FileHandle);
EFI_FAT(L"LibFileInfo failed");
}
ReadSize = FileInfo->FileSize;
// 16 MiB максимальный размер файла (рекомендовано, можно закомментировать для отключения лимита
if (ReadSize > 16*1024*1024) ReadSize = 16*1024*1024;
FreePool(FileInfo);
// Вычисление размера буфера в страницах
BufferSize = (un)((ReadSize+PAGESIZE-1)/PAGESIZE);
status = EFI(BS->AllocatePages, 0, 2, BufferSize, (u64*)&Buffer);
if (EFI_ERROR(status) || Buffer == NULL) {
EFI(FileHandle->Close, FileHandle);
EFI_FAT(L"AllocatePages провалилась: %r", status);
}
status = EFI(FileHandle->Read, FileHandle, &ReadSize, Buffer);
EFI(FileHandle->Close, FileHandle);
if (EFI_ERROR(status)) {
EFI(BS->FreePages, (u64)(Buffer), BufferSize);
EFI_FAT(L"ReadFile провалилась: %r", status);
}
*FileData = Buffer;
*FileDataLength = ReadSize;
return EFI_SUCCESS;
}
/// @brief Записать данные в файл
/// @param FileName путь к файлу
/// @param FileData данные для записи в файл
/// @param WriteSize размер данных
/// @return статус завершения функции
EFI_STATUS WriteFile(const CHAR16 *FileName, CHAR8 *FileData, UINTN WriteSize) {
EFI_STATUS status;
EFI_FILE_HANDLE FileHandle;
EFI_FILE_INFO *FileInfo;
UINTN BufferSize;
CHAR8 *Buffer = NULL;
EFI_ASSERT(g_RD || FileName, L"LoadFile: неверные параметры");
status = EFI(g_RD->Open, g_RD, &FileHandle, FileName, EFI_FILE_MODE_WRITE, EFI_FILE_HIDDEN | EFI_FILE_SYSTEM);
EFI_ASSERT(!status, L"Open error: %r", status);
// 16 MiB максимальный размер файла (рекомендовано, можно закомментировать для отключения лимита
if (WriteSize > 16*1024*1024) WriteSize = 16*1024*1024;
// запись в файл
status = EFI(FileHandle->Write, FileHandle, &WriteSize, Buffer);
EFI(FileHandle->Close, FileHandle);
EFI_ASSERT(!status, L"LoadFile провалилась: %r", status);
return EFI_SUCCESS;
}
// Чтобы они работали, нужно инициализировать RootDir:
EFI_STATUS InitFS(EFI_SYSTEM_TABLE *SysTab) {
EFI_STATUS status = EFI_SUCCESS;
EFI_LOADED_IMAGE_PROTOCOL *g_LIP = NULL;
EFI_GUID LIPguid = EFI_LOADED_IMAGE_PROTOCOL_GUID;
status = EFI(g_BS->HandleProtocol, ImageHandle, &LIPguid, (void**)&g_LIP);
EFI_ASSERT(!status || !g_LIP, L"LocateProtocol провалилась: %r\n", status);
g_RD = LibOpenRoot(g_LIP->DeviceHandle);
EFI_ASSERT(!status || !g_RD, L"LibOpenRoot провалилась: %r\n", status);
}
Так же, UEFI использует кодировку unicode (16 бит на символ), вместо ASCII (7 бит на символ), поэтому так же полезной будет данная функция:
CHAR16 *a2u(CHAR8 *str) {
static CHAR16 mem[4096]; int i;
for (i = 0; str[i]; ++i) mem[i] = (CHAR16)str[i];
mem[i] = 0; return mem;
}