Creating your own UEFI boot loader
BIOS
BIOS – this is the Basic Input Output System, the basic input/output system. This is a low-level program stored in a chip on the computer's motherboard.
The BIOS starts when you turn on the computer and is responsible for waking up the hardware components, making sure they are working correctly, and then determining the boot device.
Once the BIOS has detected a boot device, it reads the first disk sector of that device into memory. The first sector of the disk is the master boot record – Masted Boot Record (MBR) size 512 bytes. The MBR contains a bootloader program, which in turn launches the operating system.
BIOS Disadvantages
BIOS has been around for a very long time and has evolved very little. It has remained virtually unchanged since its creation, unlike other computer technologies. Therefore, after a while, various problems began to emerge, such as:
Limit loading from disks no more than 2 TB
The bootloader cannot be larger than 512 bytes
The BIOS must operate in 16-bit processor mode and only 1 MB of memory is available to it.
Problems with simultaneous initialization of several devices, which leads to a slower boot process, during which all hardware interfaces and devices are initialized.
UEFI
UEFI is a unified extensible firmware interface (Unified Extensible Firmware Interface), is a more advanced interface than the BIOS. It can analyze the file system and even download files itself. UEFI does not have a boot procedure using the MBR, instead it uses GPT.
How are UEFI bootloaders loaded?
UEFI detects drives with known file systems and searches them by address /EFI/BOOT/
file with extension .efi
which is called bootX.efi
where X is the platform for which the bootloader is written. That's all.
GPT (GUID)
GPT is a newer standard for defining the partition structure of a disk. This is part of the UEFI standard, meaning a UEFI-based system can only be installed on a disk that uses GPT.
GPT allows for an unlimited number of partitions, although some operating systems may limit the number to 128 partitions. There is also virtually no limit on partition size in GPT.
What do we need?
Linux (I'm using Kali Linux running on Virtual Box)
GCC compiler
GNU-EFI (Installation Guide with OSDev poke)
Knowledge of C
QEMU (Virtual Machine for Testing)
Start
First, let's create a working directory called gnu-efi-dir and go into it:
mkdir gnu-efi-dir
cd gnu-efi-dir
Let's install and compile GNU-EFI:
git clone https://git.code.sf.net/p/gnu-efi/code gnu-efi
cd gnu-efi
make
Now it's time to write the program itself. Let's create a file, I'll call it boot.c and start writing code! To begin with, all you need is a bootloader that doesn't load anything displays “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;
}
Assembly
Now we need to compile this whole thing, link it and make an EFI file from it. In order not to write all the commands manually, I created a 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
Now all we have to do is write the make command and we will get the final boot.efi file.
Preparing for launch
As I said above, to run our EFI application we will use a virtual machine QEMU. We also need OVMF. Let's install all this:
sudo apt install qemu-kvm qemu
sudo apt install ovmf
We also need the files OVMF_CODE.fd and OVMF_VARS-1024×768.fd. You can download them from here. Let's install them using wget in a separate directory:
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
Let’s immediately create another build directory in which our application will be built:
mkdir build
Everything is almost ready! Let's write a small script in Python Build.py (I took it from this article) which will create all the necessary directories in the build folder, copy our file there and launch 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()
Launch
All is ready! We assemble and launch our EFI application:
python Build.py build
python Build.py run
Conclusion
In this article, we looked at how to create a simple UEFI boot loader and tested it on the QEMU virtual machine. You can look at all the files (except for gnu-efi, for some reason it didn’t load correctly for me) of the project at mine GitHub.