4、Makefile与编译驱动:obj-y、obj-m与条件编译、驱动编译的Makefile实战

好,咱们今天聊聊驱动编译的“骨架”——Makefile。很多初学者觉得Makefile就是一堆变量和规则的堆砌,背下来就行了。但我不这么看。我个人习惯把Makefile看作是内核与驱动之间的“契约”,你写对了,内核就认你;写错了,编译都过不去。

说白了,内核的构建系统(Kbuild)就是靠解析Makefile来决定“谁进内核,谁当模块,谁滚蛋”。今天我就带你手撕几个核心概念:obj-y、obj-m、条件编译,以及一个完整的驱动Makefile实战。

4.1 obj-y 与 obj-m:内核的“选人标准”

内核源码树的每个子目录下,几乎都有一个Makefile。Kbuild系统会递归读取这些文件,根据你写的变量来决定编译行为。

  • obj-y:表示“Yes,我要把这个文件编译进内核镜像”。编译后生成 .o 文件,最终链接进 vmlinux。适合那些系统启动就必须存在的驱动,比如中断控制器、时钟、串口等。
  • obj-m:表示“Module,我要把它编译成可动态加载的模块”。生成 .ko 文件,可以在系统运行时用 insmod 或 modprobe 加载。

举个例子,假设你写了一个LED驱动 led_drv.c:

# 编译进内核
obj-y += led_drv.o

# 编译成模块
obj-m += led_drv.o

嗯,这里要注意:变量名后面是 .o,不是 .c。Kbuild会自动找到同名的 .c 文件来编译。你想想看,如果写成了 led_drv.c,系统会直接报错,找不到规则。

核心区别速查表:

变量编译目标生成文件加载方式典型场景
obj-y内核镜像.o → vmlinux开机自带核心驱动、必须驱动
obj-m可加载模块.koinsmod/modprobe外设驱动、调试驱动

4.2 条件编译:让Makefile“看人下菜碟”

实际项目中,同一个驱动源码可能需要在不同内核版本、不同硬件平台上编译。这时候条件编译就派上用场了。

Kbuild支持两种条件编译方式:

4.2.1 基于内核配置选项(Kconfig)

最常见的方式。你在Kconfig里定义了一个配置项 CONFIG_MY_DRIVER,然后在Makefile里判断:

obj-$(CONFIG_MY_DRIVER) += my_drv.o

如果 CONFIG_MY_DRIVER 的值是 y,就相当于 obj-y += my_drv.o;如果是 m,就是 obj-m;如果是空(未选中),这行就相当于不存在。

我在项目中遇到过一种情况:同一个驱动文件,在A平台要编进内核,在B平台要编成模块。怎么办?很简单,在Kconfig里分别定义两个不同的配置项,或者用 default y 和 default m 来控制。

4.2.2 基于Makefile变量或环境变量

有时候你不想依赖Kconfig,比如临时调试一个补丁。可以用Makefile的 ifdef 或 ifeq:

ifdef CONFIG_DEBUG_DRIVER
    obj-m += debug_drv.o
endif

ifeq ($(PLATFORM), rk3588)
    obj-y += rk_specific.o
else
    obj-m += common_drv.o
endif

我曾经在调试一个USB网卡驱动时,为了快速对比不同内核版本的行为,就用 ifdef 临时切换编译选项。嗯,这招在开发阶段特别实用,但提交代码前记得清理掉。

小技巧: 如果你想让一个文件同时支持 y 和 m,但又不想写两遍,可以用 obj-$(CONFIG_MY_DRIVER) 这种写法。Kbuild会自动根据配置值展开。

4.3 驱动编译的Makefile实战

光说不练假把式。咱们写一个完整的驱动Makefile,我把它叫做“万能模板”,你拿去改改就能用。

# 驱动名称
obj-m := hello_drv.o

# 如果驱动由多个源文件组成
# hello_drv-objs := main.o sub.o utils.o

# 指定内核源码树路径
# 如果是交叉编译,需要指定 ARCH 和 CROSS_COMPILE
KERNELDIR ?= /lib/modules/$(shell uname -r)/build

# 当前目录
PWD := $(shell pwd)

# 默认目标
all:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

# 清理
clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) clean

# 安装模块到当前系统
install:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install

# 调试:打印内核版本
debug:
    @echo "Kernel source: $(KERNELDIR)"
    @echo "Build modules for: $(shell uname -r)"

这个Makefile有几个关键点:

  • -C $(KERNELDIR):切换到内核源码目录,读取顶层的Makefile。
  • M=$(PWD):告诉Kbuild,我要编译的模块源码在当前目录。
  • modules:目标,表示编译所有 obj-m 指定的模块。

你想想看,如果没有 -C 和 M=,你只能手动调用gcc,还得自己链接内核符号表,那得多痛苦?Kbuild把这些都封装好了。

避坑指南: 我曾经犯过一个低级错误——在Makefile里直接写死了内核路径,结果换了一台机器编译就报错。后来我改用 ?= 赋值,允许外部传入 KERNELDIR,这才灵活起来。

4.4 多文件驱动的编译技巧

如果你的驱动由多个 .c 文件组成,比如 main.c、sub.c、utils.c,不能直接写:

obj-m := my_drv.o
my_drv-objs := main.o sub.o utils.o

这里 my_drv-objs 是Kbuild约定的特殊变量,它告诉系统:虽然模块名叫 my_drv.ko,但它是由这三个 .o 文件链接而成的。

我习惯把这种多文件驱动的Makefile写成这样:

obj-m := my_drv.o
my_drv-y := main.o sub.o utils.o

# 如果某些文件只在特定配置下编译
my_drv-$(CONFIG_MY_DRV_DEBUG) += debug.o
my_drv-$(CONFIG_MY_DRV_FEATURE_X) += feature_x.o

注意这里用了 my_drv-y 而不是 my_drv-objs。两者效果一样,但 -y 更符合Kbuild的“条件编译”风格。说白了,-y 就是“无条件编译”,-$(CONFIG_XXX) 就是“有条件编译”。

4.5 知识体系总览

为了让你更直观地理解整个流程,我画了一张图:

驱动编译Makefile核心流程 驱动源码 .c / .h Makefile (obj-y / obj-m / 条件编译) Kbuild 内核构建系统 (make -C /lib/modules/.../build M=$PWD) obj-y → 编译进内核 生成 .o → 链接入 vmlinux obj-m → 编译成模块 生成 .ko → insmod 动态加载 vmlinux (内核镜像) .ko (可加载模块) 条件编译介入点

这张图把整个流程串起来了:从源码开始,经过Makefile的“选人”(obj-y/obj-m),再由Kbuild系统统一调度,最终生成内核镜像或可加载模块。条件编译可以在Makefile层和Kconfig层两个地方介入。

4.6 写在最后

Makefile这东西,说难不难,说简单也不简单。我见过不少开发者,驱动写得飞起,但Makefile只会复制粘贴,出了问题一脸懵。其实你只要理解了 obj-y 和 obj-m 的本质区别,再掌握条件编译的两种方式,90%的驱动编译场景你都能应付。

嗯,下次你写驱动时,不妨试试自己手写Makefile,别总从网上抄。抄来的东西,永远不是你的。

一句话总结: obj-y 是“娶进门”,obj-m 是“住隔壁”。条件编译就是“看情况决定娶不娶”。Makefile就是那个“媒人”,Kbuild是“婚礼策划公司”。

可可精品资料库