10a

Finding the Kernel

GRUB locates the kernel image on disk and prepares to load it into memory.

GRUB has read its configuration, shown a menu, and the user (or the timeout) has selected an entry. The linux command in that entry specifies a path: something like /vmlinuz-6.2.0 or /boot/vmlinuz-6.2.0. GRUB must now find that file on disk, figure out what kind of image it is, and load it into the right place in memory.

This sounds simple, and for GRUB it mostly is -- GRUB has filesystem drivers that can read ext4, XFS, and other formats. But the kernel image itself is not straightforward. It is a compressed, multi-part file with a specific internal structure that GRUB must understand. The name vmlinuz hints at that structure, and the story behind it reaches back decades.

Where the Kernel Lives

On most Linux systems, kernel images live in /boot. This directory is sometimes on the root filesystem and sometimes on a separate small partition. A separate /boot partition was historically necessary because older bootloaders could not read beyond certain disk boundaries, or could not understand the root filesystem's format. Today, GRUB can handle most filesystems and disk sizes, but the convention persists.

Inside /boot, you will typically find several files for each installed kernel:

/boot/vmlinuz-6.2.0-36-generic
/boot/initrd.img-6.2.0-36-generic
/boot/config-6.2.0-36-generic
/boot/System.map-6.2.0-36-generic

The version number in each filename matches a specific kernel build. When you upgrade the kernel, your package manager installs new files with the new version number and runs grub-mkconfig to regenerate the GRUB configuration with updated paths.

Key term: vmlinuz The compressed Linux kernel image. The name comes from "Virtual Memory LINUx" with a "z" appended to indicate compression (like gzip). Despite the name, vmlinuz is not a standard compressed file -- it is a specially structured image containing a decompression stub, a setup header, and the compressed kernel proper.

The symbolic link /vmlinuz (in the root directory) often points to the latest installed kernel image in /boot. GRUB configurations may reference either the full path or the symlink.

The Kernel Image Format

The file vmlinuz is not a simple executable. It is not an ELF binary that you could run with a normal program loader. It is a custom format specific to the Linux boot process, and it contains several distinct parts packed together.

Think of it like a shipping container. The outside of the container has labels and handling instructions (the setup header). Inside the container is a smaller box containing the actual cargo, wrapped in packing material (the compressed kernel, surrounded by the decompression stub). You cannot use the cargo until you unpack the box.

Fig. 10a-1 -- Structure of a bzImage file
vmlinuz (bzImage) Boot Sector + Setup Header real-mode code ~15 KB (setup_sects sectors) Decompression Stub small program that unpacks the kernel Compressed Kernel (vmlinux.bin.gz) the actual kernel compressed with gzip/lz4/zstd/xz

GRUB reads this ---> to know how to load this --->

A bzImage contains three parts: the real-mode setup code with a header that describes the image, a small decompression stub, and the compressed kernel itself.

The three parts serve different purposes:

The boot sector and setup header occupy the first portion of the file. The boot sector is a legacy artifact -- it allows the kernel to be booted directly from a floppy disk without a bootloader. Nobody does this anymore, but the structure remains. The setup header, however, is critical. It contains fields that GRUB reads to know the kernel's version, where to load it in memory, what boot protocol version it supports, and the size of the setup code.

The decompression stub is a small self-contained program. Its job is to decompress the next part. We will cover this in detail in the next article.

