Kernel Panic: Compiling Your Kernel From Scratch

If you tinker around with your linux enough, you'll realise you can always go deeper.... And bring something of your own to life! The only prerequisite needed is a pc, free time and a lot of "That does not make any sense"!?.
What, Why and How To Kernel?
It is the first program that loads after the Bootloader and has complete control over everything in the system. It is the bridge between the applications (software) and the actual hardware. It lives in a memory space known as Ring 0, while user applications live in Ring 3. You can think of Ring 0 as the God mode , the ability to do anything and everything while Ring 3 could be thought of as being a regular human with the ability to talk to gods periodically. This Ring system is actually very helpful, it acts a barrier. Since applications live in Ring 3 they cannot directly see or touch the memory or actions of other applications.
Let's assume we write a program which needs to open a file, what actually happens is our program dosen't actually touch the disk rather it sends a - Prayer ( a system call ) to the kernel. The kernel then checks the permission and only then does it perform the action.
I suppose this should more than enough suffice for what we will be following through with the rest of the writeup. Additionally i'd like to clarify that id be compiling the kernel in a VM . Since I am currently Using an ARM machine i'll be using Debian 5.10 on the Virutal Machine as a means to demonstrate the entire process.
If you're on windows You must use WSL [ Windows Subsystem For Linux ] , If you're on Linux well you are all set to go and incase if you are on MacOS (like me!) you will have to use UTM.
Where To Begin?
This is basically a 5 step process:
Cloning the Repository
Downloading all the essential packages
Copying current boot config
Going through all the options to choose the correct drivers and kernel modules
Booting using your new kernel and hoping that you did not mess up anywhere.
First and Foremost, we start by cloning the recent most kernel from git, using the following commands:-
git clone --depth=1 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git linux_stable
Now, to begin with why use the "--depth=1" , basically it means to download the recent most snapshot, just the latest commit, without any of the history behind it.
Then you go to the newly created directory by:
cd linux_stable
After that we fetch the recent most branch, for this demonstration that would be kernel version 6.19. Use this:
git fetch --depth=1 origin linux-6.19.y
git checkout linux-6.19.y
Note: You can choose to simply clone the entire repository but that would take up astronomical amount of time and space.
Additionally since we just need to compile the recent most kernel since it will have everything so there's no point in really cloning everything
Okay! After all this is where the real stuff begins. We start by first downloading some essential packages. I used Debian so I will be using "Advanced Package Tool" [ apt for short ], you'll have to use some other Package manager depending on you linux distribution.
sudo apt update
sudo apt install -y build-essential libncurses-dev bison flex libssl-dev libelf-dev bc dwarves
With the packages installed We move onto step 3 and that is copying the current boot config for our new kernel. You can copy the configuration for your current kernel from /proc/config.gz or /boot
You can find your current kernel version with:
uname -r
It'll output something like 6.1.0-21-arm64. Then copy that config:
cp /boot/config-$(uname -r) ~/linux_stable/.config
Okay now, we move to step 4. This is where we actually compile the kernel to compile the kernel there are two methods
- To use the command
make oldconfig
This allows you to choose all the drivers, modules, manually add or remove them OR
- To use the command
make localmodconfig
This option creates a configuration file based on the list of modules currently loaded on your system.
This time we will be going with the option make oldconfig, Just so that we get the freedom of deciding what we to install on the kernel. As soon as you are done executing the above commands we enter a CLI menu with a lot of options which are prompted to us one by one.
In here, you get the freedom of actually choosing how you wish to optimise your kernel if you wanna keep it lightweight and blazingly fast or if you just wanna see what each option does. You'll also come across a lot of options which might be preselected so it would be best to leave them as it is, And as for some of the important modules/options/configurations we'll go through them here.
1. Kernel Compression Mode
Basically when you compile a kernel, the resulting core binary (often named vmlinux) can be quite large. Because bootloaders often have strict size limitations, and because reading large files from a disk during startup takes time, the kernel is usually wrapped into a compressed image file (like vmlinuz or bzImage).
This compressed file is a self-extracting executable. When the bootloader hands control over to this file, a tiny, uncompressed stub of code runs first. This stub allocates memory, decompresses the rest of the kernel into RAM, and then executes it. If you want the minimum size zip you should opt for ZSTD or you can leave it as is
2. Timer Tick Handling
A timer tick is a hardware interrupt generated by a physical chip on your motherboard (like the Programmable Interval Timer or the Advanced Programmable Interrupt Controller). This chip is wired directly to the CPU and is configured by the kernel during boot to send a signal at a very specific, relentless frequency—often between 100 to 1000 times per second.
I will be explaining why we need it but how it takes place is another story.
Preemptive Multitasking: It allows the kernel to forcibly yank control away from a Ring 3 application that is occupying the CPU, allowing other programs a chance to run.
Timekeeping: It is how the OS updates the system uptime, the wall-clock time, and handles application sleep functions (e.g., waking up a program that called
sleep(5)).Accounting: It allows the kernel to measure exactly how much CPU time a specific process has consumed.
1. Periodic Timer Ticks : The kernel configures the hardware timer to fire at a fixed frequency (e.g., 250 or 1000 times a second) and it never stops. It ticks when your system is under heavy load, and it ticks when your system is completely idle. This is the legacy, old-school way of handling timers. It is extremely simple and reliable. It is terrible for battery life. Because the CPU is constantly being interrupted just so the kernel can say, "Yep, still nothing to do," the processor can never enter its deep sleep states (C-states).
2. Idle Dynticks [Dynamic Ticks] system : This introduces "Dynamic Ticks." When your CPU is actively running processes, the timer ticks normally. However, the moment the scheduling queue empties and the CPU goes idle, the kernel dynamically shuts off the timer tick. The tick only resumes when an external interrupt wakes the system up (like moving your mouse or receiving a network packet). This is the rational, modern default for almost all standard desktops, laptops, and servers. By silencing the timer when the system is idle, the CPU can drop into deep, low-power sleep states. It saves a massive amount of power and keeps your thermals low.
3. Full Dynticks system (tickless) : This takes dynamic ticks to the absolute extreme. It allows the kernel to turn off the timer tick even when the CPU is actively doing work, provided there is only one runnable process on that specific CPU core. It is highly specialized. It requires manual configuration of CPU isolation (telling the kernel to keep all other background tasks off specific cores) to actually work. If you select this on a normal desktop, it will likely just increase overhead without giving you any real benefit.
3. Init RAM File System (Initram FS)
To understand why this option exists, you have to understand a classic chicken-and-egg problem in operating system design.
When your computer boots, the core kernel image (vmlinuz) is loaded into memory. Its ultimate goal is to mount your actual hard drive (your root filesystem, usually formatted as ext4 or btrfs) and hand control over to the initialization system (like systemd) in Ring 3.
However, to read your physical hard drive, the kernel needs specific drivers (e.g., NVMe, SATA, RAID, or filesystem drivers). If you compiled those drivers as dynamic modules (<M>), they are stored inside your hard drive. The kernel cannot load the drivers because it cannot read the drive, and it cannot read the drive because it has not loaded the drivers.
Enabling this option ([*]) allows the kernel to understand and unpack an initramfs (Initial RAM Filesystem). An initramfs is a tiny, compressed, temporary filesystem that the bootloader (like GRUB) loads directly into RAM right alongside the kernel. It acts as a bridge.
4. Processor Specific Settings
Well this is where you'd have to use the internet to go through the options but in a nutshell enable the options which are your CPU specific [ If they are AMD then just enable the AMD options and vice versa for Intel ]
5. Multi-Core Scheduler Support
By default, the kernel sees all your CPU cores as a flat, equal list. Enabling this option makes the scheduler "topology-aware." It actively maps out your motherboard to understand exactly which cores share memory caches and which live on entirely separate physical chips.
Performance (Cache Locality): Shifting a process between two cores that share an L3 cache is virtually instant. Blindly throwing that process to a core on a completely different chip forces the CPU to slowly fetch the data all over again from the main RAM.
A smart scheduler can pack all your light background tasks onto a single physical chip, allowing the other chips on your motherboard to drop into a deep, power-saving sleep.
All in all if you are compiling for a modern multi-core system, absolutely keep this enabled
Okay, These 5 should suffice for now since i will not be able to cover all of them but these are some them which you should know about, by no means have I covered even all the important ones but the scope is once again to FAFO and I suggest you all do that :) . The next important part which comes up is to let the kernel compile which will take a solid 1 - 5 hours depending on your cpu and the number of cores you are using for this job. The command which is to be used is
make -j$(nproc)
The -j$(nproc) is the number of cores which the process will use to execute the command. Mind you compiling a kernel is a very taxing and time taking process so it suggested that you use as many cores as possible to speed up the process but you might end up with a steaming hot machine or some our of memory errors which can be fixed instantly. Next up is installing the Modules
Module Installation
During the compilation step, whenever the compiler saw an <M> (Module) in your configuration file, it did not bake that code into the main kernel. Instead, it compiled that specific driver into a stand-alone Kernel Object file (ending in .ko). Right now, these hundreds or thousands of .ko files are scattered all over your linux_stable source directory.
Once the compilation finally finishes, the resulting files are just sitting in the local download folder. They need to be moved to the right places. Before that the question comes up Why not just let them be there? As much as it would make our lives a tad bit easier it turns out the Linux kernel is incredibly rigid about where it looks for things.
When your system is booting, or when you plug in a new USB device, the kernel doesn't have the time or ability to recursively search through your random local folders for the right driver. It is hardcoded to look in one exact, standardized location: /lib/modules/[your-kernel-version]/. If the modules aren't sitting in that specific directory, the kernel assumes they don't exist, and your hardware simply will not work.
Execute the following command:
sudo make modules_install
Running the command triggers a script inside the kernel's Makefile that performs three vital actions:
Privilege Escalation: It requires
sudobecause/lib/modules/is a restricted, root-owned system directory. A standard Ring 3 terminal session cannot write files there without elevating privileges.Consolidation: It sweeps through your entire source tree, grabs every single
.kofile you just built, and neatly copies and organizes them into subdirectories inside/lib/modules/6.19.y/.Dependency Mapping: It automatically runs a background OS tool called
depmod. Some kernel modules rely on other modules to function.depmodbuilds a text-based roadmap (a dependency map) so that if the system needs to load "Module A", it knows it must automatically load "Module B" first.
Kernel Installation
In the previous step we moved the modules but this time we will be moving the brain itself. The compiler took millions of lines of C code and forged them into a single, highly compressed binary executable. Depending on the architecture you are compiling for such as the ARM64 architecture handling your virtual machine this file is typically named vmlinuz, Image, or Image.gz.
You cannot leave this core binary in your download folder because the bootloader (like GRUB) is relatively "dumb" at startup. It does not know how to navigate your entire filesystem to hunt for an operating system. It is hardcoded to look inside one specific, small partition: the /boot directory. If the kernel isn't there, the system cannot start.
When you execute sudo make install, a script automates four massive housekeeping tasks inside the /boot directory:
The Core Deployment: It takes your newly compiled kernel binary (e.g.,
vmlinuz-6.19.y) and safely copies it into/boot.The Blueprint (
System.map): It copies over a file calledSystem.map. This is the kernel's symbol table. If your custom kernel crashes (a Kernel Panic), it spits out raw hexadecimal memory addresses. TheSystem.maptranslates those raw numbers back into human-readable C function names so you can debug what went wrong.The Recipe (
.config): It takes the exact configuration file you spent time tweaking inmenuconfigand backs it up in/boot(usually namedconfig-6.19.y). This ensures that if you ever want to recompile this kernel months from now, you have the exact recipe saved.Forging the Bridge (
initramfs): This is the magic step. The installation script automatically detects your distribution and triggers itsinitramfsgenerator tool. It looks at the/lib/modules/6.19.y/folder you populated in the previous step, grabs the essential storage drivers, and packs them into that tiny, temporary RAM disk we discussed earlier. It places thisinitrd.img-6.19.yright next to your kernel in/boot, solving the chicken-and-egg problem before you even reboot.
Updating the Bootloader
GRUB (the GRand Unified Bootloader) is the Ring 3 equivalent for your motherboard. It is the very first piece of software that runs after your firmware (BIOS/UEFI) initializes the hardware. Its only job is to present a menu, load a kernel into memory, and get out of the way.
At the exact moment your computer turns on, there is no operating system running. Therefore, GRUB has extremely limited capabilities.
Instead, GRUB relies on a static, pre-written text file (usually located at /boot/grub/grub.cfg). If your new kernel is not explicitly written down in that exact text file, GRUB will never know it exists.
When you type sudo update-grub, you are actually running a Debian-specific shortcut. Under the hood, it executes a much more complex command: grub-mkconfig -o /boot/grub/grub.cfg. And a lot of boring stuff takes place internally which is once again not the scope of this guide
Once this script finishes generating the configuration file, your bridge is complete. You can safely restart the machine, watch the GRUB menu appear, and finally boot into the custom core you just built.
Rebooting into Your New Kernel
It is time to execute a simple sudo reboot. Right now, your system's Ring 0 is still occupied by the old kernel. You have to completely halt the current operating system, hand control back to your virtual machine's firmware, and let GRUB take over again.
As the VM powers back up, pay close attention to the GRUB boot menu. Because you ran update-grub in the previous step, your newly compiled kernel (e.g., Debian GNU/Linux, with Linux 6.19.y) should now be sitting proudly at the very top of the list, or nested inside the "Advanced options" menu. Select it and press Enter.
If your screen doesn't immediately freeze with a terrifying "Kernel Panic" (which, let's be honest, is a very real possibility when you first start tinkering, speaking from experience ;-; ), log in to your desktop or terminal and then for the final time just execute uname -r in your terminal
If the output spits back your exact custom version string—like 6.19.y congratulations. You have successfully brought your very own customized core to life.
And I guess that should be it for this time. Keep messing around.
Godspeed.
