Boot: firmware, boot loader, kernel and initramfs

From power-on to systemd, and timing it.

Advanced16 min · lesson 6 of 21

A server that does not come back after a kernel update, or takes minutes to boot, has failed at one stage of a fixed chain: firmware, shim, boot loader, kernel, initramfs, systemd. This lesson follows that chain on running Ubuntu 26.04 and RHEL 10 machines, using the evidence each stage leaves behind: UEFI boot entries, the files on the EFI system partition, the kernel command line, the kernel log, the dracut initramfs and systemd's boot timing. You will change a kernel parameter the supported way on each distribution, find the unit that gates the boot, and know which levers exist when a machine stops before you can log in. Nothing here reboots the lab machines.

From power-on to systemd
1UEFI firmware
starts a boot entry; verifies shim under Secure Boot
2shim
signed by Microsoft; checks GRUB and kernel with distro key
3GRUB
Ubuntu grub.cfg, RHEL BLS entries; sets the command line
4Linux kernel
starts hardware, unpacks the initramfs, runs /init
5initramfs (dracut)
systemd in the initrd mounts root, then switches root
6systemd on the real root
starts units towards default.target
The same chain on Ubuntu 26.04 and RHEL 10; the configuration files differ.

Firmware, shim and the boot entry

UEFI firmware keeps its boot entries in non-volatile variables, each naming a file on the EFI system partition (ESP), a small FAT partition that Linux mounts at /boot/efi. /sys/firmware/efi exists only when the running system was started by UEFI firmware, and efibootmgr lists the entries.

deploy@web01 · Ubuntu 26.04 LTS
$ ls /sys/firmware/efi
efivars fw_platform_size mok-variables systab
$ efibootmgr
BootCurrent: 0002 Timeout: 0 seconds BootOrder: 0002,0000,0001 Boot0000* UEFI Misc Device PciRoot(0x0)/Pci(0x6,0x0){auto_created_boot_option} Boot0001* UEFI Misc Device 2 PciRoot(0x0)/Pci(0x7,0x0){auto_created_boot_option} Boot0002* Ubuntu HD(15,GPT,a0419627-af54-4502-aa5b-076f0aafcbca,0x800,0x31801)/\EFI\ubuntu\shimaa64.efi

BootCurrent: 0002 is the entry this boot used: "Ubuntu", which starts \EFI\ubuntu\shimaa64.efi on partition 15, the ESP. The two "UEFI Misc Device" entries are ones the firmware created for the VM's disks. On an x86_64 server the files are shimx64.efi and grubx64.efi.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo ls /boot/efi/EFI/ubuntu /boot/efi/EFI/BOOT
/boot/efi/EFI/BOOT: BOOTAA64.EFI fbaa64.efi mmaa64.efi /boot/efi/EFI/ubuntu: BOOTAA64.CSV grub.cfg grubaa64.efi mmaa64.efi shimaa64.efi

shimaa64.efi is the first stage. With Secure Boot on, the firmware only runs code signed by a key in its database, and most x86 hardware ships with Microsoft's UEFI certificates there. shim is signed by Microsoft and carries the distribution's own certificate (Canonical's on Ubuntu, Red Hat's on RHEL), which it uses to verify grubaa64.efi and then the kernel. mmaa64.efi is MokManager, the console tool for enrolling your own Machine Owner Keys, for example to sign a third-party kernel module. \EFI\BOOT\BOOTAA64.EFI is a copy of shim at the path firmware tries when no boot entry works; it then runs fbaa64.efi, which recreates the missing entries from BOOTAA64.CSV.

deploy@web01 · Ubuntu 26.04 LTS
$ mokutil --sb-state
This system doesn't support Secure Boot
$ journalctl -k -b --no-hostname | grep -i secureboot
Sep 27 08:26:06 kernel: secureboot: Secure boot disabled Sep 27 08:26:06 kernel: ima: secureboot mode disabled

