kbuild的用法及示例

kbuild (Kernel Build System) 是 Linux 内核源代码使用的高度自动化编译系统。它负责配置内核、编译源码、处理模块依赖、链接生成内核镜像 (vmlinuz) 和模块 (.ko 文件)。掌握 kbuild 是进行 Linux 内核开发和定制的基础。

核心组成部分:

  1. Makefile 内核源代码树中遍布大量 Makefile
    • 顶层 Makefile 位于内核源码根目录。这是执行所有内核构建命令的入口点 (make ...)。
    • 各级子目录 Makefile (arch/, drivers/, fs/, net/ 等) 定义了该子目录下哪些文件需要编译,编译成什么(内置到内核 y,编译成模块 m,或不编译 n)。
  2. Kconfig 定义了内核的配置选项及其层次关系、依赖关系和帮助文本。这些文件驱动 make menuconfig, make xconfig 等配置界面。
  3. .config 文件: 位于内核源码根目录。这是核心配置文件,保存了用户通过配置界面 (如 menuconfig) 选择的所有选项 (CONFIG_XXX=y/m/n)。kbuild 读取此文件决定编译哪些部分以及如何编译。
  4. scripts/ 目录: 包含 kbuild 系统本身依赖的 Perl、Python 和 Shell 脚本,用于解析 Kconfig、生成配置界面、处理依赖关系、生成头文件 (include/generated/autoconf.h) 等幕后工作。

基本用法和常用命令:

  1. 配置内核 (生成 .config): 这是构建前必须的步骤。

    • make menuconfig: 基于文本的经典菜单界面 (依赖 ncurses-dev)。最常用。
    • make nconfig: 更新的文本菜单界面 (类似 menuconfig,交互更好)。
    • make xconfig: 基于 Qt 的图形界面 (依赖 qtbase5-devqttools5-dev-tools 等)。
    • make gconfig: 基于 Gtk 的图形界面 (依赖 libglade2-dev, libgtk2.0-dev 等)。
    • make oldconfig: 基于已有的 .config 文件,询问新增的配置项。升级内核版本时常用。
    • make defconfig: 生成对应架构的默认配置。
    • make allyesconfig: 尽可能把所有选项编译为 y (生成尽可能大的内核)。
    • make allnoconfig: 尽可能把所有选项编译为 n (生成最小内核)。
    • make savedefconfig: 将当前 .config 精简保存为 defconfig 格式 (只包含非默认值),常用于分发最小配置。
  2. 编译内核:

    • make: 编译整个内核和所有配置为 ym 的模块。生成 vmlinux (原始 ELF 内核), bzImage/zImage (压缩的可引导镜像) 等。
    • make -jN: 启用并行编译,N 是并行任务数 (通常是 CPU 核心数 + 1)。显著加速编译 (如 make -j8)。
  3. 编译模块:

    • make modules: 只编译所有配置为 m 的内核模块 (.ko 文件),不编译内核本身。通常紧跟在 make 之后运行,或者单独运行以重新编译模块。
  4. 安装模块:

    • sudo make modules_install: 将编译好的模块 (.ko 文件) 安装到 /lib/modules/$(uname -r)/ 目录下。需要 root 权限$(uname -r) 会自动替换为当前内核版本字符串 (由 .config 中的 CONFIG_LOCALVERSION 等决定)。
  5. 安装内核: (可选,通常用于将新内核设为系统引导选项)

    • sudo make install: 将内核镜像 (bzImage/zImage) 和初始 RAM 磁盘 (initrd) 复制到 /boot/ 目录,并更新引导加载器 (如 GRUB) 的配置。需要 root 权限。行为由 arch/<arch>/boot/install.sh 脚本定义。
  6. 清理编译输出:

    • make clean: 删除大多数编译生成的文件 (.o, .ko, .cmd 等),保留 .config 和构建模块所需文件。下次 make 会重新编译大部分内容。
    • make mrproper: 彻底清理。删除所有编译生成的文件,包括 .config 文件!将源码树恢复到解压后的状态(除了手动修改的文件)。在切换配置或解决构建问题时常用。
    • make distclean:mrproper 更彻底,还会删除编辑器备份文件、补丁文件等。一般很少用。
  7. 编译单个目录或文件: (开发调试时常用)

    • make path/to/dir/: 编译指定目录及其子目录下的所有内容 (根据 .config 配置)。
    • make path/to/file.o: 编译指定的单个源文件 (如 make drivers/net/ethernet/realtek/r8169.o)。依赖该文件的头文件。
    • make M=path/to/dir: 非常重要! 编译指定目录下的内核模块。只编译该目录下的模块,忽略其他目录。模块开发的核心命令。
      • make M=drivers/net/wireless/realtek/ (编译特定无线驱动目录)
      • make -j8 M=drivers/bluetooth/ (并行编译蓝牙驱动目录)

