4. 内核驱动模块编译基础(Kconfig/Makefile)

你可能会想,驱动代码写完了,怎么把它编进内核或者编成独立的 .ko 文件?这背后的核心就是 Kconfig 和 Makefile。说实话,这两样东西是内核编译系统的骨架,搞不懂它们,驱动根本跑不起来。

我个人习惯把 Kconfig 看作“菜单”,Makefile 看作“菜谱”。菜单决定了你做哪些菜,菜谱决定了怎么做。下面这张图能帮你快速建立整体印象:

Kconfig & Makefile 编译体系 Kconfig 定义配置选项(菜单) Makefile 描述编译规则(菜谱) make menuconfig 读取 Kconfig 生成 .config make modules 依据 .config 编译驱动 .ko 模块文件 关键关系 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。
  • 依赖关系和符号导出要提前规划好,别等到编译报错才去翻。

嗯,这些基础打牢了,后面碰到模块加载失败时,你才能分清到底是配置没选对、编译没编对,还是运行时出了问题。