Understanding the basics of a kernel module Makefile
You may have noticed by now that we tend to follow a one-kernel-module-per-directory rule of sorts. Yes, that definitely helps keep things organized. So, let’s take our second kernel module, the ch4/printk_loglvl
one. To build it, we just cd
to its folder, type make
, and (fingers crossed!) voilà, it’s done. We have the printk_loglevel.ko
kernel module object freshly generated (which we can then apply insmod/rmmod
to). But how exactly did it get built when we typed make?
Explaining this is the purpose of this section.
First off, we do expect you to understand the basics regarding make
and the Makefile. If not, don’t fret, we’ve provided links to check this out, within the Further reading section (in the paragraph labeled Makefiles: introductory stuff) of this chapter. . Check it out! (Link: https://github.com/PacktPublishing/Linux-Kernel-Programming_2E/blob/main/Further_Reading.md#chapter-4-writing-your-first-kernel-module-lkms-part-1---further-reading).
Next, as this is our very first chapter that deals with the LKM framework and its corresponding Makefile, we will keep things nice and simple, especially regarding the Makefile. However, early on in Chapter 5, Writing Your First Kernel Module – Part 2, in the A better Makefile template for your kernel modules section, we shall introduce a more sophisticated, and (what we hope is) a better Makefile, that is still quite simple to understand. We shall then use this better Makefile in all subsequent code (but not right now); do look out for it and use it!
As you will know, the make
command will by default look for a file named Makefile
in the current directory; if it exists, it will parse it and execute command sequences as specified within it. Here’s our simple Makefile for the printk_loglevel
kernel module project (I
use the nl
– “number lines” – utility to show it with line numbers prefixed):
$ nl Makefile
1 # ch4/printk_loglvl/Makefile
2 PWD := $(shell pwd)
3 KDIR := /lib/modules/$(shell uname -r)/build/
4 obj-m += printk_loglvl.o
5 # Enable the pr_debug() and pr_devel() as well by removing the comment from
6 # one of the lines below
7 # (Note: EXTRA_CFLAGS deprecated; use ccflags-y)
8 #ccflags-y += -DDEBUG
9 #CFLAGS_printk_loglvl.o := -DDEBUG
10 all:
11 make -C $(KDIR) M=$(PWD) modules
12 install:
13 make -C $(KDIR) M=$(PWD) modules_install
14 clean:
15 make -C $(KDIR) M=$(PWD) clean
It should go without saying that the Unix Makefile syntax demands this basic format:
target: [dependent-source-file(s)]
rule(s)
The rule(s)
instances are always prefixed with a [Tab]
character, not white space.
Let’s gather the basics regarding how this (module) Makefile works. First off, a key point is this: the kernel’s Kbuild
system (which we’ve been mentioning and using since Chapter 2, Building the 6.x Linux Kernel from Source – Part 1), primarily uses two variable strings of software to build, chained up within the two variables obj-y
and obj-m
.
The obj-y
string has the concatenated list of all objects to build and merge into the final kernel image files – the uncompressed vmlinux
and the compressed (boot-able) [b][z]Image
images. Think about it – it makes sense: the y
in obj-y
stands for Yes. All kernel built-in and Kconfig
options that were set to Y
during the kernel configuration process (or are Y
by default) are chained together via this item, built, and ultimately woven into the final kernel image files by the Kbuild
build system.
On the other hand, it’s now easy to see that the obj-m
string is a concatenated list of all kernel objects to build separately, as kernel modules! This is precisely why our Makefile has this all-important line (line 4):
obj-m += printk_loglvl.o
In effect, it tells the Kbuild
system to include our code; more correctly, it tells it to implicitly compile the printk_loglvl.c
source code into the printk_loglvl.o
binary object, and then add this object to the obj-m
list. Next, with the default rule for make
being the all
rule (lines 10 and 11), it is processed:
all:
make -C $(KDIR) M=$(PWD) modules
The processing of this single statement (line 11) is quite involved; here’s what transpires:
- The
-C
option switch tomake
has themake
process change directory (via thechdir()
system call) to the directory name that follows-C
. Thus, it changes the directory to the$(KDIR)
directory, which is set (in line 3) to the kernelbuild
symbolic link under/lib/modules/$(uname -r)
(which, as we covered earlier, points to the location of the limited kernel source tree that got installed via thekernel-headers
package). To remind you, here it is (on my Ubuntu guest):$ ls -l /lib/modules/$(uname -r)/build lrwxrwxrwx 1 root root 31 May 5 10:51 build -> /home/c2kp/kernels/linux-6.1.25/
- So, clearly, the
make
process changes directory to the folder~/kernels/linux-6.1.25/
, which, in this case, points back to our original 6.1.25 kernel source tree (as we’re running off the custom kernel that we built earlier). Once there, it automatically parses in the content of the kernel’s top-level Makefile – that is, the Makefile that resides there, in the root of this kernel source tree. This is a key point. The kernel top-level Makefile is a rather large and sophisticated one (on 6.1.25, it’s well over 2,000 lines) and contains key build details and variables. This way, being parsed in every time even an out-of-tree module is being built, it’s guaranteed that all kernel modules are tightly coupled to the kernel that they are being built against (more on this a bit later). This also guarantees that kernel modules are built with the exact same set of rules – that is, the compiler/linker configurations (the*CFLAGS*
options, the compiler option switches, and so on) – as the kernel image itself is. This is required for binary compatibility. - Next, in the Makefile, still on line 11, you can see the initialization of the variable named
M
(to the present working directory), and that the target specified ismodules
; hence, themake
process now changes the directory to that specified by theM
variable, to$(PWD)
– the very folder we started from (line 2:PWD := $(shell pwd)
in the Makefile initializes it to the correct value)!
So, interestingly, it’s a recursive build: the build process, having – very importantly – parsed the kernel top-level Makefile, now switches back to the kernel module’s directory and builds the module(s) therein.
Lines 12 and 13 make up the install
target (which we cover in the next chapter), and lines 14 and 15 make up the clean
target.
Did you notice that when a kernel module is built, a fair number of intermediate working files are generated as well? Among them are modules.order, <file>.mod.c, <file>.o, Module.symvers, <file>.mod.o, .<file>.o.cmd, .<file>.ko.cmd
, a folder called .tmp_versions/
, and, of course, the kernel module binary object itself, <file>.ko
– the whole point of the build exercise. Further, there are several hidden files generated as well. Getting rid of all these temporary build artifacts, including the target (the kernel module object itself) is easy: just perform make clean
. The clean
rule cleans it all up. We shall delve into the install
target in the following chapter.
A screenshot helps convey the output seen on building and then cleaning up:
Figure 4.13: Screenshot showing the build (make) and “make clean” of our printk_loglvl kernel module (on our x86_64 Ubuntu 22.04 guest running our custom-built 6.1.25 LTS kernel)
You can look up what the modules.order
and modules.builtin
files (and other files) are meant for within the kernel documentation here: https://elixir.bootlin.com/linux/v6.1.25/source/Documentation/kbuild/kbuild.rst. Also, as mentioned previously, we shall, in the following chapter, introduce and use a more sophisticated Makefile variant – a “better” Makefile. It is designed to help you, the kernel module/driver developer, improve code quality by running targets related to kernel coding style checks, static analysis, simple packaging, and (a dummy target) for dynamic analysis.
With that, we conclude this chapter. Well done – you are now well on your way to learning Linux kernel development!