This VM's firmware does not implement Secure Boot at all, so mokutil says so and the kernel logs it as disabled; on a machine with Secure Boot on, mokutil --sb-state prints SecureBoot enabled. A lab VM cannot show you signature checks, MokManager prompts or TPM measurements (hashes of each boot stage recorded in the Trusted Platform Module, a security chip); they only happen on firmware that supports them. RHEL shows the same. There, sudo bootctl status summarises the firmware, the ESP and the EFI entries in one view; on Ubuntu the command comes in the systemd-boot-tools package, which a default Ubuntu Server install does not include:

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ command -v bootctl dpkg -l systemd-boot-tools
dpkg-query: no packages found matching systemd-boot-tools

command -v prints nothing and dpkg knows no such package. On RHEL, bootctl also warns about the grub_* lines in the boot entries, which it does not understand; those lines are left out below.

deploy@rocky10 · Rocky Linux 10.2
$ sudo bootctl status
… System: … Firmware Arch: aa64 Secure Boot: disabled (unsupported) … Boot Loaders Listed in EFI Variables: Title: rocky ID: 0x0002 Status: active, boot-order Partition: /dev/disk/by-partuuid/30ef8bef-ef6a-4da6-9054-e5ab9dbaddbb File: └─/EFI/rocky/shimaa64.efi …

GRUB and the kernel command line