重要环境变量:

  • ARCH: 指定目标 CPU 架构。这是交叉编译或编译非宿主机构建的关键。
    • export ARCH=arm64 (编译 ARM64 内核)
    • export ARCH=x86 (编译 x86/x86_64 内核,通常默认就是 x86)
    • 也可以直接在 make 命令中指定:make ARCH=arm64 menuconfig
  • CROSS_COMPILE: 指定交叉编译工具链前缀。与 ARCH 配合用于交叉编译。
    • export CROSS_COMPILE=aarch64-linux-gnu- (使用 aarch64-linux-gnu-gcc, aarch64-linux-gnu-ld 等)
    • make CROSS_COMPILE=arm-linux-gnueabihf- ARCH=arm (编译 ARM 32 位内核)
  • INSTALL_MOD_PATH: 指定 make modules_install 安装模块的根目录,而不是系统的 /
    • export INSTALL_MOD_PATH=/path/to/rootfs (常用于为嵌入式系统构建根文件系统)
    • sudo make modules_install INSTALL_MOD_PATH=/mnt/sd_card (安装到 SD 卡的挂载点)
  • KBUILD_OUTPUT (或 O): 指定编译输出目录 (对象文件, .config, 最终镜像等),与源码目录分离。
    • export KBUILD_OUTPUT=/path/to/build_dir
    • make O=/path/to/build_dir menuconfig (首次配置需指定 O)
    • make O=/path/to/build_dir -j8 (在输出目录编译)
    • 优点: 保持源码树干净;方便为不同架构/配置维护多个构建目录。

详细示例:

示例 1:在 x86_64 主机上配置、编译、安装一个用于本机的新内核

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 1. 进入内核源码目录
cd ~/linux-6.1.10/

# 2. 获取当前运行内核的配置作为起点 (可选,但推荐)
cp /boot/config-$(uname -r) .config

# 3. 基于当前 .config 检查新选项 (升级内核时尤其重要)
make oldconfig
# 或者使用菜单界面调整配置 (推荐)
make menuconfig

# 4. 清理(可选,如果是第一次编译或配置变动很大)
make clean # 或者 make mrproper (会删除 .config! 小心)

# 5. 编译内核和模块 (-jN 根据你的CPU核心数调整)
make -j8

# 6. 安装内核模块 (需要 sudo)
sudo make modules_install

# 7. 安装内核镜像并更新引导器 (需要 sudo)
sudo make install

# 8. 重启系统,在 GRUB 菜单中选择新内核启动
sudo reboot

示例 2:为 ARM64 设备交叉编译内核和驱动模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. 设置环境变量 (假设交叉编译工具链在 PATH 中)
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-

# 2. 进入内核源码目录
cd ~/linux-6.1.10/

# 3. 配置 (使用目标设备的 defconfig 或 menuconfig)
make defconfig # 使用默认配置
# 或者 (更常见)
make menuconfig # 手动调整配置

# 4. 编译整个内核
make -j8

# 5. 编译特定驱动模块 (例如 Realtek USB WiFi)
make -j8 M=drivers/net/wireless/realtek/rtl8xxxu/

# 6. 找到编译好的文件:
# - 内核镜像: arch/arm64/boot/Image (或 Image.gz)
# - 设备树: arch/arm64/boot/dts/vendor/board.dtb (路径依平台而定)
# - 模块: drivers/net/wireless/realtek/rtl8xxxu/*.ko
# 将这些文件复制到目标设备的相应位置 (boot 分区, /lib/modules/)

示例 3:使用分离的输出目录 (O=) 编译内核

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 创建输出目录
mkdir -p ~/kernel_builds/my_config

# 2. 进入内核源码目录 (可选,可以在外面指定 O)
cd ~/linux-6.1.10/

