This shows you the differences between two versions of the page.
— |
ass:labs-2024:04:tasks:02 [2025/08/03 10:12] (current) florin.stancu created |
||
---|---|---|---|
Line 1: | Line 1: | ||
+ | ==== 02. Your first kernel module ==== | ||
+ | |||
+ | Way back when, kernels used to be monolithic, meaning that adding new functionality required recompiling and installing it, followed by a reboot. Today, things are much easier. By using the **kmod** daemon (''man 8 kmod''), users are allowed to load and unload __modules__ (i.e.: kernel object files) on demand, without all the fuss. These modules are C programs that must implement initialization and removal functions that are called automatically. Usually, these functions register / unregister other functions contained in your object with core kernel systems. | ||
+ | |||
+ | We can use **lsmod** to get a list of all present modules, and **modinfo** to obtain detailed information about a specific module. | ||
+ | |||
+ | <code bash> | ||
+ | [student@host ~]$ lsmod | ||
+ | ecdh_generic 16384 1 bluetooth | ||
+ | |||
+ | [student@host ~]$ modinfo ecdh_generic | grep description | ||
+ | description: ECDH generic algorithm | ||
+ | |||
+ | [student@host ~]$ modinfo bluetooth | grep description | ||
+ | description: Bluetooth Core ver 2.22 | ||
+ | </code> | ||
+ | |||
+ | What we can understand from this is that the [[https://elixir.bootlin.com/linux/latest/source/crypto/ecdh.c|Elliptic Curve Diffie-Hellman]] module is 16384 bytes in size and is used by one other module, via the [[https://elixir.bootlin.com/linux/latest/source/net/bluetooth/ecdh_helper.c|bluetooth ECDH helper]]. As you probably noticed, [[https://elixir.bootlin.com/linux/latest/source|elixir.bootlin.com]] is a critical resource in navigating the kernel code. | ||
+ | |||
+ | If it's not a module that you're unsure about but a device, you can use **udevadvm** get more information about it. For example, if you have a NVMe drive (an SSD, let's say) and you want to figure out what drivers are involved in its operation, you can tell **udevadm** to scan //sysfs// bottom-up, starting with that device: | ||
+ | |||
+ | <code bash> | ||
+ | [student@host ~]$ udevadm info -a /dev/nvme0n1 | grep DRIVER | ||
+ | DRIVERS=="nvme" | ||
+ | DRIVERS=="pcieport" | ||
+ | </code> | ||
+ | |||
+ | From this, we glean that we need both the NVMe driver and the PCIe driver in order to operate our SSD. | ||
+ | |||
+ | === Task A - Prepare your build system === | ||
+ | |||
+ | Take a look at this piece of [[https://www.kernel.org/doc/html/latest/kbuild/modules.html|documentation]] before you get started. | ||
+ | Then, create a new directory with the following structure: | ||
+ | |||
+ | <code> | ||
+ | . | ||
+ | ├── Kbuild --> defines the output module via obj-m | ||
+ | ├── Makefile --> defines the build targets, relying on the Linux headers | ||
+ | └── my_first_module.c | ||
+ | </code> | ||
+ | |||
+ | The makefile should look something like this: | ||
+ | |||
+ | <code make> | ||
+ | KDIR ?= /lib/modules/`uname -r`/build | ||
+ | |||
+ | build: | ||
+ | $(MAKE) -C $(KDIR) M=$(PWD) modules | ||
+ | |||
+ | clean: | ||
+ | $(MAKE) -C $(KDIR) M=$(PWD) clean | ||
+ | </code> | ||
+ | |||
+ | <note warning> | ||
+ | Notice how ''KDIR'' is used to determine the precise kernel that we are compiling for. If invoked without overwriting ''KDIR'', its default value ensures that the module is compiled for our current system (given that the kernel headers are installed). Note, however, that in order to compile the module for our board, it's not sufficient to point to the correct repo path. You still have to pass the ''CROSS_COMPILE'' and ''ARCH'' variables. Otherwise, the kernel's ''.config'' will be reset. | ||
+ | |||
+ | **TLDR:** modify ''KDIR'' + pass the appropriate variables when cross-compiling the Linux kernel! | ||
+ | </note> | ||
+ | |||
+ | === Task B - Write a minimal module === | ||
+ | |||
+ | <code c> | ||
+ | #include <linux/kernel.h> | ||
+ | #include <linux/init.h> | ||
+ | #include <linux/module.h> | ||
+ | |||
+ | MODULE_DESCRIPTION("A test module."); | ||
+ | MODULE_AUTHOR("Student"); | ||
+ | MODULE_LICENSE("GPL"); | ||
+ | |||
+ | /* custom log message header; used by pr_* */ | ||
+ | #ifdef pr_fmt | ||
+ | #undef pr_fmt | ||
+ | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt | ||
+ | #endif | ||
+ | |||
+ | /* init - module initialization callback | ||
+ | * @return : 0 if everything went well ==> module is loaded | ||
+ | * -1 if an error ocurred ==> module is not loaded | ||
+ | */ | ||
+ | static int init(void) | ||
+ | { | ||
+ | pr_info("Hello world!\n"); | ||
+ | |||
+ | return 0; | ||
+ | } | ||
+ | |||
+ | /* fini - module removal callback | ||
+ | */ | ||
+ | static void fini(void) | ||
+ | { | ||
+ | pr_info("Goodbye cruel, cruel world!\n"); | ||
+ | } | ||
+ | |||
+ | /* register on_init and on_exit event handlers */ | ||
+ | module_init(init); | ||
+ | module_exit(fini); | ||
+ | </code> | ||
+ | |||
+ | A few things to mention about this code: | ||
+ | * The ''module_init'' and ''module_exit'' macros mark the module initialization and cleanup functions. In other modules you may find the ''%%__%%init'' attribute thrown around. That is just an alias for ''__atribute__((section(".init.text")))'', placing the initialization function in a special section. All function located in these section are automatically marked as safe to be deleted after first being executed, in order to reclaim some memory. | ||
+ | * ''pr_fmt'' is yet another macro that is used by the kernel print function, ''printk''. ''pr_fmt'' allows us to prepend unique module identifiers before each logged line, in order to keep track of which module generated what output. | ||
+ | * Although we mentioned ''printk'' as the main print function, it's recommended to use the [[https://www.kernel.org/doc/html/latest/core-api/printk-basics.html|pr_${LOG_LEVEL}]] alternatives. In this case, ''pr_info'' is a rather mild message that might not even be considered important enough to print to your console. Instead, all debug messages no matter their importance can be viewed when running **dmesg** (or printing ''/proc/kmsg'' for raw output). | ||
+ | * ''MODULE_LICENSE("GPL");'' this line is pretty much required in order for your module to interact with the larger kernel. With this macro, you save the //"GPL"// string in a special section, indicating that you comply with the GNU Public License that the kernel uses. Not doing so can lead to restricted API access or even the module not being accepted by the kernel. | ||
+ | |||
+ | === Task C - Insert the module into the kernel === | ||
+ | |||
+ | After you compile the kernel for your machine, insert it into the kernel, then remove it. You can do this via **insmod** and **rmmod**. Check the kernel debug log using **dmesg**. | ||
+ | |||
+ | Once you're convinced that the module works, clean the workspace then rebuild it for the board. Pass the kernel object (i.e.: ''*.ko'' file) via SSH, then repeat the process remotely. | ||
+ | |||
+ | <note tip> | ||
+ | If you want to investigate the kernel log of a previous session, you can use **journalctl**: | ||
+ | |||
+ | <code bash> | ||
+ | # show all log info from previous boot | ||
+ | [student@host ~]$ journalctl -b -1 -a | ||
+ | </code> | ||
+ | |||
+ | This can come in handy when you want to investigate crashes. | ||
+ | </note> | ||