Recently, I have been learning about the configuration, compilation, and Makefile for the Linux kernel. Today, I would like to summarize my learning results and share them with everyone.
1. Unpacking and Patching
First, you need to unpack the Linux kernel you obtained. Here I am using version linux.2.22.6. In the Linux command line, you can unpack the kernel using tar xjf linux.2.22.6.tar.bz2
. If you need to apply a patch to this kernel, use the patch command: patch -px <../linux.2.22.6.patch
. Here, px
indicates to ignore the first x directories described in the patch file.
— linux-2.6.22.6/arch/arm/configs/s3c2410_defconfig+++ linux-2.6.22.6_jz2440/arch/arm/configs/s3c2410_defconfig
If you are currently in the root directory of the kernel, which is linux-2.6.22.6
, then the patch command should ignore one directory, so the command would be patch -p1 <../linux.2.22.6.patch
.
2. Configuring the Kernel
Now that the patch is applied, the next step is to configure the kernel. There are three methods for configuration:
1> Directly run make menuconfig
. This is the most cumbersome method, as all configurations need to be handled manually.
2> Modify the default configuration by changing the defconfig
file. Use find -name "*defconfig*"
to locate the default configuration file corresponding to your architecture. I found the default configuration file for my board in arch/arm/configs
. To execute the defconfig
file, run: make XXX_defconfig
, where XXX is the specific model of your board. After this operation, the results will be saved in the .config
file. You can then run make menuconfig
, and the configuration will be slightly modified based on the default configuration.
3> Use the manufacturer’s configuration file. If your hardware has a config file provided by the manufacturer, this is the easiest option. Just run cp XXX .config
and then execute make menuconfig
.
Here, let me explain the kernel configuration in detail. The configuration of the Linux kernel is meant to generate the .config
file because this file is needed to generate other related configuration files during compilation. Most of our configuration items are like CONFIG_XXXDRIVER, where XXXDRIVER refers to various drivers. We need to inform the kernel whether these drivers are compiled into the kernel or compiled as modules. By looking for CONFIG_XXXDRIVER, we can find it in four places:
1> C source code
2> Subdirectory Makefile: drivers/XXX/Makefile
3> include/config/auto.conf
4> include/linux/autoconf.h
Here, it should be noted that the .config
file generates include/config/auto.conf
and include/linux/autoconf.h
during kernel compilation (make uImage
). By examining the C source code, we find that CONFIG_XXXDRIVER is a macro definition that equals a constant. In include/linux/autoconf.h
, the macro definition CONFIG_XXXDRIVER is a constant that can be either 0 or 1. Now, there is a question: how do we distinguish whether CONFIG_XXXDRIVER is compiled into the kernel or compiled as a module? This distinction is not possible in C language; it is reflected in the subdirectory’s Makefile. In the subdirectory Makefile, if there is obj -y += XXX.o
, it indicates that XXX.c is compiled into the kernel; obj -m += XXX.o
indicates that XXX is compiled as a module, resulting in XXX.ko. The include/config/auto.conf
file assigns values to CONFIG_XXXDRIVER; when it is y, it means compiled into the kernel, and when it is m, it means compiled as an independent module.
# This is part of include/config/auto.conf # Automatically generated make config: don’t edit # Linux kernel version: 2.6.22.6 # Sun Nov 27 18:34:38 2016 #CONFIG_CPU_S3C244X=y #CONFIG_CPU_COPY_V4WB=y #CONFIG_CRYPTO_CBC=y #CONFIG_CPU_S3C2410_DMA=y #CONFIG_CRYPTO_ECB=m #CONFIG_SMDK2440_CPU2440=y
# This is drivers/i2c/Makefile # Makefile for the i2c core.#
obj-$(CONFIG_I2C_BOARDINFO) += i2c-boardinfo.o obj-$(CONFIG_I2C) += i2c-core.o obj-$(CONFIG_I2C_CHARDEV) += i2c-dev.o obj-y += busses/ chips/ algos/
ifeq ($(CONFIG_I2C_DEBUG_CORE),y) EXTRA_CFLAGS += -DDEBUG endif
3. Compiling the Kernel Through the above description, we can see that in each driver directory, there is a Makefile that defines whether this driver is compiled into the kernel or compiled as a module. Let’s briefly mention this. We discussed how to compile a single file into the kernel or as a module in the Makefile. But how do we write it if there are two or more files? Here’s an example: obj -y += a.o b.o
indicates that a.c and b.c are compiled into the kernel.
obj -m += ab.o
ab-objs := a.o b.o
indicates that a.c and b.c are compiled together into a module. The process is that a.c generates a.o, b.c generates b.o, and a.o and b.o together generate ab.ko. In previous analyses of the U-Boot startup kernel code, we mentioned that the uImage generated by compiling the kernel consists of two parts: header + Linux kernel. When compiling the kernel, we directly execute make uImage
. So how is uImage defined in the file, and how is it generated?
First, we find uImage in arch/arm/Makefile
, and this architecture directory’s Makefile is included in the top-level directory’s Makefile. Thus, when we execute make uImage
, the top-level directory’s Makefile can call the architecture subdirectory’s Makefile to compile the kernel and generate uImage.
Relevant commands in the top-level directory Makefile: include $(srctree)/arch/$(ARCH)/Makefile
Architecture directory’s Makefile relevant commands: zImage Image xipImage bootpImage uImage: vmlinux
This is what we just mentioned: the top-level Makefile calls the architecture directory’s Makefile, which generates uImage, and it depends on the vmlinux file. Next, we will explain how to generate the vmlinux file. In the top-level Makefile, we found most of the commands related to generating vmlinux.
Top-level directory Makefile: init-y := init/ init-y := $(patsubst %/, %/built-in.o, $(init-y))
core-y := usr/ core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/ core-y := $(patsubst %/, %/built-in.o, $(core-y))
libs-y := lib/ libs-y1 := $(patsubst %/, %/lib.a, $(libs-y)) libs-y2 := $(patsubst %/, %/built-in.o, $(libs-y)) libs-y := $(libs-y1) $(libs-y2)
drivers-y := drivers/ sound/ drivers-y := $(patsubst %/, %/built-in.o, $(drivers-y))
net-y := net/ net-y := $(patsubst %/, %/built-in.o, $(net-y)) = net/built-in.o
vmlinux: $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) $(kallsyms.o) FORCE
vmlinux-init := $(head-y) $(init-y) vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y) vmlinux-all := $(vmlinux-init) $(vmlinux-main) vmlinux-lds := arch/$(ARCH)/kernel/vmlinux.lds export KBUILD_VMLINUX_OBJS := $(vmlinux-all)
Architecture directory Makefile: zImage Image xipImage bootpImage uImage: vmlinux
head-y := arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o
I have extracted the commands for generating vmlinux from both the top-level and architecture directories. First, to generate vmlinux, we need vmlinux-lds file, vmlinux-init file, and vmlinux-main file. Among them, vmlinux-lds is the linker script file that defines the storage locations of code and data segments. Next, we look at vmlinux-init, which requires head-y and init-y. By examining the two Makefiles, we can get the results after conversion:
head-y := arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o
init-y := $(patsubst %/, %/built-in.o, $(init-y)) = init/built-in.o
core-y := $(patsubst %/, %/built-in.o, $(core-y)) = usr/built-in.o kernel/built-in.o mm/built-in.o fs/built-in.o ipc/built-in.o security/built-in.o crypto/built-in.o block/built-in.o
libs-y := $(libs-y1) $(libs-y2) =lib/lib.a lib/built-in.o
drivers-y := $(patsubst %/, %/built-in.o, $(drivers-y)) = drivers/built-in.o sound/built-in.o
net-y := $(patsubst %/, %/built-in.o, $(net-y)) = net/built-in.o
Now we have analyzed the entire kernel compilation process. How do we know whether our analysis is correct? By actually executing make uImage
, we can observe the execution process. Here are some related commands during the execution of make uImage
:
arm-linux-ld -EL -p --no-undefined -X -o vmlinux -T arch/arm/kernel/vmlinux.lds arch/arm/kernel/head.o arch/arm/kernel/init_task.o init/built-in.o --start-group usr/built-in.o arch/arm/kernel/built-in.o arch/arm/mm/built-in.o
As we can see, the first target is to generate vmlinux, followed by the linker script vmlinux.lds. The first file generated is head.o, followed by init_task.o. This aligns perfectly with our analysis. The subsequent files follow suit, confirming that our analysis is correct.
SECTIONS{
. = (0xc0000000) + 0x00008000;
.text.head : { _stext = .; _sinittext = .; *(.text.head) }
.init : { /* Init code and data */ *(.init.text) _einittext = .; __proc_info_begin = .; *(.proc.info.init) __proc_info_end = .; __arch_info_begin = .; *(.arch.info.init) __arch_info_end = .; __tagtable_begin = .; *(.taglist.init) __tagtable_end = .; . = ALIGN(16); __setup_start = .; *(.init.setup) __setup_end = .; __early_begin = .; *(.early_param.init) __early_end = .; __initcall_start = .;
This is part of the linker script vmlinux.lds. First, it defines the virtual address: (0xc0000000) + 0x00008000. Then, it executes the header file first, which aligns with our analysis. The code segment, initialization code segment, etc.
This concludes the analysis of the entire process from Linux kernel configuration to compilation.^_^
