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 完成内核配置。

变量传递的完整路径如下图所示:

KERNELDIR 与 PWD 变量传递流程 驱动源码目录 PWD=$(shell pwd) M=$(PWD) Kbuild 编译系统 解析 obj-m / obj-y -C $(KERNELDIR) 内核源码树 .config / 规则 / 头文件 编译产物 .ko / .o / .mod.c 变量赋值规则 KERNELDIR ?= 路径(条件赋值)

3.3 make 命令执行流程:从用户输入到 .ko 生成

当用户在驱动目录下执行 make 时,背后发生了一系列递归调用。理解这个流程有助于定位编译错误和优化编译速度。

  1. 用户执行 make:Makefile 解析默认目标 default,该目标依赖于 $(MAKE) -C $(KERNELDIR) M=$(PWD) modules。
  2. 第一次递归(进入内核树):make 切换到 KERNELDIR 目录,读取顶层 Makefile。顶层 Makefile 设置全局编译规则、交叉编译工具链、架构标志等。
  3. 解析 M= 参数:Kbuild 检测到 M= 变量非空,进入“外部模块编译”模式。此时不会编译整个内核,而是只处理 M 指向的目录。
  4. 第二次递归(进入驱动目录):Kbuild 在 M=$(PWD) 目录下查找 Makefile 或 Kbuild 文件,读取其中的 obj-m 变量。
  5. 编译与链接:Kbuild 根据 obj-m 列表,依次执行 .c → .o 的编译,然后调用 $(LD) 链接生成 .ko 文件,同时生成 .mod.c 和 .mod.o 等辅助文件。
  6. 模块签名(可选):如果内核启用了模块签名(CONFIG_MODULE_SIG),Kbuild 还会对 .ko 进行签名。
性能提示: 每次执行 make 都会完整解析内核顶层 Makefile,这在大内核树中可能耗时数秒。如果只是修改了驱动源码,可以使用 make -C $(KERNELDIR) M=$(PWD) modules 跳过不必要的依赖检查。使用 make -jN 可以并行编译,显著加快速度。

完整的递归编译流程如下图所示:

make 命令执行流程(外部模块编译) ① 用户执行 make 解析 default 目标 ② 递归进入内核树 -C $(KERNELDIR) ③ 检测 M= 参数 进入外部模块模式 ④ 递归进入驱动目录 读取 obj-m 变量 ⑤ 编译 & 链接 生成 .ko / .mod.c 关键文件交互 • 内核顶层 Makefile • scripts/Makefile.build • .config(内核配置) • Module.symvers • 驱动目录下的 Makefile

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 获取最新规范。