4. 内核构建系统与 Makefile:Kbuild 系统架构、驱动 Makefile 编写规范、obj-y 与 obj-m 的区别、模块编译与内置编译

Linux 内核的构建系统(Kbuild)是驱动开发者必须掌握的基础设施。它并非简单的 Makefile 集合,而是一套高度结构化的递归构建框架,负责解析源码树、处理依赖、决定编译目标(内置或模块),并最终生成内核镜像与可加载模块。理解 Kbuild 的架构与驱动 Makefile 的编写规范,是确保驱动在不同内核版本间正确移植与编译的前提。

4.1 Kbuild 系统架构

Kbuild 系统采用“顶层 Makefile + 各级 Kconfig + 各级 Makefile”的三层协作模式。顶层 Makefile 定义全局规则与目标;Kconfig 定义配置选项;各级子目录中的 Makefile(通常称为 Kbuild Makefile)则声明本目录下的编译对象。整个构建流程由 make 命令触发,通过递归下降方式逐层处理。

Kbuild 系统架构图 顶层 Makefile Kconfig 配置 (.config) 递归构建:scripts/Makefile.build 遍历所有已配置的子目录 drivers/Makefile fs/Makefile net/Makefile xxx_driver.c yyy_driver.c zzz_driver.c .o / .ko .o / .ko .o / .ko 入口 配置 构建 子目录 源码 输出

Kbuild 的核心执行流程如下:

  1. 配置读取:顶层 Makefile 包含 .config 文件,将配置选项转换为 Make 变量(如 CONFIG_XXX=y 或 CONFIG_XXX=m)。
  2. 递归入口:顶层 Makefile 调用 scripts/Makefile.build,传入目标子目录列表。
  3. 子目录处理:Makefile.build 进入每个子目录,读取该目录下的 Kbuild Makefile,解析 obj-y、obj-m 等变量。
  4. 编译与链接:根据变量声明,将源文件编译为 .o 文件;对于 obj-y,最终链接入 built-in.a;对于 obj-m,生成 .ko 模块。

4.2 驱动 Makefile 编写规范

驱动开发者通常只需编写所在目录的 Kbuild Makefile。其语法简洁,核心是声明哪些目标文件参与构建。以下是一个标准示例:

# SPDX-License-Identifier: GPL-2.0
# Makefile for my_driver

obj-$(CONFIG_MY_DRIVER) += my_driver.o

my_driver-objs := core.o ioctl.o platform.o

编写规范要点:

  • 使用条件赋值:始终使用 obj-$(CONFIG_xxx) 形式,而非直接写 obj-y 或 obj-m。这确保驱动编译方式由内核配置决定,便于移植。
  • 多文件模块:当驱动由多个源文件组成时,定义 <模块名>-objs 变量列出所有目标文件。模块名必须与 obj- 赋值的左侧一致。
  • 头文件路径:使用 ccflags-y 添加额外的编译标志,如 ccflags-y += -I$(src)/include。
  • 避免绝对路径:使用 $(src) 引用当前源码目录,使用 $(obj) 引用构建输出目录。
移植提示: 不同内核版本对 Kbuild Makefile 的语法支持基本一致,但 ccflags-y 在 2.6.30 之前的内核中可能不被支持,需改用 EXTRA_CFLAGS。在移植老旧驱动时,应优先检查此差异。

4.3 obj-y 与 obj-m 的区别

obj-y 和 obj-m 是 Kbuild 中两个最核心的赋值目标,它们决定了目标文件的编译方式与最终归宿。下表总结了二者的关键区别:

属性 obj-y obj-m
编译方式 内置编译(built-in) 模块编译(loadable module)
输出文件 .o → 链接入 built-in.a .ko(内核模块文件)
链接目标 最终链接入 vmlinux 内核镜像 独立编译,不链接入内核镜像
加载方式 随内核启动自动加载 通过 insmod/modprobe 动态加载
配置值 CONFIG_xxx=y CONFIG_xxx=m
典型场景 核心子系统、必须的驱动 可选驱动、调试驱动、树外驱动

在驱动移植中,理解这一区别至关重要:

  • 如果驱动被标记为 obj-y,其代码会直接链接入内核镜像,无法单独卸载。这适用于平台必须的驱动(如中断控制器、时钟驱动)。
  • 如果驱动被标记为 obj-m,则生成独立的 .ko 文件,可在系统运行时动态加载与卸载。这是大多数外设驱动的首选方式。
  • 同一驱动可以在不同配置下切换:例如,开发阶段使用 =m 便于调试,发布阶段改为 =y 减少启动依赖。

4.4 模块编译与内置编译

Kbuild 为模块编译和内置编译提供了两套独立的编译流程,但共享同一套 Makefile 声明。开发者无需为两种模式编写不同的 Makefile,只需通过配置选项控制。

模块编译与内置编译流程对比 内置编译 (obj-y) 编译为 .o 文件 链接入 built-in.a 最终链接入 vmlinux 随内核启动自动加载 模块编译 (obj-m) 编译为 .o 文件 链接为 .ko 模块文件 独立于内核镜像 insmod / modprobe 动态加载

在实际工程中,模块编译与内置编译的选择会影响驱动的调试与部署策略:

工程实践建议:
  • 开发阶段:优先使用 obj-m 编译为模块。修改驱动后只需重新编译模块并 insmod,无需重启整个内核,大幅缩短调试周期。
  • 发布阶段:对于关键路径驱动(如存储控制器、网络核心驱动),建议改为 obj-y 内置编译,减少模块加载开销与依赖风险。
  • 树外驱动:使用 obj-m 配合 KDIR 指向内核源码树,通过 make -C /lib/modules/$(uname -r)/build M=$(PWD) modules 命令独立编译。

4.5 树外驱动编译示例

对于不在内核源码树内的驱动(如厂商提供的 BSP 驱动),需要编写独立的 Makefile 来调用 Kbuild 系统。以下是一个标准模板:

# SPDX-License-Identifier: GPL-2.0
# Makefile for out-of-tree driver

# 内核源码树路径(或已构建的内核头文件路径)
KDIR ?= /lib/modules/$(shell uname -r)/build

# 当前驱动源码路径
PWD := $(shell pwd)

# 模块名称
obj-m := my_driver.o
my_driver-objs := core.o ioctl.o

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

install:
	$(MAKE) -C $(KDIR) M=$(PWD) modules_install
	depmod -a

该模板的关键点:

  • KDIR 指向内核构建目录,必须包含已配置的 .config 和编译所需的头文件。
  • M=$(PWD) 告诉 Kbuild 将当前目录作为外部模块目录处理。
  • modules 目标触发模块编译流程;modules_install 将生成的 .ko 安装到 /lib/modules/<version>/extra/。
  • 使用 depmod -a 更新模块依赖关系,确保 modprobe 能正确加载。
版本兼容警告: 树外驱动编译时,KDIR 指向的内核版本必须与目标运行系统的内核版本完全一致(包括小版本号)。即使 .config 相同,不同补丁级别的内核导出的符号表(Module.symvers)可能存在差异,导致模块加载时出现 Unknown symbol 错误。