Posted on 4 minutes read

Unified Kernel Image

UKIs can run on UEFI systems and simplify the distribution of small kernel images. For example, they simplify network booting with iPXE. UKIs make rootfs and kernels composable, making it possible to derive a rootfs for multiple kernel versions with one file for each pair.

A Unified Kernel Image (UKI) is a combination of a UEFI boot stub program, a Linux kernel image, an initramfs, and further resources in a single UEFI PE file (device tree, cpu µcode, splash screen, secure boot sig/key, ...). This file can either be directly invoked by the UEFI firmware or through a boot loader.

The simple way

If you use a systemd-based OS and want to use UKIs, you're probably better off using systemd-boot.

In this post, we aim to create a ready-to-boot UKI for another machine rather than the one we are creating it from.

We can use ukify, which is a dedicated tool to create UKIs, but I'd like to get a little more in-depth on how they are constructed.

Linux EFI

The Linux kernel should be compiled with CONFIG_EFI_STUB=y see https://docs.kernel.org/admin-guide/efi-stub.html

note

When building the kernel with EFI support, the final artifact name in the arch/"$ARCH"/boot/ directory changes. For X86_64, it will be named bzImage, but for other architectures, it will be vmlinuz.efi.

Let's find a pre-compiled kernel in the Arch Linux repository[1]:

export klatest=$(curl 'https://archive.archlinux.org/packages/l/linux/' | xmllint --recover --html --xpath 'string(//a[last() - 1]/@href)' -)
# We're only interested in the vmlinuz file
curl "https://archive.archlinux.org/packages/l/linux/${klatest}" | tar --zstd --wildcards --no-anchored  --transform='s:.*/::' -xf - 'vmlinuz'

We can see with readpe that the file is indeed a PE executable:

readpe vmlinuz
DOS Header
    Magic number:                    0x5a4d (MZ)
    Bytes in last page:              0
    Pages in file:                   0
    Relocations:                     0
    Size of header in paragraphs:    0
    Minimum extra paragraphs:        0
    Maximum extra paragraphs:        0
    Initial (relative) SS value:     0
    Initial SP value:                0
    Initial IP value:                0
    Initial (relative) CS value:     0
    Address of relocation table:     0
    Overlay number:                  0
    OEM identifier:                  0
    OEM information:                 0
    PE header offset:                0x40
COFF/File header
    Machine:                         0x8664 IMAGE_FILE_MACHINE_AMD64
    Number of sections:              4
    Date/time stamp:                 0 (Thu, 01 Jan 1970 00:00:00 UTC)
    Symbol Table offset:             0
    Number of symbols:               1
    Size of optional header:         0xa0
    Characteristics:                 0x206
    Characteristics names
                                         IMAGE_FILE_EXECUTABLE_IMAGE
                                         IMAGE_FILE_LINE_NUMS_STRIPPED
                                         IMAGE_FILE_DEBUG_STRIPPED
Optional/Image header
    Magic number:                    0x20b (PE32+)
    Linker major version:            2
    Linker minor version:            20
    Size of .text section:           0xc99000
    Size of .data section:           0x5f000
    Size of .bss section:            0
    Entrypoint:                      0xc93579
    Address of .text section:        0x5000
    ImageBase:                       0
    Alignment of sections:           0x1000
    Alignment factor:                0x200
    Major version of required OS:    0
    Minor version of required OS:    0
    Major version of image:          3
    Minor version of image:          0
    Major version of subsystem:      0
    Minor version of subsystem:      0
    Size of image:                   0xcfd000
    Size of headers:                 0x1000
    Checksum:                        0
    Subsystem required:              0xa (IMAGE_SUBSYSTEM_EFI_APPLICATION)
    DLL characteristics:             0x100
    DLL characteristics names
                                         IMAGE_DLLCHARACTERISTICS_NX_COMPAT
    Size of stack to reserve:        0
    Size of stack to commit:         0
    Size of heap space to reserve:   0
    Size of heap space to commit:    0
