linux 内核 编译构建方式及Kbuild 系统说明

本文将分为两个主要部分:

  1. 内核编译实践指南:一步步教你如何从源码编译一个可用的 Linux 内核。
  2. Kbuild 构建系统深度解析:详细解释内核是如何通过 MakefileKconfig 和各种脚本来组织和管理编译过程的。

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 通常会自动完成以下工作:

  1. 复制 arch/x86/boot/bzImage/boot/vmlinuz-版本号
  2. 复制 .config 文件到 /boot/config-版本号
  3. 复制 System.map 文件到 /boot/System.map-版本号
  4. (在很多发行版上) 自动运行 update-initramfs 来创建初始内存盘。
  5. (在很多发行版上) 自动运行 update-grub 来更新 GRUB 启动菜单。

步骤六:重启并验证

重启你的计算机。在 GRUB 启动菜单中,你应该能看到一个新的内核选项。选择它启动。
进入系统后,通过以下命令验证你是否正在运行新内核:

uname -r

如果显示的和你刚刚编译的版本一致,那么恭喜你,编译成功!


Part 2: Kbuild 构建系统深度解析

Linux 内核是一个极其庞大的项目,有数万个源文件和配置选项。为了能有效管理它,内核开发者创建了一套名为 Kbuild 的强大构建系统。Kbuild 的核心是 make 和一系列精心设计的 Makefile

Kbuild 的核心组件

Kbuild 系统主要由以下五个部分组成:

  1. .config 文件: 内核配置的核心。它定义了 CONFIG_ 系列的宏,告诉 Makefile 哪些功能被选择为 y (编译进内核)、m (编译为模块) 或 n (不编译)。该文件由 make menuconfig 等配置工具生成。
  2. 顶层 Makefile: 位于源码根目录的 Makefile。这是整个构建过程的入口。它的主要职责是:
    • 读取 .config 文件,并将其中的 CONFIG_ 变量导出到环境中。
    • 确定内核版本号。
    • 包含体系架构相关的 Makefile (如 arch/x86/Makefile)。
    • 自顶向下地、递归地调用子目录中的 Makefile 来构建各个部分。
  3. arch/*/Makefile: 位于 arch/<architecture> 目录下的 Makefile。它定义了特定于体系架构的变量和规则,例如,指定最终的内核镜像名称 (bzImage)、平台相关的编译选项等。顶层 Makefile 会包含它。
  4. Kbuild 文件 (以及子目录中的 Makefile): 在内核源码的几乎每个子目录中,都有一个名为 KbuildMakefile 的文件(Kbuild 系统会优先查找 Kbuild,若不存在则查找 Makefile)。这些文件是 Kbuild 的“工作单元”,它们告诉 make 在该目录下需要编译哪些文件。
  5. 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 中导出并赋值为 ym。所以,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, mn。如果是 bool,则只能是 yn
  • depends on TTY: 定义依赖关系。只有当 CONFIG_TTY=y 时,这个选项才可见。
  • select SERIAL_CORE: 定义反向依赖。如果 CONFIG_SERIAL_8250 被选中,则 CONFIG_SERIAL_CORE 也会被自动选中。
  • help: 提供详细的帮助信息。

完整的构建流程总结

  1. 配置阶段:
    • 用户运行 make menuconfig
    • mconf 程序被调用,它解析所有 Kconfig 文件,构建出依赖关系树,并显示交互式菜单。
    • 用户保存配置,生成 .config 文件。
  2. 编译阶段:
    • 用户运行 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 等工具完成对模块的最后处理。

本文版权归原作者zhaofujian所有,采用 CC BY-NC-ND 4.0 协议进行许可,转载请注明出处。

发表评论