Creating your own UEFI boot loader

BIOS

BIOS

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:

  1. Limit loading from disks no more than 2 TB

  2. The bootloader cannot be larger than 512 bytes

  3. The BIOS must operate in 16-bit processor mode and only 1 MB of memory is available to it.

  4. 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

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 .efiwhich 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?

  1. Linux (I'm using Kali Linux running on Virtual Box)

  2. GCC compiler

  3. GNU-EFI (Installation Guide with OSDev poke)

  4. Knowledge of C

  5. 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
Final result

Final result

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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *