Building a bootable Linux System from Scratch
Building a bootable Linux System from Scratch: A Simplified 5-Step Guide to an Initramfs Emergency Shell
When I moved my custom kernel (7.1.1-krsz) from QEMU to my physical laptop using just a single FAT32 EFI partition, I hit a major roadblock: a completely black screen. My kernel was booting "blind" because the display pipeline wasn't initialized early enough.
Here is the quick, no-nonsense guide on how I configured, built, tested, and successfully deployed my minimal RAM-only emergency shell on bare metal.
Step 1: Kernel Configuration & Configuration Anatomy
Inside make menuconfig, my strategy was to compile essential early display drivers as Built-in ([*]) so the screen lights up immediately, while keeping everything else as Modules (<M>).
To fix the black screen, the kernel needs to use the motherboard's UEFI graphics canvas before the heavy GPU drivers load. I welded these options directly into the kernel core:
Device Drivers --->
└─ Graphics support --->
├─ [*] Simple framebuffer support (CONFIG_X86_SYSFB=y)
├─ [*] Simple DRM driver (CONFIG_DRM_SIMPLEDRM=y)
├─ [*] Support for frame buffer devices --->
│ └─ [*] EFI Based Framebuffer Support (CONFIG_FB_EFI=y)
└─ [*] Console display driver support --->
└─ [*] Framebuffer Console support (CONFIG_FRAMEBUFFER_CONSOLE=y)
Step 2: Compilation & Installing Modules
Once configured, I compiled the kernel and exported the hardware modules to my host system's module directory so they would be available for packaging:
# Compile the kernel
make -j$(nproc)
# Install the modules to /usr/lib/modules/7.1.1-krsz/
sudo make modules_install
Step 3: Building the Initramfs with mkinitcpio
Because my storage drivers were compiled as modules, I needed mkinitcpio to dynamically load them during boot. I kept my MODULES array empty and relied on automated discovery hooks to sweep up the required files.
My /etc/mkinitcpio.conf:
MODULES=()
HOOKS=(base udev block filesystems)
I then generated the final initramfs image:
# Generate the initramfs toolset
sudo mkinitcpio -c /etc/mkinitcpio.conf -k 7.1.1-krsz -g ./initramfs-7.1.1-krsz.img
Step 4: Testing Inside QEMU via ttyS0
Before doing anything on physical hardware, I used QEMU to verify that my kernel and initramfs worked together. I directed the output to my host terminal using the virtual serial port (ttyS0):
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-initrd ./initramfs-7.1.1-krsz.img \
-append "console=ttyS0" \
-m 256M \
-nographic \
-no-reboot
The kernel rolled its logs inside my terminal and successfully stopped at the interactive sh emergency shell prompt. Stage one complete.
Step 5: Partitioning, Bootloader Installation, and Bare-Metal Launch
With validation complete, I moved to my physical target machine. I didn't want a full Linux distribution layout — just an isolated sandbox.
5.1 Partitioning and systemd-boot Setup
I booted into a live environment, used gdisk to wipe my NVMe drive, and created a single EFI System Partition (ESP) formatted to FAT32.
# Format the single EFI partition
sudo mkfs.vfat -F 32 /dev/nvme0n1p1
# Mount it and install systemd-boot
sudo mount /dev/nvme0n1p1 /boot
sudo bootctl --path=/boot install
I copied my custom binaries over to the ESP and created my boot entry configuration at /boot/loader/entries/emergency.conf:
sudo cp path/to/bzImage /boot/vmlinuz-7.1.1-krsz
sudo cp path/to/initramfs-7.1.1-krsz.img /boot/initramfs-7.1.1-krsz.img
# /boot/loader/entries/emergency.conf
title Linux Emergency Shell (Custom Kernel 7.1.1-krsz)
linux /vmlinuz-7.1.1-krsz
initrd /initramfs-7.1.1-krsz.img
options rw console=tty0
Note: I purposefully omitted the
root=parameter. Since there is no root partition, this forces the system to drop straight into the initramfs emergency shell.
5.2 The Bare-Metal Timeline
I rebooted my physical laptop and selected my custom entry from the menu:
- 0.0s – 0.5s: The kernel boots.
efifbimmediately captures the UEFI screen canvas, and the text console (tty0) instantly lights up the screen with scrolling logs. - 0.5s – 1.5s: The
blockandfilesystemshooks scan the PCI bus, automatically loadingnvme.koand setting up the hardware interface. - 1.5s – 2.0s: My built-in
amdgpudriver wakes up, takes over from the temporary EFI canvas via a smooth handover, and sharpens the display resolution. - Conclusion: The initramfs realizes there is no
root=target partition to mount. It safely drops execution control, presenting me with a fully operational, root-privileged interactive shell right on my laptop's physical display.
Key Takeaways
Keeping MODULES empty while welding efifb into the core kernel gives you an incredibly lightweight initramfs footprint with zero-frame visual feedback. Turn on your framebuffers, trust your hooks, and enjoy your custom shell!