This shows you the differences between two versions of the page.
isc:labs:kernel:tasks:01 [2021/11/25 01:34] radu.mantu |
— (current) | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ==== 01. [??p] Prerequisites ==== | ||
- | === [??p] Task A - Dependencies installation === | ||
- | |||
- | <note important> | ||
- | TODO: Ubuntu | ||
- | </note> | ||
- | |||
- | === [??p] Task B - Development environment === | ||
- | |||
- | When developing new features for the kernel, chances are that you will screw up. Often. Depending on the severity, the kernel may or may not recover. So to avoid restarting your PC over and over, it's better to work in a minimal virtualized environment. As such, we will first bootstrap a loopback disk image with a basic Ubuntu system, but without the kernel. Eventually, we will boot a virtual macine with **qemu-system-x86_64** from this disk image, with a custom kernel that we will build ourselves. | ||
- | |||
- | <note> | ||
- | The bootstrapping and kernel building process may take a few (~15) minutes. Feel free to jump to Exercise 2 and come back once in a while to see if any progress was made. While Task A is doable on your live kernel, make sure to stop there. Task B is meant to generate errors and should be solved in the VM. For your sake :p | ||
- | </note> | ||
- | |||
- | === Bootstrapping === | ||
- | |||
- | For the bootstrapping process, we will use **debootstrap**. This tool will download a Debian-based ecosystem and install it in whatever directory we tell it to. Incidentally, that directory will be the mount point of the disk image that we are going to create. | ||
- | |||
- | <code bash> | ||
- | # create a 5GB empty file -- this will be our disk image | ||
- | [student@host]$ qemu-img create images/ubuntu.raw 5G | ||
- | |||
- | # build an ext4 filesystem onto the disk image -- now we can mount it | ||
- | [student@host]$ mkfs.ext4 images/ubuntu.raw | ||
- | |||
- | # mount the ext4 filesystem -- now we can copy files onto it | ||
- | [student@host]$ sudo mount images/ubuntu.raw /mnt | ||
- | |||
- | # bootstrat the Ubuntu system | ||
- | [student@host]$ sudo debootstrap --arch amd64 focal /mnt https://mirrors.kernel.org/ubuntu | ||
- | </code> | ||
- | |||
- | Almost there... if you list the contents of //%%/mnt/%%//, you will see most of the usual entries from your //root// directory. At this point, we should be able to boot into this machine (if we had a kernel image), but we wouldn't be able to log in. The only thing that's left for us to do is set a password for the //root// user. For this, we need to trick the **passwd** tool to thing that //%%/mnt/%%// is in fact the root of our filesystem. So we use **chroot**: | ||
- | |||
- | <code bash> | ||
- | # pretend that /mnt/ is our new / and start a bash instance inside | ||
- | [student@host]$ sudo chroot /mnt /bin/bash | ||
- | |||
- | # change password for current user (root) | ||
- | [ root@jail]$ passwd | ||
- | New password: root | ||
- | Retype new password: root | ||
- | passwd: password updated successfully | ||
- | |||
- | # exit from this bash instance and escape from the chroot jail | ||
- | [ root@jail]$ exit | ||
- | |||
- | # finally, unmount our disk -- we're done with it for now | ||
- | [student@host]$ sudo umount /mnt | ||
- | </code> | ||
- | |||
- | === Kernel building === | ||
- | |||
- | Next step is to get the kernel source code and compile it. By separating the kernel from the disk image, we are able to checkout to other branches / commits and test out different versions without installing them anywhere. Normally, you would have to select what options you want included in the compilation process (e.g.: memory allocators, cryptographic systems, etc.) by running ''make menuconfig''. After finishing your selection and saving the configuration, a //.config// file would be created. Because we haven't the faintest idea what most of the things enumerated in that menu even are, we will rely on default configurations. | ||
- | |||
- | <code bash> | ||
- | # clone the linux kernel locally | ||
- | [student@host]$ git clone --depth=1 https://github.com/torvalds/linux.git | ||
- | |||
- | # go into the repo directory | ||
- | [student@host]$ pushd linux | ||
- | |||
- | # create a default configuration file (.config) | ||
- | [student@host]$ make x86_64_defconfig kvm_guest.config | ||
- | |||
- | # optional: check out the generated .config file | ||
- | [student@host]$ less .config | ||
- | |||
- | # compile the kernel using all cores | ||
- | [student@host]$ make -j $(nproc) bzImage | ||
- | |||
- | # required for building out-of-tree modules -- see warning below | ||
- | [student@host]$ make modules_prepare | ||
- | |||
- | # return to the previous direcotry | ||
- | [student@host]$ popd | ||
- | </code> | ||
- | |||
- | <note important> | ||
- | In kernel 5.10.x, a new feature was introduced. This feature crashed many build environments and if you follow older kernel development tutorials, you may encounter such an error when building out-of-tree kernel modules (which we'll also do later on): | ||
- | |||
- | <code> | ||
- | make[3]: *** No rule to make target 'scripts/module.lds', needed by '...'. Stop. | ||
- | make[2]: *** [scripts/Makefile.modpost:140: __modpost] Error 2 | ||
- | make[1]: *** [Makefile:1761: modules] Error 2 | ||
- | </code> | ||
- | |||
- | The problem here is that a linker script is missing. [[https://wiki.osdev.org/Linker_Scripts|Linker scripts]] are used by **ld** when combining object files into the final executable. Without it, the linker doesn't know where to place each section in the output binary. If the user does not supply one, **ld** uses a built-in script. Normally, this default script does its job well. So much so that most programmers never learn of its existence. The kernel, however, is a different kind of beast and needs some fine tuning during compilation. Before 5.10.x, there was a //module-common.lds// script that was used for module compilation. After 5.10.x, someone considered it a good idea to replace it with //module.lds.S//, which requires a bit of processing before obtaining //module.lds//. Hence why we run ''$ make modules_prepare ''. | ||
- | |||
- | </note> | ||
- | |||
- | === Booting up the virtual machine === | ||
- | |||
- | We are finally here. Let's boot up the VM from our bootstrapped disk image, with our personally compiled Linux kernel. | ||
- | |||
- | <code bash> | ||
- | $ sudo qemu-system-x86_64 \ | ||
- | -m 4G \ | ||
- | -smp 1 \ | ||
- | -enable-kvm \ | ||
- | -kernel linux/arch/x86/boot/bzImage \ | ||
- | -drive file=images/ubuntu.raw,format=raw,index=0 \ | ||
- | -append 'root=/dev/sda rw console=ttyS0' \ | ||
- | -nographic | ||
- | </code> | ||
- | |||
- | Let us have a look at this command, line by line: | ||
- | - ''-m 4G'': allocate 4GB of memory (change this as you wish) | ||
- | - ''-smp 1'': use only 1 vCPU; this is recommended for debugging purposes | ||
- | - ''-enable-kvm'': [[https://www.redhat.com/en/topics/virtualization/what-is-KVM|KVM]] is a Linux kernel module that transforms your operating system intro a bare-metal hypervisor. This is what allows you to run __actual virtual machines__ on Linux. Without it, **qemu** would try to __emulate__ the system, resulting in worse performance. | ||
- | - ''-kernel .../bzImage'': this specifies the compiled & compressed kernel image to use when booting the virtual machine | ||
- | - ''-drive ...'' : specifies the disk image to load; note that ''intex=0'' will make the VM consider this to be //%%/dev/sda%%//. Adding another drive with ''index=1'' will cause it to be regarded as //%%/dev/sdb%%//. | ||
- | - ''-append ...'': these are command line arguments for the kernel (yes, even it has those). ''root=/dev/sda rw'' marks //%%/dev/sda%%// (i.e.: our //Ubuntu.raw// disk image) as the root device to be mounted onto the root directory (i.e.: //%%/%%//) in read-write mode. ''console=ttyS0'' exposes an [[https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter|UART]] serial interface to the VM and tells Linux to use it for I/O. | ||
- | - ''-nographic'': tells **qemu:** not to open a separate window for the GUI. In stead, it will take the virtual serial device (which the VM will recognize as //ttyS0//) and link it to the terminal. So whatever the VM sends via the serial to be printed will end out in your //stdout//. Whatever you type into //stdin// will be forwarded to the VM as input. | ||
- | |||
- | <note tip> | ||
- | If you have problems with the VM booting and you can't //<Ctrl-C>// out of it, try //<Ctrl+A X>// to signal **qemu** that is time to exit. Note that if you feel something odd happening with your terminal (e.g.: overlapping lines), you can run **reset**. | ||
- | |||
- | Under normal circumstances, exit the VM by running **poweroff**. | ||
- | </note> | ||
- | |||
- | After starting the VM and logging in as //root// (with the password set earlier), try finding out the kernel version in both the host and guest operating systems: | ||
- | |||
- | <code bash> | ||
- | # host has the latest Arch Linux kernel (you may have Ubuntu, etc.) | ||
- | [student@host]$ uname -r | ||
- | 5.15.2-arch1-1 | ||
- | |||
- | # guest has the newest Linux kernel release candidate | ||
- | [ root@guest]$ uname -r | ||
- | 5.16.0-rc2+ | ||
- | </code> |