The GRUB configuration on the ESP is only a pointer: it finds the /boot filesystem by UUID and loads the real configuration from there.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo cat /boot/efi/EFI/ubuntu/grub.cfg
search.fs_uuid e57a54d1-fbe4-4b61-93d1-e8a9516127ff root set prefix=($root)'/grub' configfile $prefix/grub.cfg
$ grep -E '^GRUB_(TIMEOUT|TIMEOUT_STYLE|CMDLINE_LINUX_DEFAULT)=' /etc/default/grub /etc/default/grub.d/*.cfg
/etc/default/grub:GRUB_TIMEOUT_STYLE=hidden /etc/default/grub:GRUB_TIMEOUT=0 /etc/default/grub:GRUB_CMDLINE_LINUX_DEFAULT="quiet splash" /etc/default/grub.d/50-cloudimg-settings.cfg:GRUB_TIMEOUT=0 /etc/default/grub.d/50-cloudimg-settings.cfg:GRUB_CMDLINE_LINUX_DEFAULT="console=tty1 console=ttyAMA0"

On Ubuntu, /boot/grub/grub.cfg is generated by update-grub from /etc/default/grub, then every file in /etc/default/grub.d/, and the scripts in /etc/grub.d/; never edit it by hand, because the next kernel update regenerates it. Later files override earlier ones, which is why the cloud image's drop-in replaces quiet splash with two consoles. GRUB_TIMEOUT_STYLE=hidden hides the menu until the timeout passes; pressing Esc or holding Shift during it shows the menu, but with GRUB_TIMEOUT=0, as on this image, there is no such window, so set a timeout before you need the menu.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo grep -m1 -E '^\s+linux\s' /boot/grub/grub.cfg
linux /vmlinuz-7.0.0-34-generic root=UUID=daaf8301-e652-48d8-ae35-1c7fc60c3182 ro console=tty1 console=ttyAMA0
$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-7.0.0-34-generic root=UUID=daaf8301-e652-48d8-ae35-1c7fc60c3182 ro console=tty1 console=ttyAMA0

The linux line in grub.cfg is what GRUB will pass next time. /proc/cmdline is what the running kernel actually received, with BOOT_IMAGE added by GRUB, and it is the only reliable answer to "is this parameter active?" RHEL does it differently: its GRUB reads Boot Loader Specification (BLS) entries, one small file per installed kernel in /boot/loader/entries/, managed with grubby.

deploy@rocky10 · Rocky Linux 10.2
$ sudo ls /boot/loader/entries/
1d349827085342da994400280f0d7183-0-rescue.conf 1d349827085342da994400280f0d7183-6.12.0-211.16.1.el10_2.0.1.aarch64.conf 35aeb5b63c50495e85e5ba4beb292cca-0-rescue.conf 35aeb5b63c50495e85e5ba4beb292cca-6.12.0-211.60.1.el10_2.aarch64.conf
$ sudo sh -c 'cat /boot/loader/entries/*-$(uname -r).conf'
title Rocky Linux (6.12.0-211.16.1.el10_2.0.1.aarch64) 10.2 (Red Quartz) version 6.12.0-211.16.1.el10_2.0.1.aarch64 linux /vmlinuz-6.12.0-211.16.1.el10_2.0.1.aarch64 initrd /initramfs-6.12.0-211.16.1.el10_2.0.1.aarch64.img options console=ttyS0,115200n8 no_timer_check crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M root=UUID=adbc700e-905a-4aa3-90a2-084cd25fcc12 …
$ uname -r sudo grubby --default-kernel
6.12.0-211.16.1.el10_2.0.1.aarch64 /boot/vmlinuz-6.12.0-211.60.1.el10_2.aarch64

The entries directory is readable only by root, so the wildcard has to expand in a root shell. The entry for the running kernel holds its title, kernel, initramfs and options; the grub_* lines are GRUB extensions. The last command is worth remembering: the running kernel is 211.16.1, but grubby reports 211.60.1 as the default, because an update installed a newer kernel that this VM has not started yet. A difference between these two lines usually means a kernel update is waiting for a reboot, but a default someone pinned with grubby --set-default or a one-off boot of another entry looks the same, so confirm it:

deploy@rocky10 · Rocky Linux 10.2
$ dnf needs-restarting -r
Core libraries or services have been updated since boot-up: * kernel-core Reboot is required to fully utilize these updates. More information: https://access.redhat.com/solutions/27943

dnf needs-restarting -r compares what is running with what is installed and exits 1 when a reboot is needed, here because of kernel-core, so it also works in scripts and monitoring.

deploy@rocky10 · Rocky Linux 10.2
$ cat /etc/kernel/cmdline ls /usr/lib/kernel/install.d/
console=ttyS0,115200n8 no_timer_check crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M root=UUID=adbc700e-905a-4aa3-90a2-084cd25fcc12 10-devicetree.install 20-grub.install 50-depmod.install 50-dracut.install 51-dracut-rescue.install 60-kdump.install 90-loaderentry.install 90-uki-copy.install 92-crashkernel.install 95-kernel-hooks.install 99-grub-mkconfig.install

When a kernel package is installed, kernel-install runs the plugins in /usr/lib/kernel/install.d/: 20-grub.install writes the BLS entry and 50-dracut.install builds the initramfs, and /etc/kernel/cmdline is where kernel-install looks first for the options. To change the command line on RHEL, use grubby --update-kernel=ALL --args=..., as the method lesson did with psi=1; on Ubuntu, add a drop-in to /etc/default/grub.d/ and run update-grub, as the exercise at the end does. Either way the change takes effect at the next boot.

The kernel and the dracut initramfs

The kernel's own messages, with timestamps in seconds since it started, show the rest of the handover. -t kernel -t systemd selects the messages of the kernel and of systemd.

deploy@web01 · Ubuntu 26.04 LTS
$ journalctl -b -o short-monotonic --no-hostname -t kernel -t systemd | grep -E 'Kernel command line|unpack rootfs|Freeing initrd|Run /init|Running in initrd|Switching root'
[ 0.196529] kernel: Kernel command line: BOOT_IMAGE=/vmlinuz-7.0.0-34-generic root=UUID=daaf8301-e652-48d8-ae35-1c7fc60c3182 ro console=tty1 console=ttyAMA0 [ 0.201625] kernel: Trying to unpack rootfs image as initramfs... [ 0.203790] kernel: Freeing initrd memory: 64192K [ 0.203864] kernel: Run /init as init process [ 0.203904] systemd[1]: Running in initrd. [ 1.312437] systemd[1]: Switching root.

At 0.20 s the kernel prints the command line it received. It then unpacks the initramfs, a compressed archive GRUB loaded into memory next to the kernel, frees it once unpacked, and runs /init from it. /init is systemd, which reports "Running in initrd". The initramfs exists because what the kernel needs to find the root filesystem (storage drivers built as modules, LVM, software RAID, LUKS encryption, network storage) lives on disks it cannot read yet. At 1.31 s systemd switches the root to the real filesystem and starts again from there.

deploy@web01 · Ubuntu 26.04 LTS
$ dpkg -S /usr/sbin/update-initramfs dracut --version
dracut: /usr/sbin/update-initramfs dracut 110-11ubuntu0.1
$ sudo lsinitrd | grep -E '^Image:| init -> | usr/lib/systemd/systemd$'
Image: /boot/initrd.img-7.0.0-34-generic: 62M lrwxrwxrwx 1 root root 23 Sep 27 09:15 init -> usr/lib/systemd/systemd -rwxr-xr-x 1 root root 133696 Jul 27 20:44 usr/lib/systemd/systemd

Both distributions build the initramfs with dracut: Ubuntu 26.04 uses dracut 110, and update-initramfs, the command older Ubuntu guides use, is now a wrapper shipped by the dracut package (initramfs-tools is not installed). lsinitrd lists the image's contents; /init is a link to systemd. RHEL 10 uses dracut 107 with the same layout and names the file initramfs-VERSION.img instead of initrd.img-VERSION.

deploy@rocky10 · Rocky Linux 10.2
$ rpm -q dracut sudo lsinitrd | grep -E '^Image:| init -> | usr/lib/systemd/systemd$'
dracut-107-9.el10_2.aarch64 Image: /boot/initramfs-6.12.0-211.16.1.el10_2.0.1.aarch64.img: 49M lrwxrwxrwx 1 root root 23 Jan 30 2026 init -> usr/lib/systemd/systemd -rwxr-xr-x 1 root root 135616 Jan 30 2026 usr/lib/systemd/systemd

Kernel packages rebuild the image on install. Rebuild it yourself after changing something it contains, such as a storage driver option, /etc/crypttab or a file in /etc/dracut.conf.d/: sudo update-initramfs -u on Ubuntu, sudo dracut -f on RHEL. A broken image stops the boot in the initramfs, so keep the previous kernel and its image installed until the new one has booted.

systemd takes over: timing the boot

deploy@web01 · Ubuntu 26.04 LTS
$ systemd-analyze time
Startup finished in 117ms (kernel) + 1.248s (initrd) + 2.884s (userspace) = 4.249s graphical.target reached after 2.489s in userspace.
$ systemctl get-default
graphical.target

systemd-analyze time splits the boot into the kernel (117 ms), the initrd (1.248 s) and userspace (2.884 s), and says when the default target was reached. The default target is graphical.target because that is what this cloud image sets (systemctl get-default); on a server with no display manager it adds nothing to multi-user.target, and the result is the same. There are no firmware or loader figures: the boot loader has to report them through the Boot Loader Interface, which systemd-boot implements and GRUB does not, so a GRUB machine never shows them. The same command on RHEL tells a different story.

deploy@rocky10 · Rocky Linux 10.2
$ systemd-analyze time
Startup finished in 546ms (kernel) + 1.943s (initrd) + 6.929s (userspace) = 9.419s multi-user.target reached after 2.950s in userspace.
$ systemd-analyze blame | head -3
6.481s dnf-makecache.service 3.773s cloud-final.service 2.226s dev-vport1p0.device

multi-user.target was reached after 2.95 s, yet startup "finished" after 6.9 s of userspace. systemd reports startup as finished only when every job queued at boot has completed, and cloud-final.service, a one-shot cloud-init job that runs at the end of the boot, took 3.8 s and finished last. The machine was usable before that. dnf-makecache.service at the top of the list did not delay the boot at all: its timer started it half an hour later. Read both lines of systemd-analyze time before calling a boot slow.

deploy@web01 · Ubuntu 26.04 LTS
$ systemd-analyze blame | head -6
1.678s sys-devices-virtual-misc-rfkill.device 1.678s dev-rfkill.device 1.653s dev-vport1p0.device 1.653s sys-devices-pci0000:00-0000:00:05.0-virtio1-virtio\x2dports-vport1p0.device 1.488s dev-disk-by\x2dlabel-BOOT.device 1.488s dev-disk-by\x2duuid-e57a54d1\x2dfbe4\x2d4b61\x2d93d1\x2de8a9516127ff.device
$ systemd-analyze critical-chain
The time when unit became active or started is printed after the "@" character. The time the unit took to start is printed after the "+" character. graphical.target @2.489s └─multi-user.target @2.488s └─snapd.seeded.service @1.946s +541ms └─basic.target @1.913s └─sockets.target @1.913s └─sshd-vsock.socket @1.909s +4ms └─sysinit.target @1.899s └─cloud-init-network.service @1.484s +414ms └─cloud-init-local.service @1.415s +67ms └─cloud-init-main.service @238ms +452ms └─systemd-remount-fs.service @210ms +26ms └─systemd-fsck-root.service └─dracut-pre-mount.service └─cryptsetup.target @354ms └─systemd-ask-password-console.path @354ms

systemd-analyze blame ranks units by how long they took to start, and that ranking alone does not tell you what delayed the boot. It lists every unit started since boot, including timer jobs that run hours later, and here its top entries are device units, none of which is on the path below. A slow unit that nothing waits for does not delay the boot. critical-chain shows the path that did gate the default target. After @ is the time, counted from the start of userspace, at which the unit became active or started; after + is how long it took to start. Units that ran in the initrd, such as systemd-fsck-root.service and dracut-pre-mount.service, have no times on this chain. sshd-vsock.socket is the SSH socket the VM tool adds (the units lesson explains it). Here snapd.seeded.service held up multi-user.target for 541 ms. Optimise the units on this chain; shortening anything else moves nothing.

deploy@web01 · Ubuntu 26.04 LTS
$ journalctl --list-boots
IDX BOOT ID FIRST ENTRY LAST ENTRY -2 fe9d6a1664c14b67aa73a56205ff2435 Sat 2026-09-26 19:46:57 UTC Sat 2026-09-26 19:56:28 UTC -1 2a043efbc2dd462eac4deeacce00e4aa Sat 2026-09-26 19:56:31 UTC Sun 2026-09-27 08:26:02 UTC 0 60492fd2965b48179fa78e0894522e50 Sun 2026-09-27 08:26:06 UTC Sun 2026-09-27 09:31:29 UTC
$ journalctl -b -1 -n 3 --no-hostname
Sep 27 08:26:02 systemd-udevd[478]: Failed to remove file descriptor "config-serialization" from the store, ignoring: Connection refused Sep 27 08:26:02 systemd-journald[1123911]: Received SIGTERM from PID 1 (systemd-shutdow). Sep 27 08:26:02 systemd-journald[1123911]: Journal stopped

The journal keeps each boot separately (Ubuntu stores it persistently in /var/log/journal). -b -1 is the previous boot, and its last lines show how it ended: journald received SIGTERM from systemd-shutdown and stopped, an orderly shutdown. A crash or power loss ends mid-stream with no shutdown messages. After any unexpected reboot, systemctl --failed lists the units that did not start.

When the boot stops

Without a login you work from the console, which on a cloud instance is the provider's serial console. At the GRUB menu, press e on an entry, add parameters to the end of the linux line and press Ctrl-X or F10 to boot once with them; nothing is saved. The useful ones: systemd.unit=rescue.target gives a single-user shell with the local filesystems mounted and only basic services; systemd.unit=emergency.target gives a shell with nothing else started, not even other mounts; rd.break (dracut) stops in the initramfs before the switch to the real root; and init=/bin/bash replaces systemd with a shell as the last resort, with no services, no logging and no clean shutdown.

deploy@web01 · Ubuntu 26.04 LTS
$ sudo passwd -S root
root L 2026-09-18 0 99999 7 -1

Rescue and emergency mode ask for root's password through sulogin, and on Ubuntu 26.04, as on the Rocky cloud image used here, the root account is locked (the L). sulogin then prints "Cannot open access to console, the root account is locked." and no shell opens. Adding systemd.setenv=SYSTEMD_SULOGIN_FORCE=1 to the same boot makes it start the shell without a password, which is exactly why console access and the boot menu deserve protection (on RHEL, grub2-setpassword). The classic cause of a remote server stopping in emergency mode is a local /etc/fstab entry that cannot be mounted, such as a mistyped UUID: local-fs.target fails and systemd starts emergency.target. The essentials lesson "Disks, filesystems and mounts" shows the safe routine for editing /etc/fstab that prevents it.

Boot changes are tested with a console
Change the boot loader, the initramfs or /etc/fstab only when you can reach the console if the next boot fails, keep the previous kernel installed, and schedule the reboot while you are watching. Code that runs this early also runs before most monitoring, which is why the advanced security course (optional) treats boot-time persistence separately.

Try this

On Ubuntu, add a harmless parameter with a drop-in and prove where it lands: echo 'GRUB_CMDLINE_LINUX_DEFAULT="$GRUB_CMDLINE_LINUX_DEFAULT loglevel=4"' | sudo tee /etc/default/grub.d/99-sd-boot-lab.cfg, then sudo update-grub. Expect update-grub to list each file it sources, including yours, and each kernel it finds. sudo grep -m1 -E '^\s+linux\s' /boot/grub/grub.cfg now ends in loglevel=4, while cat /proc/cmdline does not, because the running kernel booted before the change. Undo it with sudo rm /etc/default/grub.d/99-sd-boot-lab.cfg and sudo update-grub, and check that the linux line is back to what it was. Do not reboot a machine you cannot reach at its console.

Takeaway

Name the stage before you fix anything: the firmware entry (efibootmgr, bootctl), the boot loader and its command line (grub.cfg or BLS entries against /proc/cmdline), the initramfs (lsinitrd, journalctl -k), or systemd (critical-chain); each leaves evidence you can read on a running machine.

Quick check
01On a RHEL 10 host, a colleague ran grubby --update-kernel=ALL --args=transparent_hugepage=madvise yesterday. grubby --info=DEFAULT shows the parameter, but the kernel still behaves as before. What is the most likely reason?
Incorrect — On RHEL, grubby writes the options into the BLS entries that GRUB reads directly; no regeneration step is needed.
Incorrect — Kernel command-line parameters and sysctl settings are separate mechanisms; systemd-sysctl does not undo boot parameters.
Incorrect — GRUB passes the command line to the kernel directly; the initramfs does not store it for the kernel.
Correct — The boot entry is updated, but the running kernel keeps the command line it booted with until the next boot.
02systemd-analyze time on a RHEL cloud instance reports 40 s of userspace startup, but multi-user.target was reached after 3.4 s and SSH logins work within seconds of boot. What explains the difference?
Correct — systemd reports startup as finished only when every boot job has completed, so one slow late job stretches the total.
Incorrect — GRUB does not report those times at all, so they are simply missing from the output; they are never added to another stage.
Incorrect — The initrd has its own figure in the same line; userspace starts after the switch to the real root.
Incorrect — Logins play no part in the calculation; the figure depends on systemd's own boot jobs.
03An Ubuntu 26.04 server stopped in emergency mode after an fstab edit. At the provider's serial console you see "Cannot open access to console, the root account is locked." What gets you a shell to fix fstab?
Incorrect — sulogin does not give up and open a shell; with a locked root it denies access every time.
Incorrect — emergency.target starts nothing but the shell, so there is no SSH listener to connect to.
Correct — It makes the sulogin shell start without a password for that boot only; fix fstab, then reboot normally.
Incorrect — rescue.target also starts its shell through sulogin, so the locked root account blocks it the same way.

Related