# 3. 在输出目录中生成配置 (首次)
make O=~/kernel_builds/my_config menuconfig

# 4. 在输出目录中编译
make O=~/kernel_builds/my_config -j8

# 5. 在输出目录中安装模块 (到默认的 /lib/modules/ 或指定 INSTALL_MOD_PATH)
sudo make O=~/kernel_builds/my_config modules_install
sudo make O=~/kernel_builds/my_config install

# 后续操作都通过 `O=` 指向该构建目录即可

示例 4:仅重新编译并安装一个修改过的驱动模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 假设你修改了 drivers/net/ethernet/intel/e1000/e1000_main.c

# 1. 进入内核源码目录
cd ~/linux-6.1.10/

# 2. 编译单个模块 (确保 .config 中该驱动配置为 'm')
make M=drivers/net/ethernet/intel/e1000/

# 3. 仅安装这个模块 (需要 sudo)
# 先找到模块在目标系统 /lib/modules/ 中的安装路径
sudo cp drivers/net/ethernet/intel/e1000/e1000.ko /lib/modules/$(uname -r)/kernel/drivers/net/ethernet/intel/e1000/
# 或者更安全地使用 modules_install (它会处理依赖和符号链接,但会安装所有模块,效率稍低)
sudo make M=drivers/net/ethernet/intel/e1000/ modules_install

# 4. 更新模块依赖
sudo depmod -a

# 5. 卸载旧模块 (如果已加载)
sudo rmmod e1000
# 6. 加载新模块
sudo modprobe e1000

关键提示:

  • 仔细阅读 make help 在源码目录运行 make help 会列出所有可用的 make 目标及其简短描述。
  • 理解 .config 这是构建的核心。备份好你的工作配置 (cp .config my_backup_config)。
  • menuconfig 是核心工具: 花时间学习如何在 menuconfig 中导航、搜索 (/) 和查看帮助 (?)。
  • M= 是模块开发的生命线: 极大地加速了模块的编译-测试循环。
  • 交叉编译是嵌入式开发的常态: 熟练使用 ARCHCROSS_COMPILE
  • 输出目录 (O=) 保持源码整洁: 强烈推荐用于管理多个构建配置。
  • 错误信息: 编译错误通常指向具体的文件和行号。配置错误可能更隐晦,注意检查 make 输出的警告和 .config 中的依赖关系 (在 menuconfig 中按 ? 查看选项依赖)。
  • 文档: Linux 内核源码 Documentation/kbuild/ 目录下有关于 kbuild 系统的详细文档。

掌握这些命令和概念,你就能高效地使用 kbuild 系统来配置、构建和定制 Linux 内核及其模块了。

Linux内核Kbuild文件的用法及示例

在 Linux 内核构建系统(Kbuild)中,Kbuild 文件(有时命名为 MakefileKbuild)是定义模块/驱动编译规则的核心配置文件。它们位于内核源码树的每个子目录中,控制着当前目录下的源代码如何被编译成内核对象(.o 文件)或可加载模块(.ko 文件)。


