本文将分为两个主要部分:
- 内核编译实践指南:一步步教你如何从源码编译一个可用的 Linux 内核。
- Kbuild 构建系统深度解析:详细解释内核是如何通过
Makefile
、Kconfig
和各种脚本来组织和管理编译过程的。
Part 1: Linux 内核编译实践指南
这是一个通用的、适合大多数发行版(如 Ubuntu, Debian, CentOS, Fedora)的内核编译步骤。
准备工作:安装必要的工具链
编译内核需要一系列的开发工具。
在 Debian/Ubuntu 系统上:
sudo apt update
sudo apt install git build-essential kernel-package fakeroot libncurses5-dev libssl-dev ccache bison flex libelf-dev
在 CentOS/Fedora/RHEL 系统上:
sudo yum groupinstall "Development Tools"
sudo yum install ncurses-devel openssl-devel elfutils-libelf-devel bison flex
build-essential
/"Development Tools"
: 包含最核心的编译工具,如gcc
,make
等。libncurses5-dev
/ncurses-devel
:make menuconfig
图形化配置界面所必需。bison
,flex
: 词法/语法分析器生成器,Kbuild 系统解析 Kconfig 文件时需要。libelf-dev
/elfutils-libelf-devel
: 用于处理 ELF 对象文件,一些新特性(如 BTF)需要它。git
: 用于下载内核源码。ccache
: (可选,但强烈推荐) 一个编译器缓存工具,能极大地加速重复编译过程。
步骤一:获取内核源代码
你可以从官方网站 kernel.org 下载 tar 包,或者使用 git 克隆。推荐使用 git,因为它便于版本管理和切换。
# 克隆 Linus Torvalds 的主线仓库
git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
# 或者克隆稳定版仓库
# git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
# 进入源码目录
cd linux
步骤二:配置内核编译选项
内核有数以万计的配置选项,决定了哪些功能、驱动和特性会被编译进去。配置是编译中最关键的一步。
1. 清理环境 (可选)
如果是第一次编译,可以跳过。如果之前编译过,建议清理。
make mrproper
这会删除所有之前的编译产物和 .config
配置文件。
2. 生成初始配置文件
通常不建议从零开始配置。最好的方法是基于你当前正在运行的系统配置进行修改。
# 复制当前系统的配置文件到源码目录,并重命名为 .config
cp /boot/config-$(uname -r) .config
uname -r
会返回你当前内核的版本号,例如 5.15.0-76-generic
。
3. 使用 make olddefconfig
更新配置
新版内核可能增加了新的选项,或者废弃了旧的。此命令会使用默认值自动回答所有新选项,保持旧配置不变。
make olddefconfig
4. (核心步骤) 自定义配置
这是最重要的一步,你可以通过一个交互式菜单来启用或禁用内核功能。
make menuconfig
这会打开一个基于文本的图形化界面:
- 导航: 使用方向键移动,
Enter
进入子菜单,Esc
两次返回上一级或退出。 - 选择: 按
Y
(Yes) 将功能编译进内核 (<*>
),按M
(Module) 将功能编译成模块 (<M>
),按N
(No) 不编译此功能 (< >
)。 - 帮助: 高亮某个选项时按
?
可以查看详细帮助信息。 - 搜索: 按
/
可以搜索特定的配置项(例如,搜索NTFS
)。
完成配置后,选择 < Save >
保存到 .config
文件,然后 < Exit >
退出。
其他配置方式:
make defconfig
: 生成一个适用于当前体系架构的默认配置。make localmodconfig
: 自动检测当前加载的模块,并只将这些模块对应的驱动编译进去,生成一个非常精简的内核。make xconfig
/make gconfig
: 分别是基于 QT 和 GTK 的图形化配置工具,功能更强大。
步骤三:编译内核
配置完成后,开始编译。这个过程会消耗很长时间。
# 使用 ccache (如果安装了)
export CC="ccache gcc"
# -j 后面跟的是你希望并行的任务数,通常设置为 CPU 核心数的 1 到 2 倍
# 例如,一个 8 核 CPU 可以使用 `make -j16`
# 可以通过 nproc 命令查看核心数
make -j$(nproc)
编译成功后,你会在源码根目录和 arch/x86/boot/
目录下找到关键产物:
vmlinux
: 未压缩的、包含调试信息的内核镜像 (ELF 格式)。arch/x86/boot/bzImage
: 压缩后的、可启动的内核镜像 (这才是我们通常要用的)。
步骤四:安装模块
将编译好的内核模块 (.ko
文件) 安装到 /lib/modules/
目录下。
sudo make modules_install
这会在 /lib/modules/
下创建一个以新内核版本号命名的新目录,并把所有模块文件复制进去。
步骤五:安装内核
这一步会将 bzImage
内核镜像、.config
文件和 System.map
文件复制到 /boot
目录,并自动更新引导加载程序 (GRUB)。
sudo make install
make install
通常会自动完成以下工作:
- 复制
arch/x86/boot/bzImage
到/boot/vmlinuz-版本号
。 - 复制
.config
文件到/boot/config-版本号
。 - 复制
System.map
文件到/boot/System.map-版本号
。 - (在很多发行版上) 自动运行
update-initramfs
来创建初始内存盘。 - (在很多发行版上) 自动运行
update-grub
来更新 GRUB 启动菜单。
步骤六:重启并验证
重启你的计算机。在 GRUB 启动菜单中,你应该能看到一个新的内核选项。选择它启动。
进入系统后,通过以下命令验证你是否正在运行新内核:
uname -r
如果显示的和你刚刚编译的版本一致,那么恭喜你,编译成功!
Part 2: Kbuild 构建系统深度解析
Linux 内核是一个极其庞大的项目,有数万个源文件和配置选项。为了能有效管理它,内核开发者创建了一套名为 Kbuild 的强大构建系统。Kbuild 的核心是 make
和一系列精心设计的 Makefile
。
Kbuild 的核心组件
Kbuild 系统主要由以下五个部分组成:
- .config 文件: 内核配置的核心。它定义了
CONFIG_
系列的宏,告诉Makefile
哪些功能被选择为y
(编译进内核)、m
(编译为模块) 或n
(不编译)。该文件由make menuconfig
等配置工具生成。 - 顶层 Makefile: 位于源码根目录的
Makefile
。这是整个构建过程的入口。它的主要职责是:- 读取
.config
文件,并将其中的CONFIG_
变量导出到环境中。 - 确定内核版本号。
- 包含体系架构相关的
Makefile
(如arch/x86/Makefile
)。 - 自顶向下地、递归地调用子目录中的
Makefile
来构建各个部分。
- 读取
- arch/*/Makefile: 位于
arch/<architecture>
目录下的Makefile
。它定义了特定于体系架构的变量和规则,例如,指定最终的内核镜像名称 (bzImage
)、平台相关的编译选项等。顶层Makefile
会包含它。 - Kbuild 文件 (以及子目录中的 Makefile): 在内核源码的几乎每个子目录中,都有一个名为
Kbuild
或Makefile
的文件(Kbuild 系统会优先查找Kbuild
,若不存在则查找Makefile
)。这些文件是 Kbuild 的“工作单元”,它们告诉make
在该目录下需要编译哪些文件。 - scripts/* 目录: 包含了大量的脚本和辅助程序,它们是 Kbuild 系统的“大脑”。例如:
scripts/kconfig/
:mconf
(menuconfig
的后端)、conf
等程序,用于解析Kconfig
文件并生成.config
。scripts/Makefile.*
: 定义了通用的编译规则,例如如何从.c
文件生成.o
文件,如何将多个.o
文件链接起来等。这些被子目录的Makefile
隐式使用。modpost
: 用于处理模块的工具,在编译模块时 indispensable。fixdep
: 用于优化头文件依赖,加速增量编译。
Kbuild 的工作机制和语法
Kbuild 的语法非常简洁。在子目录的 Kbuild
文件中,你主要会看到以下形式的赋值:
obj-y
: 如果对应的CONFIG_FOO
选项在.config
中被设为y
,那么obj-y
列表中的目标文件就会被编译并链接到内核镜像vmlinux
中。obj-m
: 如果对应的CONFIG_FOO
选项在.config
中被设为m
,那么obj-m
列表中的目标文件就会被编译成可加载的内核模块 (.ko
文件)。obj-n
: 如果设为n
或未定义,则不进行编译。
示例:
假设在 drivers/char/
目录下的 Kbuild
文件中有这样一行:
# 如果 CONFIG_SERIAL_8250=y,则 8250.o 会被链接进 vmlinux
# 如果 CONFIG_SERIAL_8250=m,则会生成 8250.ko 模块
obj-$(CONFIG_SERIAL_8250) += 8250.o
这个 $(CONFIG_...)
的用法是 Kbuild 的精髓。当 make
读取 Kbuild
文件时,CONFIG_SERIAL_8250
变量已经被顶层 Makefile
从 .config
中导出并赋值为 y
或 m
。所以,obj-$(CONFIG_SERIAL_8250)
就会动态地变成 obj-y
或者 obj-m
。
多文件目标:
如果一个模块或功能由多个源文件组成,语法如下:
# 定义一个名为 "my_multi_file_module" 的目标
obj-$(CONFIG_MY_FEATURE) += my_multi_file_module.o
# 指定这个目标由哪些源文件构成
my_multi_file_module-objs := file1.o file2.o file3.o
Kbuild 会先将 file1.c
, file2.c
, file3.c
编译成 .o
文件,然后将它们链接成一个单独的 my_multi_file_module.o
,最后根据 CONFIG_MY_FEATURE
的值决定是把它链接进 vmlinux
还是生成 my_multi_file_module.ko
。
Kconfig 的角色
Kbuild
文件决定了 如何构建,而 Kconfig
文件则决定了 可以构建什么。
每个包含可配置功能的目录都有一个 Kconfig
文件。它定义了 make menuconfig
中看到的菜单结构、选项、依赖关系和帮助文本。
示例 (drivers/char/Kconfig
):
menu "Character devices"
config SERIAL_8250
tristate "8250/16550 and compatible serial support"
depends on TTY
select SERIAL_CORE
help
This is the standard serial driver for the original IBM PC
and compatibles. If you have a standard serial port (e.g., COM1),
say Y or M here.
endmenu
menu
/endmenu
: 定义一个菜单块。config SERIAL_8250
: 定义一个名为CONFIG_SERIAL_8250
的配置选项。tristate "..."
: 定义选项的提示文本,tristate
表示它可以是y
,m
或n
。如果是bool
,则只能是y
或n
。depends on TTY
: 定义依赖关系。只有当CONFIG_TTY=y
时,这个选项才可见。select SERIAL_CORE
: 定义反向依赖。如果CONFIG_SERIAL_8250
被选中,则CONFIG_SERIAL_CORE
也会被自动选中。help
: 提供详细的帮助信息。
完整的构建流程总结
- 配置阶段:
- 用户运行
make menuconfig
。 mconf
程序被调用,它解析所有Kconfig
文件,构建出依赖关系树,并显示交互式菜单。- 用户保存配置,生成
.config
文件。
- 用户运行
- 编译阶段:
- 用户运行
make
。 - 顶层
Makefile
被执行。- 它读取
.config
文件,将CONFIG_*
变量导出到环境中。 - 它包含
arch/$(ARCH)/Makefile
来设置体系架构相关的参数。 - 它从一个核心目标列表(如
init
,core
,drivers
,net
等)开始,递归地调用子目录的make
。
- 它读取
- 子目录
make
被执行。make
使用scripts/Makefile.build
等通用规则文件。- 它读取本地的
Kbuild
文件。 - 根据
Kbuild
中的obj-y/m
和从.config
导入的环境变量,决定要编译哪些.c
文件为.o
文件。 - 所有
obj-y
对应的.o
文件被链接成一个built-in.o
文件。 - 所有
obj-m
对应的.o
文件被单独处理,最终生成.ko
模块文件。
- 递归返回。
- 子目录的
built-in.o
文件被上一级目录的Makefile
链接到它自己的built-in.o
中。 - 这个过程一直持续到顶层。
- 子目录的
- 最终链接。
- 在顶层
Makefile
中,所有核心部分的built-in.o
文件(如init/built-in.o
,drivers/built-in.o
等)以及其他必要的文件,最终被链接器ld
链接成最终的vmlinux
内核文件。 vmlinux
再经过处理(压缩、添加启动头等)生成可启动的bzImage
。modpost
等工具完成对模块的最后处理。
- 在顶层
- 用户运行