The compressed kernel is the actual Linux kernel -- the vmlinux ELF binary, stripped and then compressed with gzip, LZ4, LZMA, XZ, or Zstandard (depending on the kernel's build configuration).

vmlinux vs. vmlinuz vs. bzImage

These names are confusing because they evolved over decades and carry historical baggage. Here is the lineage:

vmlinux is the raw, uncompressed Linux kernel. It is an ELF binary -- the direct output of the kernel build's linking step. It contains debug symbols and section headers. On a modern kernel, it can be 30-80 MB or more. You will find it in the kernel build tree but almost never in /boot.

zImage was the original compressed kernel format. The "z" stands for the compression. It was limited to 512 KB of compressed data because it loaded the kernel below the 1 MB mark in memory (the real-mode address space). This limit made it unusable for modern kernels.

bzImage stands for "big zImage." It lifts the size restriction by loading the compressed kernel above the 1 MB mark. Despite the name containing "bz," the compression is not bzip2 -- it was historically gzip, and modern kernels can use several algorithms. Nearly every Linux system today uses bzImage.

vmlinuz is the file you find in /boot. It is a bzImage -- the same file, just installed with a different name.

Key term: bzImage The standard format for a bootable Linux kernel image. "Big zImage" loads the compressed kernel above the 1 MB boundary, allowing much larger kernels than the original zImage format. The file installed as /boot/vmlinuz is a bzImage.
Fig. 10a-2 -- Kernel image name lineage
vmlinux raw ELF, 30-80 MB strip + compress vmlinux.bin.gz compressed, 8-12 MB add stub + header bzImage bootable, 8-13 MB /boot/vmlinuz same file, installed zImage historical, <512 KB
The kernel build pipeline: vmlinux is compiled, stripped and compressed, then wrapped with a setup header and decompression stub to produce a bzImage. That bzImage is installed as /boot/vmlinuz.

The Setup Header

The setup header is the critical interface between the bootloader and the kernel. It is a binary structure starting at offset 0x01F1 in the kernel image, and it tells the bootloader everything it needs to know about how to load this particular kernel.

GRUB reads the following fields from the setup header:

  • setup_sects: How many 512-byte sectors the setup code occupies. This tells GRUB where the protected-mode kernel code begins in the file.
  • syssize: The size of the protected-mode code, in 16-byte paragraphs.
  • boot_flag: Must be 0xAA55 (same magic number as the MBR, for historical reasons).
  • header: Must be the magic string "HdrS" (0x53726448). This confirms the file is a valid Linux kernel image.
  • version: The boot protocol version. Different versions support different features. Modern kernels use protocol 2.15 or higher.
  • loadflags: Bit flags that control loading behavior. Bit 0 indicates whether the kernel was compiled as a bzImage (loaded high) or a zImage (loaded low).
  • cmd_line_ptr: The physical address where GRUB should place the kernel command-line string.
  • initrd_addr_max: The maximum physical address where the initrd can end. This prevents GRUB from loading the initrd above the kernel's addressable range.
  • ramdisk_image and ramdisk_size: Where GRUB loaded the initrd and how large it is. GRUB fills these in so the kernel can find the initrd later.
Key term: Boot protocol A versioned specification that defines the contract between the Linux kernel and the bootloader. The boot protocol defines which fields exist in the setup header, what values are valid, and what the bootloader must do before jumping to the kernel. The protocol version increments when new fields or capabilities are added.
Fig. 10a-3 -- Key setup header fields

Setup Header (starts at file offset 0x01F1)

setup_sects Number of setup sectors (real-mode code size) header Magic "HdrS" = valid Linux kernel version Boot protocol version (e.g. 0x020F = 2.15) loadflags Bit 0: loaded_high (1 = bzImage) cmd_line_ptr Address for kernel command line string ramdisk_image Bootloader writes initrd load address here ramdisk_size Bootloader writes initrd size here payload_offset Offset to compressed kernel within the image

0x01F1 0x0202 0x0206 0x0211 0x0228 0x0218 0x021C 0x0248

The setup header contains fields that GRUB reads (yellow, blue) and fields that GRUB writes (orange) to communicate with the kernel. The protocol version determines which fields are present.

How GRUB Loads the Image

When GRUB processes the linux command, it executes a precise loading sequence:

  1. Open the file. GRUB uses its filesystem driver to open vmlinuz from the specified partition and path.

  2. Read the setup header. GRUB reads the first few kilobytes of the file and parses the setup header starting at offset 0x01F1. It checks for the "HdrS" magic and the boot protocol version.

  3. Validate the image. GRUB checks that the loadflags indicate a bzImage (it refuses to load a zImage on modern systems where the kernel would not fit below 1 MB). It verifies the protocol version is recent enough to support the features GRUB needs.

  4. Load the real-mode code. The first setup_sects + 1 sectors of the file (the boot sector plus the setup sectors) contain the real-mode kernel code. GRUB loads this to a low memory address, typically around 0x10000 (64 KB).

  5. Load the protected-mode code. Everything after the setup sectors is the protected-mode kernel -- the decompression stub followed by the compressed kernel. GRUB loads this at the 1 MB mark (address 0x100000) or higher if the setup header specifies a preferred address.

  6. Store boot parameters. GRUB writes the kernel command line to memory and records its address in cmd_line_ptr. It fills in the memory map, video mode information, and other fields in the boot parameters structure (the "zero page" at the start of the real-mode code segment).

The kernel image vmlinuz is a bzImage containing a setup header, a decompression stub, and the compressed kernel. GRUB reads the setup header to learn how to load the image, places the real-mode code in low memory and the protected-mode code above 1 MB, and fills in boot parameters so the kernel knows where everything is. The setup header is the contract between bootloader and kernel.

The initrd Companion

When GRUB processes the initrd command, it loads the initramfs image into memory above the kernel. The setup header field initrd_addr_max tells GRUB the highest address the kernel can access for the initrd, and GRUB loads it as high as possible within that limit (to leave room for the kernel to decompress and expand).

GRUB then writes the initrd's physical address and size into the ramdisk_image and ramdisk_size fields of the boot parameters. The kernel will read these values during its initialization to find the initramfs.

The initrd is not optional on most systems. Without it, the kernel would need every driver compiled in -- SATA, NVMe, SCSI, USB, LVM, dm-crypt, every filesystem -- to mount the root partition. The initramfs provides exactly the drivers needed for this specific machine, keeping the kernel itself smaller and more generic.

Finding the Kernel on UEFI

On UEFI systems, the process is slightly different but the kernel image format is the same. The UEFI version of GRUB reads files from the EFI System Partition (or from the /boot partition, which GRUB can access via its filesystem modules). The kernel is still a bzImage with the same setup header.

One notable UEFI feature: modern Linux kernels (since about 2011, with the EFI boot stub) can be booted directly by the firmware without GRUB at all. The kernel's EFI stub makes the bzImage look like a valid EFI application. The firmware loads it, the EFI stub runs, sets up the boot parameters itself, and jumps to the kernel. This is the basis of systemd-boot and other simple UEFI boot managers.

But whether GRUB or the firmware loads the image, the kernel's internal structure is the same. The setup header, the decompression stub, and the compressed kernel are all present. The next step is always the same: decompress.

What Happens Next

GRUB has found the kernel file, parsed its setup header, loaded its real-mode and protected-mode parts into the correct memory addresses, and filled in the boot parameters. The initramfs is in memory too. Everything is staged and ready.

Now the CPU must jump to the kernel's entry point. But the code it jumps to is not the kernel itself -- it is the decompression stub. The real kernel is still compressed, packed inside the bzImage. The stub must unpack it, and to do that, it first needs to leave real mode behind.

Next: Decompressing and Handing Off