3. Makefile 深入解析:obj-m 与 obj-y 的区别、KERNELDIR 与 PWD 变量、make 命令执行流程
在嵌入式 Linux 驱动开发中,Makefile 是连接源码与内核编译系统的桥梁。理解 obj-m 与 obj-y 的语义差异、KERNELDIR 与 PWD 的变量传递机制,以及 make 命令的递归执行流程,是编写正确、可移植驱动 Makefile 的基础。本章将逐一拆解这些核心概念。
3.1 obj-m 与 obj-y:内核模块的编译开关
Linux 内核的 Kbuild 系统通过 obj-* 系列变量决定哪些目标文件(.o)需要被编译,以及以何种方式链接。在驱动开发中,最常接触的是 obj-m 和 obj-y。
| 变量 | 全称含义 | 编译行为 | 生成产物 | 典型使用场景 |
|---|---|---|---|---|
obj-m |
module | 将目标文件编译为可动态加载/卸载的内核模块 | .ko 文件 |
外部驱动、设备驱动、文件系统驱动等可插拔模块 |
obj-y |
yes | 将目标文件静态编译并链接进内核镜像 | 内置在 vmlinux 或 zImage 中 |
核心子系统、必须随内核启动的驱动、板级支持包 |
关键区别:
obj-m 生成独立的 .ko 文件,可在系统运行时通过 insmod / modprobe 加载;obj-y 则直接编译进内核,无法动态卸载。在驱动开发调试阶段,优先使用 obj-m 以加快迭代速度。
一个典型的单文件驱动 Makefile 写法如下:
# 最简单的驱动模块 Makefile
obj-m := my_driver.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
当驱动由多个源文件组成时,需要使用 <模块名>-objs 或 <模块名>-y 语法:
# 多文件驱动模块 Makefile
obj-m := complex_drv.o
complex_drv-objs := main.o ioctl.o probe.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
避坑指南: 不要混淆
obj-m 与 obj-y 的赋值符号。使用 := 是简单赋值,使用 += 是追加赋值。在内核源码树内,obj-y 通常使用 += 累积多个子目录或文件;在外部模块 Makefile 中,obj-m 推荐使用 := 明确指定模块目标。
3.2 KERNELDIR 与 PWD:编译上下文的锚点
外部驱动模块的编译必须依赖已配置好的内核源码树。两个关键变量 KERNELDIR 和 PWD 共同定义了编译的上下文环境。
- KERNELDIR:指向内核源码树的路径。Kbuild 系统在此目录下读取
Makefile、.config以及所有头文件和内建规则。常见取值包括:/lib/modules/$(shell uname -r)/build—— 指向当前运行内核的编译树(通常是一个符号链接)。- 交叉编译时指向目标平台的内核源码绝对路径,如
/home/user/kernel/linux-5.10。
- PWD:当前驱动源码所在的绝对路径。通过
M=$(PWD)传递给 Kbuild,告诉内核编译系统“请进入此目录编译obj-m指定的目标”。
常见错误: 如果
KERNELDIR 指向的内核源码未经过配置(缺少 .config 或 Module.symvers),编译会立即失败,提示找不到 rules 或 modules 目标。务必先执行 make defconfig 或 make menuconfig 完成内核配置。
变量传递的完整路径如下图所示:
3.3 make 命令执行流程:从用户输入到 .ko 生成
当用户在驱动目录下执行 make 时,背后发生了一系列递归调用。理解这个流程有助于定位编译错误和优化编译速度。
- 用户执行
make:Makefile 解析默认目标default,该目标依赖于$(MAKE) -C $(KERNELDIR) M=$(PWD) modules。 - 第一次递归(进入内核树):
make切换到KERNELDIR目录,读取顶层 Makefile。顶层 Makefile 设置全局编译规则、交叉编译工具链、架构标志等。 - 解析
M=参数:Kbuild 检测到M=变量非空,进入“外部模块编译”模式。此时不会编译整个内核,而是只处理M指向的目录。 - 第二次递归(进入驱动目录):Kbuild 在
M=$(PWD)目录下查找Makefile或Kbuild文件,读取其中的obj-m变量。 - 编译与链接:Kbuild 根据
obj-m列表,依次执行.c→.o的编译,然后调用$(LD)链接生成.ko文件,同时生成.mod.c和.mod.o等辅助文件。 - 模块签名(可选):如果内核启用了模块签名(
CONFIG_MODULE_SIG),Kbuild 还会对.ko进行签名。
性能提示: 每次执行
make 都会完整解析内核顶层 Makefile,这在大内核树中可能耗时数秒。如果只是修改了驱动源码,可以使用 make -C $(KERNELDIR) M=$(PWD) modules 跳过不必要的依赖检查。使用 make -jN 可以并行编译,显著加快速度。
完整的递归编译流程如下图所示:
3.4 实战:编写一个完整的驱动 Makefile
综合以上知识点,下面给出一个支持交叉编译、多文件、带条件编译的通用驱动 Makefile 模板:
# 通用外部驱动模块 Makefile 模板
# 支持交叉编译、多文件、条件编译
# 模块名称
MOD_NAME := my_driver
# 目标文件
obj-m := $(MOD_NAME).o
# 多文件组成
$(MOD_NAME)-objs := core.o ioctl.o platform.o
# 条件编译:如果定义了 CONFIG_FEATURE_A,则包含 feature_a.o
ifeq ($(CONFIG_FEATURE_A),y)
$(MOD_NAME)-objs += feature_a.o
ccflags-y += -DCONFIG_FEATURE_A
endif
# 内核源码树路径(支持交叉编译时覆盖)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# 当前目录
PWD := $(shell pwd)
# 交叉编译工具链前缀(默认空,即使用本地 gcc)
CROSS_COMPILE ?=
# 架构(默认 x86,交叉编译时需指定)
ARCH ?= x86_64
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) \
ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
# 调试目标:打印变量
debug:
@echo "KERNELDIR = $(KERNELDIR)"
@echo "PWD = $(PWD)"
@echo "ARCH = $(ARCH)"
@echo "CROSS_COMPILE = $(CROSS_COMPILE)"
@echo "obj-m = $(obj-m)"
@echo "ccflags-y = $(ccflags-y)"
使用建议: 将上述模板保存为
Makefile,放在驱动源码根目录。执行 make debug 可快速验证变量是否正确;执行 make 编译;执行 make install 将模块安装到 /lib/modules/<kernel_version>/extra/ 目录。
3.5 常见编译错误与排查
| 错误现象 | 根本原因 | 解决方案 |
|---|---|---|
*** No rule to make target 'modules' |
KERNELDIR 指向的目录不是有效内核源码树 |
检查 KERNELDIR 路径,确认存在 Makefile 和 .config |
*** missing separator. Stop. |
Makefile 中使用了空格而非 Tab 缩进 | 将缩进全部替换为 Tab 字符 |
WARNING: could not find /lib/modules/.../modules.order |
内核模块依赖未更新 | 执行 depmod -a 更新模块依赖 |
error: implicit declaration of function |
缺少必要的头文件或内核 API 版本不匹配 | 检查内核版本,添加 #include 或使用兼容宏 |
modpost: "symbol" [module.ko] undefined! |
模块引用了未导出的内核符号 | 使用 EXPORT_SYMBOL 导出,或检查 Module.symvers |
内核版本兼容性: 不同内核版本的 Kbuild 行为可能存在细微差异。例如,Linux 5.x 之后对
MODULE_LICENSE 的检查更加严格。建议在 Makefile 中通过 VERSION 和 PATCHLEVEL 变量做版本判断,或参考内核源码 Documentation/kbuild/modules.rst 获取最新规范。