Creating your own UEFI program / Habr

Creating your own UEFI program / Habr

Introduction

Hello, Habre! I am 16 years old, I am a student, studying in the first year of college to become a programmer. Recently got into low-level programming in Assembler and C/C++.

And so, at some point I decided for self-development to create my own simple loader on assembler, which would load the kernel written in C and something like “Hello World!” would be displayed on the screen. I read a bunch of articles on this topic on Khabra and some other resources. After ten mistakes I got it all right and I was really happy.

But I was saddened by the fact that most of these articles describe bootloader code for BIOS-MBR that is several decades old. After all, a new UEFI-GPT appeared relatively recently and it is obvious that the future lies with it, but at the same time, I did not find a single article on Habra that describes in detail the creation of such a simple UEFI application for it! Of course, there are some people who wrote about it, but there are very few of them, and those materials that are there seemed to me to be too complicated and unclear. It was this thought that gave me the idea to look into it myself and write this article.

BIOS

BIOS

BIOS is the Basic Input Output System, a basic input-output system. This is a low-level program stored in a chip on the computer’s motherboard.

The BIOS starts when the computer is turned on and is responsible for waking up the hardware components, making sure that they are working, and then determining the boot device.
Once the BIOS has identified a bootable 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) with a size of 512 bytes. The MBR contains the bootloader program, which in turn starts the operating system.

UEFI

UEFI

UEFI – is a unified extensible firmware interface (Unified Extensible Firmware Interface), is a more modern interface than BIOS. It can analyze the file system and even download files. UEFI does not have an MBR boot procedure, instead it uses GPT.

How do UEFI bootloaders boot?

UEFI detects drives with known file systems and searches them by address /EFI/BOOT/ file with the extension .efiwhich is called bootX.efi where X is the platform for which the bootloader is written. That, actually, is all.

GPT (GUID)

GPT is a new standard for determining the structure of partitions on a disk. It is part of the UEFI standard, meaning a UEFI-based system can only be installed on a drive that uses GPT.
GPT allows the creation of an unlimited number of partitions, although some operating systems may limit the number to 128 partitions. Also, GPT has practically no partition size limit.

What will we need?

  1. Linux (I use Kali Linux running on Virtual Box)

  2. The GCC compiler

  3. A GNU-EFI library that adds standard functionality

  4. Knowledge of Si

  5. QEMU (Virtual Machine for Testing)

beginning

First, let’s create a working directory called gnu-efi-dir and go to 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 let’s start writing the code! To begin with, a program that 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;
}

Drafting

Now we need to compile and link this whole thing and make an EFI file out of 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.

Preparation for launch

As I said above, we will use the QEMU virtual machine to run our EFI program. We will also need OVMF. This is the UEFI implementation that QEMU will use since it doesn’t have one by default. We install all this:

sudo apt install qemu-kvm qemu
sudo apt install ovmf

We will 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 program will be assembled:

mkdir build

Everything is almost ready! Let’s write a small script in Python Build.py (I took it from this article) that will create all the necessary directories in the build folder, copy our file there and start 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()

Launching

Everything is ready! We compile and run our EFI application:

python Build.py build
python Build.py run

Final result

Conclusion

In this article, we looked at how to create a simple UEFI application and tested it on a QEMU virtual machine. You can see all the files (except gnu-efi, it somehow loaded crookedly) of the project on my GitHub.

Related posts