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 包含
.config文件,将配置选项转换为 Make 变量(如CONFIG_XXX=y或CONFIG_XXX=m)。 - 递归入口:顶层 Makefile 调用
scripts/Makefile.build,传入目标子目录列表。 - 子目录处理:
Makefile.build进入每个子目录,读取该目录下的 Kbuild Makefile,解析obj-y、obj-m等变量。 - 编译与链接:根据变量声明,将源文件编译为
.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-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 错误。