Kbuild 文件的核心功能

  1. 定义编译目标:指定哪些文件要编译进内核(y)、编译为模块(m)或不编译(n
  2. 处理多文件模块:将多个源文件组合成一个模块
  3. 递归构建子目录
  4. 设置编译选项:添加目录级或文件级的编译标志
  5. 条件编译:根据内核配置(CONFIG_XXX)动态调整编译行为

核心语法与常用变量

变量 作用
obj-y 编译进内核(built-in)的对象文件列表
obj-m 编译成可加载模块的对象文件列表
obj-$(CONFIG_X) 根据配置选项 CONFIG_X 动态选择编译方式(y/m/n
foo-y 多文件模块依赖列表(如 foo-objs := file1.o file2.o
lib-y 编译成本地库文件(.a)的对象列表
ccflags-y 添加当前目录的 C 编译器标志(如 -I 指定头文件路径)
CFLAGS_file.o 为特定文件添加编译选项
subdir-y 递归构建子目录(已弃用,改用 obj-y += dir/

典型示例解析

示例 1:单文件驱动模块

1
2
# drivers/char/mydriver/Kbuild
obj-$(CONFIG_MY_DRIVER) += mydriver.o
  • CONFIG_MY_DRIVER=y 时:mydriver.c 编译进内核
  • CONFIG_MY_DRIVER=m 时:生成 mydriver.ko 可加载模块

示例 2:多文件模块

1
2
3
4
5
6
# drivers/net/wireless/realtek/rtw88/Kbuild
obj-$(CONFIG_RTW88) += rtw88_core.o
rtw88_core-y := main.o mac80211.o util.o phy.o

obj-$(CONFIG_RTW88_8822BE) += rtw88_8822be.o
rtw88_8822be-y := rtw8822be.o
  • rtw88_core.o 由 4 个源文件(main.c, mac80211.c 等)编译合成
  • rtw88_8822be.o 是独立模块,依赖 rtw8822be.c

示例 3:递归构建子目录

1
2
3
4
5
6
7
# fs/ext4/Kbuild
obj-$(CONFIG_EXT4_FS) += ext4.o
ext4-y := balloc.o bitmap.o dir.o ...
ext4-$(CONFIG_EXT4_FS_ENCRYPTION) += crypto_policy.o

# 递归构建 crypto/ 子目录
obj-$(CONFIG_EXT4_FS_ENCRYPTION) += crypto/
  • crypto/ 子目录下的 Kbuild 会被自动处理
  • 只有开启 CONFIG_EXT4_FS_ENCRYPTION 时才构建该子目录

示例 4:条件编译与特殊选项

1
2
3
4
5
6
7
8
9
10
11
12
13
# drivers/gpu/drm/i915/Kbuild
obj-$(CONFIG_DRM_I915) += i915.o

# 多文件组件
i915-y := i915_drv.o i915_irq.o i915_params.o ...

# 调试选项
ccflags-y += -I$(src) # 添加当前目录到头文件路径
CFLAGS_i915_debugfs.o = -DDEBUG # 仅为 debugfs.o 添加 -DDEBUG

# 条件编译组件
i915-$(CONFIG_ACPI) += i915_acpi.o
i915-$(CONFIG_DRM_I915_GVT) += gvt/

示例 5:模块别名定义

1
2
3
4
5
# drivers/net/ethernet/intel/e1000/Kbuild
obj-$(CONFIG_E1000) += e1000.o

# 定义模块别名(用于 modprobe 自动加载)
$(eval $(call add_netdev_module_macro,e1000,CONFIG_E1000))
  • 实际调用 scripts/netdev_modules.mk 中的宏
  • 生成 /lib/modules/$(KVER)/modules.alias 条目:
    1
    alias pci:v00008086d000010B7sv*sd*bc*sc*i* e1000

高级技巧

1. 文件级编译选项

1
2
# 禁用特定警告(适用于第三方驱动)
CFLAGS_third_party.o = -Wno-unused-function -Wno-missing-field-initializers

2. 依赖外部头文件

1
2
# 添加非标准头文件路径
ccflags-y += -I$(src)/../include -I$(src)/../../shared

3. 条件文件包含

1
2
3
# 根据架构选择文件
obj-$(CONFIG_X86) += x86_specific.o
obj-$(CONFIG_ARM) += arm_specific.o

目录结构示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
drivers/net/wireless/realtek/
├── rtl818x/
│ ├── Kbuild # 定义 rtl8180/rtl8187 驱动
│ ├── rtl8180/
│ │ ├── Kbuild # 子目录构建规则
│ │ ├── dev.c
│ │ └── ...
│ └── rtl8187/
│ └── ...
└── rtw88/
├── Kbuild # 主 Kbuild(示例 2)
├── main.c
├── mac80211.c
└── ...

调试建议

  1. 查看实际编译命令

    1
    make V=1 M=drivers/net/ethernet/intel/e1000
  2. 检查生成的对象依赖

    1
    cat drivers/net/ethernet/intel/e1000/.e1000.o.cmd
  3. 验证 Kbuild 包含关系

    1
    make -n M=path/to/module  # 干跑模式显示构建步骤

关键规则

  1. 每个子目录的 obj-y/m 会合并到该目录的 built-in.a(旧版为 built-in.o
  2. 顶层 Makefile 通过 vmlinux-alldirs 递归所有包含的目录
  3. 模块最终由 scripts/Makefile.modpost 处理生成 .ko

通过合理设计 Kbuild 文件,您可以高效管理复杂的内核驱动结构,实现条件编译、跨平台支持与自动化构建。实践中可参考内核源码树中 drivers/fs/net/ 等目录的官方示例。