Skip to main content

Updated Sep 3, 2026

Inside bzImage

The layout of a compressed kernel image, and why vmlinux, vmlinuz, and bzImage are three different things.

vmlinux, vmlinuz, and bzImage are three names people use interchangeably, and they name three different files with three different jobs. Confusing them costs people hours — pointing GDB at the wrong one, or trying to objdump a file that was never meant to be read as one. This page opens the actual file up: what's inside it, who is responsible for filling in each part, and how a boot loader's few bytes of trust turn into a running kernel.

The three names​

NameWhat it isBootable?Used for
vmlinuxAn uncompressed ELF binary, with full debug symbols if built with themNo — nothing in the boot chain knows how to load a bare ELF this wayGDB, objdump, nm — anything that needs symbol information
bzImageReal-mode setup code, plus a setup header, plus a compressed payload with its own self-extracting decompressorYes — this is what arch/x86/boot/ actually produces and what a boot loader loadsBooting the machine
vmlinuzA distribution's installed copy of a bzImage, under a conventional name in /bootYes — it's the same file, just installed and renamedSame as bzImage — the z is a hint that it's compressed, nothing more

Three names for kernel output, and why only one of them is safe to load into a debugger.

The layout of a bzImage​

A bzImage is not "a compressed vmlinux" — it's a small, self-sufficient program with the compressed kernel riding along as data:

  1. Real-mode setup code — the part of the file that still runs in 16-bit real mode, the same constraint legacy BIOS imposes on anything it loads directly. This code does minimal hardware setup and reads the setup header the loader filled in.
  2. The setup header — a fixed-layout block of fields, described below, that is the entire contract between whatever loaded this file and the kernel about to run.
  3. The compressed payload — the actual kernel, compressed with whatever algorithm the build chose, plus a small decompressor stub that runs once the setup code hands off. The decompressor is not the boot loader's job and not the setup code's job — it's part of the payload itself.

The setup header​

The setup header is arch/x86/include/uapi/asm/bootparam.h:setup_header — a packed struct with no padding, because its layout is a binary ABI, not an internal kernel detail. The fields a boot loader actually has to care about:

The setup_header fields a boot loader has to fill in or check — gaps compressed, not to scale

The gray blocks are the fields between the named ones — root_flags, syssize, jump, realmode_swtch, setup_move_size, and others — compressed to a fixed width purely so the fields that matter stay readable. Real offsets, from Documentation/arch/x86/boot.rst:

FieldOffsetSizeMeaning
boot_flag0x1FE2 bytesMust contain 0xAA55 — the closest thing this format has to a magic number
header0x2024 bytesMust contain "HdrS" (0x53726448) — confirms this is a real boot-protocol header, not an old-format image
version0x2062 bytesBoot protocol version, as (major << 8) + minor
loadflags0x2111 byteBitmask — includes CAN_USE_HEAP, QUIET_FLAG, and a bit the decompressor uses internally to signal KASLR status to the kernel proper
code32_start0x2144 bytesProtected-mode entry address; defaults to the kernel's own load address
ramdisk_image0x2184 bytesLinear address of the initramfs, once loaded
ramdisk_size0x21C4 bytesSize of the initramfs at that address
cmd_line_ptr0x2284 bytesLinear address of the kernel command line string

Who fills the header in​

This table is the loader/kernel contract — every field is one of three kinds, and getting the kind wrong is a real class of boot-loader bug:

KindMeaningExample fields
readThe kernel reads this; a loader must never modify itboot_flag, header, version
write (obligatory)The loader must fill this in correctly, or the kernel gets garbageramdisk_image, ramdisk_size, cmd_line_ptr, loadflags
modify (optional)The loader may override the kernel's own default, but doesn't have tocode32_start

A loader that gets an obligatory write field wrong doesn't fail cleanly — ramdisk_size set to zero when an initramfs really was loaded, for instance, means the kernel proceeds believing there is no initramfs at all, which looks like an early-user-space failure with no boot-loader-shaped explanation anywhere in the trail.

Decompression​

The payload can be compressed with any of several algorithms the build selects — CONFIG_KERNEL_GZIP, CONFIG_KERNEL_XZ, and CONFIG_KERNEL_ZSTD are the common choices, alongside less common ones the same init/Kconfig menu offers. Whichever one is chosen, the decompressor built into the payload is the only thing that knows how to undo it — nothing outside the bzImage needs to know or care which compression scheme it is. The decompressor runs, relocates the now-uncompressed kernel to its final address, and jumps into the kernel proper's own arch/x86/kernel/head_64.S:startup_64() — a different symbol of the same name from the one inside the decompressor's own arch/x86/boot/compressed/head_64.S:startup_64() entry point, since the decompressor and the kernel it unpacks are two separate compiled programs that happen to share an entry-point name. From there, early boot is architecture setup in C, culminating in start_kernel().

What actually happens​

Getting from an installed image back to something GDB can use is one command, not archaeology:

$ file /boot/vmlinuz-6.18.0
/boot/vmlinuz-6.18.0: Linux kernel x86 boot executable bzImage, version 6.18.0 (build@host) ...,
RO-rootFS, swap_dev 0x0, Normal VGA

file already recognises the format — that's the "three names" table's bzImage row, confirmed on a real installed file. Getting the ELF back out:

$ scripts/extract-vmlinux /boot/vmlinuz-6.18.0 > /tmp/vmlinux
extract-vmlinux: Extracted vmlinux using 'unxz' from offset 12345
$ file /tmp/vmlinux
/tmp/vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ...

scripts/extract-vmlinux tries each supported decompressor's magic bytes against the file in turn until one of them produces something file or readelf recognises as an ELF binary — it doesn't need to be told which algorithm was used, because it just tries all of them. The output is a real, symbol-bearing ELF — usable for objdump -d, but only fully useful in GDB if it's from the same build as the running kernel, since a rebuild can change addresses and inlining even with identical source.

Misconceptions​

  • "bzImage means bzip2." It means big zImage. The original zImage format had a hard 512 KB size limit; bzImage is the format that lifted it. The name predates bzip2 support in the kernel build and has nothing to do with that compression algorithm.
  • "vmlinuz can be loaded into GDB." Not usefully. vmlinuz is a bzImage — setup code plus a compressed payload — not an ELF file with symbols. GDB needs vmlinux from the exact same build.
  • "The boot loader decompresses the kernel." It doesn't. The boot loader's job ends at handoff; the compressed payload carries its own decompressor and extracts itself once running.

References​

  • The x86 boot protocol — the primary source for every setup-header field, its offset, and whether the loader must read, write, or may optionally modify it.
  • The zero-page layout — struct boot_params, the larger structure the setup header lives inside once the kernel is running.
  • scripts/extract-vmlinux via scripts/extract-vmlinux — the tool this page's "What actually happens" section uses to recover an ELF vmlinux from an installed bzImage.