In previous articles, we analyzed character device drivers together. If we have already written the driver code, what should we do next?
First, we need to run
make
, and upon successful compilation, a globalmem.ko file will be generated. Next, we need to insert this kernel module into the kernel and use themknod
command to create a device file node. After that, we can use a test program to test the driver we have written.All of the above tasks can be accomplished using the following Makefile. Simply input
make
in the shell terminal to compile this kernel module, usemake clean
to remove some intermediate files, andmake install
to insert the compiled kernel module into the kernel. More importantly, this Makefile is highly portable. This article will delve deeper into the syntax of Makefile and some shell programming by analyzing the Makefile provided below.
This Makefile (New Makefile) is much more powerful than the one here (Old Makefile).
1. Conditional Statements
First, note that this new Makefile has undergone significant changes in its logical structure, using conditional statements: ifneq-else-endif
. This conditional statement is used to determine whether the two variables before and after the comma in parentheses are not equal. The statements following ifneq
are executed if the condition is met, while those following else
are executed if the condition is not met. The ifneq ($(KERNELRELEASE),)
in the above Makefile checks whether the KERNELRELEASE variable is empty; if it is not empty, the condition is met.
Similarly, there are the following conditional statements, but with different types of condition checks.
ifeq-else-endif
: If the two variables are equal, the condition is met.
In the following two conditional statements, the condition checks are for variable names, which check whether this variable has been defined.
ifdef-else-endif
: If the variable is defined, the condition is met.
ifndef-else-endif
: If the variable is not defined, the condition is met.
However, the two conditional statements mentioned above do not have recursive definitions for the variables, as shown in the example below:
In this example, the final execution is right=yes. Although path is empty, cur_path=$(path) is considered to have defined the cur_path variable. As mentioned, the definition does not have recursive properties.
2. Variable Assignment
The format for defining a variable in a Makefile is: variable_name assignment_operator variable_value
The assignment operator typically has the following four types:
=
,:=
,?=
,+=
. The=
operator is similar to the equals sign we use daily, but here we need to clarify the concept of recursive variable expansion. To illustrate this concept more clearly, please see the example below:
Clearly, the result is yes. When executing make
, first is expanded to second, then second is expanded to third, and finally, the value of third which is yes is referenced. It can be seen that first is recursively expanded to obtain the final value of yes. This is what we refer to as recursive variable expansion.
In contrast to the above variable assignment operators, the :=
assignment operator immediately expands the variable. The same example, but this time we use the :=
assignment operator:
At this point, first is empty. This is because when defining the first variable, second was immediately expanded, and second was not defined at that time. Even if second is assigned a value later, first‘s value remains empty.
The other two assignment operators are relatively easy to understand. First, the
+=
assignment operator adds a new value to the existing value of the variable instead of overwriting the original variable value. The?=
assignment operator first checks whether the variable has already been assigned a value; only if the variable has not been assigned a value can it be assigned at this moment.OK, now that we understand the meaning of the assignment operators, let’s take another look at the Makefile above, and it will be much clearer.
3. Phony Targets
As mentioned above, simply entering
make
in the shell terminal executes the commands after the all target; this does not mean that the all target has any default effect. In a Makefile, the first target is always considered the final target. Therefore, it can be imagined that if you swap the positions of all and clean, executingmake
will automatically execute the commands after clean. Moreover, the first target does not necessarily have to be named all; you can use any target name you like (perhaps all is a silent convention).
Typically, there are no dependency files after a target like clean because our goal is to execute the commands following these targets. However, if there is a file named clean in the directory where the Makefile is located, then
make clean
will be interpreted as generating the clean target file. Since there are no dependency files after clean, each timemake clean
is executed, the clean target file will be considered up-to-date, and the commands below will not be executed. While this conforms to the syntax rules, it does not achieve our intended purpose of using clean.Therefore, we must define targets like clean as phony targets. The definition method is:
.PHONY: all
. This way, regardless of whether there is a file with the same name in that directory, the commands after clean will be executed. Now you should understand why there are so many targets in the Makefile starting with.PHONY
.4. Why Use Makefile
Kernel modularization is simple and practical, but compilation can become an issue: sometimes we only modify a small part of a file and have to compile the entire kernel, which is quite daunting. However, with the introduction of GNU make, this problem is easily solved: make will only compile the files that have been modified, rather than compiling all files. But how does make specifically compile the source files? At this point, the Makefile comes into play. In previous articles, we defined the Makefile as having “compilation rules”; below, we will experience this “compilation rule” by analyzing the above Makefile.
The entire Makefile divides different compilation rules (methods) based on the value of KERNELRELEASE; this KERNELRELEASE will only display the current kernel version number in the kernel source directory.
Generally, the directory where the source files of the kernel module we write is not located in the kernel source root directory (or its subdirectories). In this case, it does not meet the
ifeq
condition, and the compilation rules under theelse
statement will be executed. In this case, when we inputmake
, it will execute the commandmake -C $(KDIR) M=$(PWD) modules
. Note that the modules after this command indicates that all contents selected for module compilation in the configuration menu (i.e., those assigned to obj-m) will be compiled. Next, due to the-C $(KDIR)
parameter, make will switch to the kernel source root directory to execute. Based on the directory after M, it will compile the source code of the kernel module we wrote to generate .o files. Then, it combines some intermediate files to generate a .ko file. This is the process by which make generates the entire kernel target file. This process can be observed in the series of descriptive texts generated in the terminal after runningmake
.The above situation will compile the source code of the kernel module we wrote into the target file. Next, we are familiar with the insertion of the kernel module. However, when the source file of the kernel module we wrote is located in the kernel source directory, KERNELRELEASE will be non-empty (currently the version number), and at this point, the
ifeq
condition will be met. When will the source code of the kernel module we wrote be in the kernel source directory? What kind of kernel compilation is this?In previous articles, we assumed that we had already written the driver code, and in the Kconfig file, we wrote configuration options for this driver. After having relevant configuration options for this driver in the configuration menu, the user can choose whether to compile this driver source code into the kernel. At this point, the role of the Makefile is to compile this kernel module source code into the kernel (obj-m := $(TARGET).o). However, note that it only informs the kernel to “bring me along” in the next compilation; it does not actually compile.
Now, you should understand the logical structure of the entire Makefile!