Data directories
Imported functions
Exported functions
Sections
    Section
        Name:                            .setup
        Virtual Size:                    0x3000 (12288 bytes)
        Virtual Address:                 0x1000
        Size Of Raw Data:                0x3000 (12288 bytes)
        Pointer To Raw Data:             0x1000
        Number Of Relocations:           0
        Characteristics:                 0x42000040
        Characteristic Names
                                             IMAGE_SCN_CNT_INITIALIZED_DATA
                                             IMAGE_SCN_MEM_DISCARDABLE
                                             IMAGE_SCN_MEM_READ
    Section
        Name:                            .compat
        Virtual Size:                    0x1000 (4096 bytes)
        Virtual Address:                 0x4000
        Size Of Raw Data:                0x1000 (4096 bytes)
        Pointer To Raw Data:             0x4000
        Number Of Relocations:           0
        Characteristics:                 0x42000040
        Characteristic Names
                                             IMAGE_SCN_CNT_INITIALIZED_DATA
                                             IMAGE_SCN_MEM_DISCARDABLE
                                             IMAGE_SCN_MEM_READ
    Section
        Name:                            .text
        Virtual Size:                    0xc99000 (13209600 bytes)
        Virtual Address:                 0x5000
        Size Of Raw Data:                0xc99000 (13209600 bytes)
        Pointer To Raw Data:             0x5000
        Number Of Relocations:           0
        Characteristics:                 0x60000020
        Characteristic Names
                                             IMAGE_SCN_CNT_CODE
                                             IMAGE_SCN_MEM_EXECUTE
                                             IMAGE_SCN_MEM_READ
    Section
        Name:                            .data
        Virtual Size:                    0x5f000 (389120 bytes)
        Virtual Address:                 0xc9e000
        Size Of Raw Data:                0x1200 (4608 bytes)
        Pointer To Raw Data:             0xc9e000
        Number Of Relocations:           0
        Characteristics:                 0xc0000040
        Characteristic Names
                                             IMAGE_SCN_CNT_INITIALIZED_DATA
                                             IMAGE_SCN_MEM_READ
                                             IMAGE_SCN_MEM_WRITE

With this, we can already boot Linux directly from the UEFI:

qemu-system-x86_64 -m 512M -enable-kvm -nographic -cpu host -bios /usr/share/ovmf/OVMF.fd -drive file=fat:rw:./,format=raw
# Press ESC to enter uefi shell
fs0:vmlinuz console=ttyS0 loglevel=6

The kernel crashed because there is no root FS, but it successfully booted as an EFI app.

SD Boot

Linux doesn't support UKIs directly, so we need glue code to copy UKI sections to memory and pass control to Linux. In the future UKI might directly be supported by the kernel, see nmbl.

For now, we need to compile systemd efi stub as the bootloader to put in the UKI so it copies Linux and initrd to the right place in memory, displays the splash screen, measures secure boot values, and finally gives control to Linux with the cmdline and dtb.

git clone https://github.com/systemd/systemd.git
cd systemd
meson setup build
cd build
ninja src/boot/efi/linuxx64.efi.stub
cp src/boot/efi/linuxx64.efi.stub ../../uki_base.efi

Populating UKI

UKIs have a PE file layout with standardized sections[2]. We will use the following:

  • .linux where we put the kernel; we can just copy vmlinuz
  • .osrel a key-value file with metadata about the booted image
  • .initrd the initramfs that will be loaded with the kernel

Previously we built a minimal rootfs that we are going to use in our UKI

Creating a UKI involves mainly objcopy with the right parameters to pack all data into one PE file.

We create two files that will be the UKI metadata and kernel command line:

# cmdline.txt
console=ttyS0 loglevel=6
# osrel.txt
NAME=UKI
PRETTY_NAME=UKI
VERSION_CODENAME=1
ID=1
BUILD_ID=1

We have uki_base.efi our shell UKI image; we now just have to fill the aforementioned sections.

The composition can be done in any order using multiple calls to objcopy.

# sd_boot_stub randomize the virtual memory address (vma) so we first need to get the `IMAGE_BASE` to put the section after the base.
# we give enough room between sections vma so they don't overlap for big files
export IMAGE_BASE=$(objdump -x "uki_base.efi" | grep ImageBase | awk '{print $2}')
# copy files into UKI section
objcopy \
    --add-section .osrel=osrel.txt \
    --add-section .cmdline=cmdline.txt \
    --add-section .initrd=initramfs.cpio \
    --add-section .linux=vmlinuz \
    --change-section-vma .osrel=$( (( off = 16#00020000 + 16#$IMAGE_BASE )); printf 0x%x $off ) \
    --change-section-vma .cmdline=$( (( off = 16#00020200 + 16#$IMAGE_BASE )); printf 0x%x $off ) \
    --change-section-vma .initrd=$( (( off = 16#00100000 + 16#$IMAGE_BASE )); printf 0x%x $off ) \
    --change-section-vma .linux=$( (( off = 16#02000000 + 16#$IMAGE_BASE )); printf 0x%x $off ) \
    --set-section-flags .linux=code,readonly \
    uki_base.efi bootx64.efi

Test with Qemu

Create a directory and file efi/boot/bootx64.efi[3], then run Qemu:

mkdir -p efi/boot
cp bootx64.efi efi/boot/
qemu-system-x86_64 -m 512M -enable-kvm -nographic -bios /usr/share/ovmf/OVMF.fd -cpu host -drive file=fat:rw:./,format=raw

Conclusion

We created a single UKI file that contains a kernel, initramfs, cmdline, and metadata. It is easy to distribute and boot with any UEFI-compatible BIOS of the same architecture. We can imagine giving the UKI to U-Boot or iPXE to boot it in even more environments.

Next time, we will see how to sign the UKI so it works with secure boot.


  1. Found this usage of curl + xmllint here ↩

  2. More sections can be found in ukify manpage. ↩

  3. A FAT partition with a file efi/boot/bootx64.efi will be booted automatically by most UEFI implementations. The file suffix depends on the target CPU architecture. ↩