4. 内核驱动模块编译基础(Kconfig/Makefile)
你可能会想,驱动代码写完了,怎么把它编进内核或者编成独立的 .ko 文件?这背后的核心就是 Kconfig 和 Makefile。说实话,这两样东西是内核编译系统的骨架,搞不懂它们,驱动根本跑不起来。
我个人习惯把 Kconfig 看作“菜单”,Makefile 看作“菜谱”。菜单决定了你做哪些菜,菜谱决定了怎么做。下面这张图能帮你快速建立整体印象:
4.1 Kconfig:配置的大门
Kconfig 文件定义了一个个配置选项。你在执行 make menuconfig 时看到的菜单树,就是内核从各个目录的 Kconfig 里解析出来的。
核心规则:每个驱动模块必须有一个对应的 Kconfig 配置选项,否则无法通过内核配置系统启用它。
4.1.1 一个实际的 Kconfig 例子
我们来看 USB OTG 驱动里的一段 Kconfig:
config USB_OTG_FSM
tristate "USB OTG Finite State Machine support"
depends on USB
help
Say Y here to enable support for the USB OTG
Finite State Machine (FSM). This is required for
OTG-capable devices that need to switch between
host and peripheral modes.
If unsure, say N.
config USB_OTG_FSM_DEBUG
bool "OTG FSM debug support"
depends on USB_OTG_FSM
help
Enable debug output for OTG FSM driver.
这里有几个要点:
- tristate 表示三态选项:Y(编译进内核)、M(编译为模块)、N(不编译)。驱动模块通常用 tristate,因为它可以生成 .ko 文件。
- bool 只有 Y/N 两种选择,不能编译为模块。适合纯功能开关。
- depends on 说明依赖关系。比如
USB_OTG_FSM_DEBUG依赖USB_OTG_FSM,父选项不选中的话,子选项不会显示。
我的经验:刚开始写 Kconfig 时,我经常忘记写 depends on,结果在 menuconfig 里选项总是灰掉或者莫名其妙出现。后来我养成了一个习惯:每写完一个选项,先模拟运行 make menuconfig 看一眼,确认层级和依赖都正确。
4.1.2 Kconfig 的关键语法
| 关键字/语法 | 含义 | 常见用法 |
|---|---|---|
| tristate | 三态:Y/M/N | 驱动模块首选 |
| bool | 布尔:Y/N | 功能开关/调试选项 |
| depends on | 依赖项 | 缺了依赖选项不显示 |
| select | 反向选择 | 选中本选项时自动选中另一个 |
| default | 默认值 | Y/M/N,生产环境常用 n |
| help | 帮助文本 | 给开发者看的说明 |
我曾经踩过的坑:有一个驱动我用 select 强制依赖了一个不兼容的 USB 控制器驱动,结果编译虽然通过了,运行时却反复崩溃。最后发现 select 只保证了编译依赖,不保证运行时兼容性。所以能用 depends on 就别用 select,除非你能非常确定两者完全兼容。
4.2 Makefile:编译的剧本
Kconfig 负责“要不要编”,Makefile 负责“怎么编”。没 Makefile,再好的 Kconfig 也编不出 .ko 文件。
4.2.1 驱动模块 Makefile 基本结构
一个 USB OTG 驱动的 Makefile 可能长这样:
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_USB_OTG_FSM) += otg_fsm.o
obj-$(CONFIG_USB_OTG_FSM_DEBUG) += otg_fsm_debug.o
otg_fsm-objs := otg_core.o otg_state.o otg_timer.o
我解释一下:
- obj-$(CONFIG_USB_OTG_FSM):这是核心。当 CONFIG_USB_OTG_FSM 被设置为 y 或 m 时,这个obj会包含对应的目标文件。设置成 y 则编译进内核,m 则编译成 .ko 模块。
- otg_fsm-objs:如果模块由多个源文件组成,用这个变量列出所有 .o 文件。编译系统会自动将它们链接成一个 .ko 文件。
- 如果你只有一个源文件,比如
otg_core.c,那么直接写obj-$(CONFIG_XXX) += otg_core.o就够了,不需要额外定义 xxx-objs。
4.2.2 关键的编译变量
驱动开发中经常会用到这些变量,尤其是当你的驱动依赖其他内核模块时:
| 变量名 | 作用 | 示例 |
|---|---|---|
| ccflags-y | 添加额外编译标志 | ccflags-y += -DDEBUG |
| CFLAGS_xxx.o | 仅对某个文件添加标志 | CFLAGS_otg_core.o += -O2 |
| ldflags-y | 链接器标志 | 通常用不到,但 debug 时可用 |
| KBUILD_EXTRA_SYMBOLS | 引用其他模块导出的符号 | 多模块依赖时必须设置 |
4.2.3 依赖管理:不只是头文件
你想想看,如果驱动 A 依赖驱动 B 里的一个函数,但 Makefile 没处理好,编译时找不到符号怎么办?
常见的做法是用 KBUILD_EXTRA_SYMBOLS 来指定依赖模块的符号文件:
KBUILD_EXTRA_SYMBOLS += $(srctree)/drivers/usb/otg/Module.symvers
另外,还有一点容易被忽视:如果驱动 B 没有被选中(不管是 y 还是 m),它的符号根本不会生成,你就算加了 KBUILD_EXTRA_SYMBOLS 也没用。所以 Kconfig 里的 depends on 要写对。
我个人习惯:每次写 Makefile 时,都会在文件开头加一行注释,写明这个驱动依赖哪些外部模块,以及对应的 Kconfig 选项。这样半年后自己回来看,或交给别人维护,都不容易出错。说白了,就是给自己降低返工成本。
4.3 常见的编译错误与应对
这些错误我在刚开始写驱动时都遇到过,写出来帮你少走弯路:
| 错误现象 | 可能原因 | 解决办法 |
|---|---|---|
No rule to make target |
源文件名和 Makefile 里的不一致 | 检查 .o 名是否和 .c 名对应 |
undefined symbol |
缺少依赖或 KBUILD_EXTRA_SYMBOLS 没设置 | 检查 Kconfig 依赖,并确认符号已导出 |
multiple definition |
同一个函数在多个 .c 文件里定义了 | 使用 static 或改成不同函数名 |
| 模块无法 insmod | 内核版本不匹配或模块格式错误 | 用 modinfo 查看模块信息确认 |
4.4 小结
Kconfig 和 Makefile 是内核驱动编译的入口。Kconfig 决定了你的驱动能不能被选上,Makefile 决定了被选上后能不能编出来。写的时候记住几条:
- 每个驱动模块的 Kconfig 选项用 tristate,除非你不打算编成模块。
- Makefile 里的 obj-y 和 obj-m 由 Kconfig 的 CONFIG_XXX 自动切换,别手动改。
- 多文件模块用 xxx-objs 列出所有 .o。
- 依赖关系和符号导出要提前规划好,别等到编译报错才去翻。
嗯,这些基础打牢了,后面碰到模块加载失败时,你才能分清到底是配置没选对、编译没编对,还是运行时出了问题。