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. ForX86_64
, it will be namedbzImage
, but for other architectures, it will bevmlinuz.efi
.
Let's find a pre-compiled kernel in the Arch Linux repository[1]:
# We're only interested in the vmlinuz file |
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:
# Press ESC to enter uefi shell
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.
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 copyvmlinuz
.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 # copy files into UKI section
Test with Qemu
Create a directory and file efi/boot/bootx64.efi
[3], then run Qemu:
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.
More sections can be found in ukify manpage. ↩
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. ↩