Naked Linux – running a single kernel
So, Linux is not an operating system, but only a kernel for it. Everything else comes from the GNU Project (and others). And I wonder what the kernel itself is good for?
This article is very “beginner” level. Let's do a little experiment – create a clean virtual machine and try to run the Linux kernel “without everything.” Or almost “without”, because we will need an OS bootloader – and some kind of “user program” (we will create it ourselves). Of course, advanced Linux users can conduct such an “experiment” by simply editing the startup parameters – but our story is still for those who are almost (or completely) out of the topic 🙂
As a bonus, we’ll touch a little on system calls and say a few words about other kernels.
What is a kernel – and how to use it
At this stage, it is enough for us to imagine that the kernel is some kind of program in the form of a healthy file. Speaking in programmer terms – a framework for running our programs – and at the same time a large library of system calls and a collection of drivers.
In other words, the kernel embodies work with all the main entities of the OS (in particular, processes – in which other programs will be launched) – and also contains drivers for working with all kinds of computer hardware. Well, not all possible drivers in the world – but for the most current and popular systems. For disks, screen, network interfaces.
Therefore, our goal is to launch the kernel and transfer control to a small demo program that can successfully “pull” these system calls. Along the way we will look at some additional things – although not relevant to our experiment – but which are useful to have an idea about.
Create a virtual machine
In general, you can practice on a physical machine, but we recommend starting with a virtual machine. Download VirtualBox (or another emulator, if you have a favorite one), create a new virtual machine (you can give it a gigabyte of RAM, or less – and an automatic size disk – we will need quite a bit).
Here and further, although we talk in detail about the steps that should be performed, we still avoid overly detailed instructions “press such and such a button.” Let such little things remain just as an “exercise” 🙂 In particular, the VirtualBox interface is quite intuitive, and it’s easy to Google questions about it.
So, the machine has been created – if you try to start it, a black screen will appear with a message that a device from which you can boot has not been found. This is understandable – after all, the disk is still pristine. You need to write a bootloader onto it. Obviously, for any operations you need to temporarily launch from some LiveCD. Again, you can choose as you wish, but I recommend (for this purpose) downloading the relatively small SystemRescueCD image. Download the image and connect it as a CD in the settings of the created virtual machine. Restart the machine – after booting, the system boot menu on the LiveCD should appear:
Now we will launch the default menu item (just press Enter) – but in the future, pay attention to the “Boot existing OS” item – it cancels booting from LiveCD and loads what you have installed on the main disk – we will use this to more conveniently check what happened .
Preparing the hard drive
So click “Boot SystemRescueCD using default options”. Some activity will begin which sooner or later will end with the inscription “automatic login” and below with a command line prompt in the spirit [root@systemresccd ~]#
– in principle, you can launch the graphical shell (startx), but we don’t need it now.
Our hard drive is visible among devices, try entering ls /dev/sd*
and you should probably detect the disk /dev/sda
– it is not yet divided into sections (like /dev/sda1
) – and this is what we will do now.
Run the utility fdisk
specifying the detected disk as a parameter, that is fdisk /dev/sda
– it has a simple interface, single-letter commands – and immediately reminds you that you can click for a list of commands m
(for some reason).
From all this diversity we need a little:
create a new partition table (make DOS partition table) by pressing “o”
create a new partition (primary) press “p” and allocate the entire disk for it
make this partition bootable (toggle bootable flag) by pressing “a”
you can check the resulting table by pressing “p”
and finally write all changes (and exit) by pressing “w”
Now the team ls /dev/sd*
will inform you that you also have a section /dev/sda1
– We will actively use it in the future.
In particular, you need to create a file system on it – this is a simple commandmkfs.ext4 /dev/sda1
Now everything is ready to record the bootloader. Well, or almost everything (but more on that later).
It was possible to create a GPT partition table (not related to the now fashionable AI) – but for our purposes this is not important, and a more archaic MBR will simplify the experiment a little.
“extlinux” bootloader
A popular boot loader in Linux is grub2
. However, it is quite large and complex in terms of configuration, so it is useful to look at alternatives as part of the experiment:
also popular
systemd-boot
– but in my opinion it requires EFI, which creates additional unnecessary steps in our experimentsyslinux
/extlinux
– but we’ll take them into action, especially since they are used for the SystemRescueCD itself
We use extlinux
– this is the version syslinux
for ext4fs, a Linux file system. If you try to run it from the command line, it will display a prompt that tells you, among other things, that the installation disk must first be mounted.
We have an empty directory /mnt
– let's connect it there:
mount /dev/sda1 /mnt
This is optional, but to stay consistent with the popular file layout, let's create a folder /boot
at the root:
mkdir /mnt/boot
Now everything is ready to record the bootloader, use the command that it itself prompts:
extlinux --install /mnt/boot
It will place a couple of files in the specified directory and in addition (thanks to the –install switch) write the boot code to the beginning of the partition. Unfortunately, this is not yet enough to launch; now we will make sure.
For curiosity, you can see what is now in the root folder of our disk and in the /boot folder – using the commands ls /mnt
And ls /mnt/boot
accordingly – when you’ve had enough of admiring it, let’s unmount the disk (to be sure that everything was recorded) with the command umount /mnt
then do the following:
in the virtual machine settings (Devices menu), remove the virtual CD
in the Machine menu click Reset
The machine will reboot and again complain that you do not have a boot device!
This is because there is no boot code in the very first sector of the partition table (MBR – master boot record). Insert the virtual disk back, reboot the machine again into SystemRescue and let's fix this problem.
MBR recording – a small touch
Actually this is optional. You can get around the problem by starting with SystemRescueCD and selecting “Boot existing OS” – in this case, the boot baton goes directly to the desired disk partition, bypassing the MBR. But still, we’ll spend a couple of minutes to do it well right away.
Somewhere in the depths of the file system lies a file mbr.bin
– it contains exactly the code that needs to be written down. Find him with the command
find / -name mbr.bin
I found it in /usr/lib/syslinux/bios/mbr.bin
– write it to disk with the cat command:
cat /usr/lib/syslinux/bios/mbr.bin > /dev/sda
(just sda and not sda1 – because it is MBR). Now if you repeat the reboot experiment, you should see that extlinux
launched – it will say that it did not find the configuration file and will show a prompt boot:
– in principle, here you can manually specify the kernel and boot parameters. But we don't have a core yet.
Let's add a core, or better yet two
So, reboot the machine again in SystemRescue – in the future, do not “eject” the virtual CD, and when you want to try booting from the hard drive, just use the “Boot Existing OS” item from the boot menu.
Mount the hard drive as before and go to the directory /mnt/boot
– let's drag the core here!
Where can I get it? it’s not hard to guess that at least one must be somewhere in the depths of SystemRescueCD itself – let’s try to find it (it usually has a name starting with vmlinuz
:
find / -name vmlinuz*
For example, I found it somewhere in the depths /usr/lib
– file size is about 5 megabytes. Let's copy it (while in /mnt/boot):
cp /usr/lib/.../vmlinuz vmlinuz1
As you can see, we gave it a name with the suffix 1
– this will not interfere with loading, and we will be able to distinguish between kernels. Since we're going to try different ones.
The fact is that Linux (and other *nix systems) make it easy to replace kernels by selecting the one you need when booting. I took the second kernel from the main OS on my laptop (Ubuntu 18.04 it seems). You may not have this option at hand, but you can probably find different kernels on the Internet. I uploaded mine to GitHub – so you can use the direct link:
wget https://github.com/RodionGork/bare-linux-experiment/raw/refs/heads/main/vmlinuz64
Naturally, do this in the same /mnt/boot folder so that the kernels are located next to the boot files (this is not necessary, but convenient). If you find that the virtual machine cannot reach the network, check the network settings (in it) – for Internet requests, the easiest way is to select NAT.
Attention: using “ready-made” kernels dragged from nowhere is a bad idea! In a good way, you need to take the sources and thoughtfully compile the kernel of the required version and with the required settings. We're skipping this step just to simplify the experiment!
One way or another, I hope you and the team managed to stock up on kernels ls /mnt/boot
shows the presence of files vmlinuz1
And vmlinuz64
– Let's try to download them!
Don't forget umount
now restart the machine and select “Boot existing OS”.
At the bootloader prompt which looks like boot:
write /boot/vmlinuz1
for example – the full path to the kernel we downloaded. Press Enter.
After a couple of seconds of active activity, a message with “kernel panic” and “Unable to mount root fs…” will appear on the screen.
Great, the kernel is loading, but for some reason it wants some kind of “VFS”?
Preparing initrd – virtual file system
The situation is that modern *nix kernels assume such a boot order that immediately after launch it looks for a small image with a file system that can be deployed directly in the RAM. And then connect the remaining file systems (on disk, etc.) after performing various additional initializations.
An image with this file system is passed as a kernel parameter initrd=...
(from the words “init root directory” or something)
We will prepare such an image for him, consisting of just one file – our user program! Here we will use one more trick – the first program that the kernel tries to run is init
– some of the most important OS process. So we will call the executable file of our application exactly that – and place it in the root.
Next, we will try to write and compile a couple of simple programs – if you don’t have anything at hand to compile them on, it doesn’t matter – you can download ready-made initrd images in the same repository where the kernels are located.
Let's write a simple program in C – it simply enters lines from the user and prints their length:
#include <stdio.h>
#include <string.h>
int main() {
printf("I'm mini-shell, type in your commands:\n");
while (1) {
char ur[1024];
fgets(ur, sizeof(ur), stdin);
if (ur[0] < ' ') break;
printf("%ld - not supported\n", strlen(ur));
}
return 0;
}
The program is presented as a “mini-shell” although in reality, of course, it is not a shell at all – if you want to add any useful commands here, you will have to implement them. For now, let’s forgive ourselves this little deception and build the program, specifying the static compilation key (since we won’t have any dynamic libraries at hand). This program only uses standard C library functions (which will be added to the executable code) and kernel system calls for input and output – so everything should be fine. Name the file init.c
gcc --static -o init init.c
Now you need to upload the compiled file init
to virtual. In it, firstly, reboot again into SystemRescueCD, mount the disk and go to /mnt/boot
– and secondly, switch the network settings to “host network only” (before this you need to create a new host-only adapter in the settings of VirtualBox itself). After this, you can either connect from the virtual machine to the parent machine using sftp
and download the file, or run some web server on the parent machine (at least python3 -m http.server
) and extract the file from the virtual machine with wget. In both cases, the address of the parent machine will be something like 192.168.56.1 (check ifconfig).
When you managed to get your hands on the compiled file init
in the virtual machine, make sure that it has an executable flag (or just put it to be sure chmod u+x init
) – now we need street magic that collects the image using the cpio command:
echo init | cpio -o --format=newc > initrd-c
the result will be a file initrd-c
which we are going to use when starting. Let's unmount the disk, reboot into the “existing OS” and try to load the kernel by specifying the required initrd file:
boot: /boot/vmlinuz1 initrd=/boot/initrd-c
With a high degree of probability, this attempt will fail – you will see a similar screen with a download log, but stating that it was not possible to start init
. A little higher in the log it will be possible to find a specific error, for example (error -8)
something like this:
If you see error -13
– this means “permission denied” – you forgot to make the file executable. But error -8
about another “wrong executable file format” – the file is compiled for a 64-bit system and the kernel runs a 32-bit one.
Naturally, this depends on what system and how the file was compiled.
There are two ways to correct the situation – either try to specify a different kernel at startup (the one that is vmlinuz64) – or take another initrd file – for example, in the repository above there is initrd-asm
– in it init
assembled by a small program in assembler (its code is also there somewhere for the curious – a kind of “hello-world”). If you want to build it yourself, use the commands
as --32 init.c && ld -m elf_i386 -o init a.out
Then drag it into the virtual machine and pack it as before (for convenience, I suggest naming the file differently – for example, initrd32 or initrd-asm).
As for this assembler program, it is just a development of the example from the previous one articles about 5 assemblers – if you are curious to go deeper, I will definitely write a separate article with an analysis of this program, after which you will be able to write similar ones yourself – for entertainment or for educational purposes!
I think you'll be lucky after a few tries 🙂 You will either see a message that the “mini-shell” is ready for your experiments – try entering lines and when you get tired press Ctrl-C – to find out what happens if init
– the process in Linux ends. Or you will see a message about “nedo-bash”.
Configuration for extlinux boot loader
You can add a configuration file next to the bootloader files so that by default some selected kernel is loaded with some selected initrd (and other options if necessary). To do this, mount the disk in SystemRescueCD (once again) and using vi
or nano
create a file /mnt/boot/extlinux.conf
with approximately the following content:
prompt 1
timeout 100
default testlinux
label testlinux
kernel vmlinuz64
append initrd=initrd-c
The first line means that you need to show the invitation boot:
so that the user can enter alternative download parameters, the second sets a timeout (in tenths of a second) after which the download will continue, marked with the name indicated in the third line.
Conclusion
I hope you managed to complete this “exercise” to the end. As you understand, this is just a starting point for further experiments. There remain a variety of interesting questions that can be explored.
For example, you can drag initrd
file from Ubuntu (it weighs about 50MB) – and try to connect it. You will get a more or less working system – but make sure that you need to create some folders on the disk (like /dev
) and mount it as a root system.
Or you might wonder why our “nedo-bash”, although it displays a message on the screen, only performs input when running with one of the two cores – as if the other core does not have a keyboard enabled. However, here it is better to return to the advice mentioned above – you should not use incomprehensible, unfamiliar kernels. Try to collect your own.
A separate direction could be experimenting with other nuclei – take a kernel from Gnu Hurd – or from FreeBSD. However, when compiling programs for them, you need to keep in mind that they have a slightly different system call format compared to Linux (so simply compiling a program on a Linux machine, you may not get what you need).
In general, this is a separate interesting topic – the numbers of system functions in *nix systems are the same, including Linux – but firstly, in 64-bit Linux they were suddenly shuffled, and secondly, Linux makes calls by passing parameters in registers (a la DOS) – while the rest push them “in a cinch” through the stack. In general, a separate article is also needed!
Perhaps I will try to cover some of these questions myself in the near future, they are quite interesting!