Linux驱动 2026年9月9日 45 分钟

驱动模块编译进内核的完整方法

目录 方法一:外部模块编译(最灵活) 驱动代码放在内核源码外部,单独 make 编译,生成 .ko 文件。 目录结构 M...
本文目录 展开目录

目录


方法一:外部模块编译(最灵活)

驱动代码放在内核源码外部,单独 make 编译,生成 .ko 文件。

目录结构

/home/eh/linux/xuexi1/my_driver/01char_driver/驱动环境测试/
├── moudle.c
└── Makefile

Makefile

export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
obj-m +=moudle.o
KDIR := /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
PWD := $(shell pwd)

modules:
    $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

编译和加载

make                          # 编译 .ko
insmod moudle.ko             # 加载
rmmod moudle                 # 卸载

适用场景

驱动开发调试阶段,改代码后快速重新编译测试。


方法二:内置模块编译(编译进内核镜像)

驱动代码放在内核源码内部,编译时链接进 zImage。

核心概念:Linux Kbuild 构建系统

Linux 内核使用 Kbuild 系统进行配置和编译,由三个关键组件构成:

  1. Kconfig – 配置选项定义(用户在 menuconfig 中看到的菜单项)
  2. Makefile – 编译规则(告诉构建系统如何编译、何时编译)
  3. .config – 最终生成的配置文件(记录哪些功能被启用)

步骤 1:创建内核源码中的驱动目录

mkdir -p /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char/my_driver

创建物理目录结构,还未与内核构建系统连接。


步骤 2:复制驱动源码

cp /home/eh/linux/xuexi1/my_driver/01char_driver/驱动环境测试/moudle.c \
   /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char/my_driver/

此时目录结构:

drivers/char/my_driver/
├── moudle.c        # 驱动源码
├── Kconfig         # (待创建) 配置选项定义
└── Makefile        # (待创建) 编译规则

步骤 3:创建 Kconfig(配置选项定义)

vi /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char/my_driver/Kconfig
config MY_HELLO
    tristate "Hello driver for IMX6ULL"
    default y
    help
      A simple hello world character device driver.

配置项解析:

字段含义
config MY_HELLO定义配置符号 MY_HELLO,对应变量 CONFIG_MY_HELLO
tristate三态选项:y 编译进内核 / m 编译为模块 / n 不编译
default y默认编译进内核
helpmenuconfig 中显示的帮助文本

作用:

  • 运行 make menuconfig 时,这个配置项会出现在菜单中供用户选择
  • 用户的选择会写入 .config 文件,如 CONFIG_MY_HELLO=y

步骤 4:创建子目录 Makefile(定义编译规则)

vi /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char/my_driver/Makefile
obj-$(CONFIG_MY_HELLO) += hello.o

Makefile 变量展开机制:

用户选择.config 内容Makefile 展开后效果
[*] (编译进内核)CONFIG_MY_HELLO=yobj-y += hello.o编译 moudle.c → hello.o → 链接进内核
<M> (编译为模块)CONFIG_MY_HELLO=mobj-m += hello.o编译 moudle.c → hello.ko 模块
< > (不编译)# CONFIG_MY_HELLO is not setobj- += hello.o不编译(空规则)

编译过程:

moudle.c  →  [gcc -c] → hello.o  →  [链接] → drivers/char/my_driver/built-in.o
                                              ↓
                                   合并到 drivers/char/built-in.o
                                              ↓
                                   合并到 drivers/built-in.o
                                              ↓
                                   链接进 vmlinux → zImage

步骤 5:在父级 Makefile 中引用(递归编译入口)

vi /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char/Makefile

在文件末尾添加:

obj-$(CONFIG_MY_HELLO) += my_driver/

这一行的核心作用:

**告诉 Kbuild 系统”进入 my_driver/ 子目录,寻找并执行该目录下的 Makefile”**。

本质上是递归编译的入口指令,类似于:

# 如果 CONFIG_MY_HELLO=y,则执行
cd my_driver && make

如果没有这一行会怎样?

  • Kbuild 系统 不会自动扫描子目录
  • 即使 my_driver/Makefile 存在、CONFIG_MY_HELLO=y,也 不会编译
  • 这是 显式递归 设计,而非自动遍历

递归编译流程:

drivers/char/Makefile (父级)
    ↓ 读取到:obj-y += my_driver/
    ↓ Kbuild 执行:进入 my_driver/ 子目录
    ↓
drivers/char/my_driver/Makefile (子级)
    ↓ 读取到:obj-y += hello.o
    ↓ Kbuild 执行:gcc -c moudle.c -o hello.o
    ↓
生成 hello.o 并链接到 built-in.o
  1. 顶层 Makefile 读取 .config
    ↓
  2. 解析 drivers/Makefile
    发现:obj-y += char/
    ↓
  3. 进入 drivers/char/Makefile
    发现:obj-y += my_driver/ (因为 CONFIG_MY_HELLO=y)
    ↓
  4. 进入 drivers/char/my_driver/Makefile
    发现:obj-y += hello.o
    ↓
  5. 编译 moudle.c
    arm-linux-gnueabihf-gcc -c moudle.c -o hello.o
    ↓
  6. 链接到内核
    将 hello.o 合并到 drivers/char/built-in.o
    ↓
  7. 最终链接
    drivers/char/built-in.o → drivers/built-in.o → vmlinux → zImage

步骤 6:在父级 Kconfig 中引用(配置菜单入口)

vi /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/drivers/char/Kconfig

找到合适的位置添加:

source "drivers/char/my_driver/Kconfig"

source 指令的作用:

将子目录的 Kconfig 文件 包含 到父级 Kconfig 中,类似 C 语言的 #include。

效果: 运行 make menuconfig 时:

Device Drivers --->
    Character devices --->
        [*] Hello driver for IMX6ULL  ← 这个选项来自 my_driver/Kconfig

如果没有 source 会怎样?

  • CONFIG_MY_HELLO 配置项 不会出现在 menuconfig 菜单中
  • 用户无法通过图形界面选择该驱动
  • 即使手动在 .config 中添加 CONFIG_MY_HELLO=y,也会因为符号未定义而被 make oldconfig 忽略

Kconfig 树状包含关系:

arch/arm/Kconfig (顶层)
    ↓ source "drivers/Kconfig"
drivers/Kconfig
    ↓ source "drivers/char/Kconfig"
drivers/char/Kconfig
    ↓ source "drivers/char/my_driver/Kconfig"
drivers/char/my_driver/Kconfig
    ↓ 定义 config MY_HELLO

两个关键配置的对比

文件位置作用系统缺少后果
source "drivers/char/my_driver/Kconfig"将子目录配置项注入 menuconfig 菜单配置系统用户看不到选项,无法启用驱动
obj-$(CONFIG_MY_HELLO) += my_driver/告诉构建系统进入子目录编译编译系统即使配置启用,也不会编译驱动

两者独立但配合工作:

  • source 让用户 能选择
  • obj-y += 让编译器 能找到

步骤 7:配置并编译

cd /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek

# 方式A:通过 menuconfig 图形界面
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 导航到:Device Drivers -> Character devices -> [*] Hello driver for IMX6ULL
# 按 Y 键选择,保存退出

# 方式B:直接修改 .config
echo "CONFIG_MY_HELLO=y" >> .config
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- oldconfig  # 更新依赖

# 编译内核
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j16

完整编译流程详解

阶段 1:配置阶段(生成 .config)

make menuconfig

用户操作:

Device Drivers --->
    Character devices --->
        [*] Hello driver for IMX6ULL  ← 选中

生成 .config 文件:

CONFIG_MY_HELLO=y

阶段 2:编译阶段(Kbuild 递归构建)

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j16

Kbuild 系统工作流程:

1. 顶层 Makefile 读取 .config
   发现:CONFIG_MY_HELLO=y
   ↓
2. 解析 drivers/Makefile
   发现:obj-y += char/
   ↓ 进入 drivers/char/
   ↓
3. 解析 drivers/char/Makefile
   读取:obj-$(CONFIG_MY_HELLO) += my_driver/
   展开为:obj-y += my_driver/  (因为 CONFIG_MY_HELLO=y)
   ↓ 进入 drivers/char/my_driver/
   ↓
4. 解析 drivers/char/my_driver/Makefile
   读取:obj-$(CONFIG_MY_HELLO) += hello.o
   展开为:obj-y += hello.o
   ↓
5. 编译驱动源码
   执行:arm-linux-gnueabihf-gcc -c moudle.c -o hello.o
   ↓
6. 链接到 built-in.o
   执行:ld -r hello.o -o my_driver/built-in.o
   合并:my_driver/built-in.o → drivers/char/built-in.o
   合并:drivers/char/built-in.o → drivers/built-in.o
   ↓
7. 最终链接内核
   ld drivers/built-in.o fs/built-in.o ... -o vmlinux
   ↓
8. 压缩生成镜像
   objcopy + gzip → arch/arm/boot/zImage

关键编译命令示例:

# 编译驱动源码
arm-linux-gnueabihf-gcc -Wp,-MD,drivers/char/my_driver/.hello.o.d \
  -nostdinc -isystem /usr/lib/gcc-cross/arm-linux-gnueabihf/7/include \
  -I./arch/arm/include -Iarch/arm/include/generated -Iinclude \
  -D__KERNEL__ -mlittle-endian -Wall -Wundef -Wstrict-prototypes \
  -fno-strict-aliasing -fno-common -O2 -fno-dwarf2-cfi-asm \
  -c -o drivers/char/my_driver/hello.o drivers/char/my_driver/moudle.c

# 链接到 built-in.o
arm-linux-gnueabihf-ld -r -o drivers/char/my_driver/built-in.o \
  drivers/char/my_driver/hello.o

# 合并到上层 built-in.o
arm-linux-gnueabihf-ld -r -o drivers/char/built-in.o \
  drivers/char/my_driver/built-in.o drivers/char/其他.o ...

验证编译结果

检查编译产物

# 检查驱动目录的编译产物
ls drivers/char/my_driver/
# 应该看到:hello.o  built-in.o  moudle.c  Makefile  Kconfig

# 检查符号是否在内核中
arm-linux-gnueabihf-nm vmlinux | grep hello_init
# 输出:c0a12340 t hello_init

# 检查内核镜像大小(应该略微增大)
ls -lh arch/arm/boot/zImage

运行时验证

# 内核启动后,驱动自动初始化(不需要 insmod)
dmesg | grep -i hello
# 输出:[    1.234567] Hello driver initialized

配置变量的三种状态对比

# 用户在 menuconfig 中的不同选择,产生完全不同的编译行为:

# 情况 1:编译进内核 (=y)
CONFIG_MY_HELLO=y
    ↓
obj-y += my_driver/     # 进入子目录
obj-y += hello.o        # 编译并链接进 vmlinux
    ↓
drivers/char/my_driver/built-in.o → 合并到内核镜像
内核启动时自动加载,无需 insmod

# 情况 2:编译为模块 (=m)
CONFIG_MY_HELLO=m
    ↓
obj-m += my_driver/     # 进入子目录
obj-m += hello.o        # 编译为独立模块
    ↓
生成 drivers/char/my_driver/hello.ko
需要手动 insmod hello.ko 加载

# 情况 3:不编译
# CONFIG_MY_HELLO is not set
    ↓
obj- += my_driver/      # 空规则,不进入子目录
    ↓
moudle.c 完全不参与编译

适用场景

选择适用场景
=y (编译进内核)核心驱动、启动必需的驱动(如存储、串口)
=m (编译为模块)可选驱动、调试阶段、需要动态加载卸载的驱动
不编译不需要的驱动、目标硬件不支持的驱动

补充:编译但不链接进内核的配置方式

在内核开发中,有时需要编译某些代码生成 .o 文件,但不将其链接进最终的内核镜像。

方式 1:extra-y(编译但不链接)

extra-y += hello.o

效果:

  • 会编译 moudle.c → hello.o
  • hello.o 不会链接进 built-in.o
  • hello.o 不会进入最终的内核镜像

用途:

  • 生成中间文件供其他目标使用(如链接脚本)
  • 工具类代码(如内核构建工具)
  • 测试编译但暂不启用的代码

实际示例:

# arch/arm/kernel/Makefile
extra-y += vmlinux.lds  # 生成链接脚本但不链接进内核

方式 2:lib-y(归档到库但按需链接)

lib-y += hello.o

效果:

  • 编译 moudle.c → hello.o
  • 链接到 lib.a 静态库中
  • 只有被其他代码引用时才会最终链接进内核
  • 如果没有符号被引用,则不会进入最终镜像

用途:

  • 通用库函数(如 lib/string.o)
  • 按需链接,减小内核体积

实际示例:

# lib/Makefile
lib-y += crc32.o  # 只有当代码调用 crc32() 时才链接进内核

方式 3:自定义目标(编译为独立产物)

# 自定义编译规则
my-tool: my-tool.o
    $(CC) -o my-tool my-tool.o

my-tool.o: my-tool.c
    $(CC) -c my-tool.c -o my-tool.o

# 不加入 obj-y,所以不会链接进内核

效果:

  • 编译生成 my-tool.o 和可执行文件 my-tool
  • 但不参与内核链接
  • 常用于构建内核辅助工具

编译配置对比总结

配置方式是否编译是否链接进内核用途场景
obj-y✅✅普通内核代码,编译并链接
obj-m✅❌(生成 .ko)可加载模块
obj-n❌❌完全不编译
extra-y✅❌编译但不链接,生成中间文件
lib-y✅⚠️(按需)编译到库,只有被引用才链接
自定义目标✅❌编译为独立工具或测试程序

**最常见的”编译但不链接”场景是使用 extra-y**,用于生成构建过程需要但不应该链接进最终内核的中间产物。


方法三:通过 menuconfig 图形界面配置

启动 menuconfig

cd /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
make menuconfig

操作步骤

  1. 按 / 键,输入 MY_HELLO,搜索配置项位置
  2. 记住路径:Location: -> Device Drivers -> Character devices -> My Driver
  3. 用方向键导航到该位置
  4. 按 Y 键 — 编译进内核([*])
  5. 或按 M 键 — 编译为模块(<M>)
  6. 保存退出:Esc Esc → Yes

验证配置

grep "CONFIG_MY_HELLO" .config
# 输出应为:CONFIG_MY_HELLO=y  或  CONFIG_MY_HELLO=m

方法四:直接修改 .config 文件

添加配置项

# 编辑 .config
vi /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/.config

在文件末尾添加:

CONFIG_MY_HELLO=y

用 sed 命令行修改

# 编译进内核 (=y)
sed -i '/# CONFIG_MY_HELLO is not set/s/^# //' .config
sed -i 's/CONFIG_MY_HELLO=m/CONFIG_MY_HELLO=y/' .config

# 编译为模块 (=m)
sed -i 's/CONFIG_MY_HELLO=y/CONFIG_MY_HELLO=m/' .config

# 禁用 (=n)
sed -i '/CONFIG_MY_HELLO/d' .config

追加新配置

# 如果 .config 中没有 CONFIG_MY_HELLO
echo "CONFIG_MY_HELLO=y" >> .config

# 更新依赖
make oldconfig

方法五:通过 defconfig 预置配置

从现有 defconfig 修改

cd /home/eh/linux/xuexi1/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek

# 基于 IMX6ULL 默认配置
make imx_v7_defconfig

# 添加自定义驱动配置
echo "CONFIG_MY_HELLO=y" >> .config

# 更新配置(解析依赖)
make oldconfig

# 编译
make -j4

导出当前配置为 defconfig

# 把当前 .config 导出为 defconfig(精简格式,只保留非默认选项)
make savedefconfig

# 生成的文件
mv defconfig arch/arm/configs/imx_v7_mydefconfig

方法六:通过 Kbuild 条件编译(不修改 Kconfig)

有时候只需要根据现有配置决定是否编译,不需要新增配置项。

方式 A:依赖已有配置项

# 假设 IMX6ULL 已经有 CONFIG_SOC_IMX6ULL
obj-$(CONFIG_SOC_IMX6ULL) += hello.o

方式 B:根据架构编译

obj-$(CONFIG_ARCH_MXC) += hello.o

方式 C:使用 ifdef 条件编译

ifdef CONFIG_MY_HELLO
obj-y += hello.o
endif

方式 D:根据模块参数

obj-$(CONFIG_HELLO_ENABLE) += hello.o

方法七:通过 modprobe 自动加载

如果驱动是 .ko 模块,可以让内核自动加载。

在 /etc/modules 中注册

# 开发板上执行
echo "moudle" >> /etc/modules

创建模块依赖配置

# 更新模块依赖
depmod -a

# 查看依赖关系
cat /lib/modules/4.1.15/modules.dep | grep moudle

systemd 自动加载

# 创建 systemd 服务
vi /etc/systemd/system/hello.service
[Unit]
Description=Hello Driver
After=basic.target

[Service]
Type=oneshot
ExecStart=/sbin/insmod /path/to/moudle.ko
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
systemctl enable hello
systemctl start hello

方法八:通过内核引导参数加载

在 U-Boot 或内核启动参数中指定模块路径:

# U-Boot 中设置
setenv bootargs "root=/dev/nfs nfsroot=... modules=module_name"

或在开发板启动后:

# 自动从指定目录加载所有模块
modprobe -a moudle

总结对比

方法配置方式编译位置是否修改内核源码适用场景
外部模块外部 Makefile外部目录不修改开发调试
内置模块Kconfig + Makefile内核源码内修改正式集成
menuconfig图形界面–不修改源码快速配置
直接改 .config文本编辑器–不修改源码快速测试
defconfig预置配置–不修改源码批量部署
条件编译已有配置项内核不修改源码复用配置
modprobe 自动/etc/modules外部目录不修改自动加载

开发阶段推荐流程

开发阶段:外部模块(方法一)
    ↓ 代码稳定后
集成阶段:内置模块(方法二)+ menuconfig(方法三)
    ↓ 配置确定后
发布阶段:defconfig(方法五)固化配置

补充:驱动 Makefile 中的 CONFIG 判断

在 Makefile 中,CONFIG_XXX 会被替换为实际的值(y/m/n):

# 基础写法
obj-$(CONFIG_MY_HELLO) += hello.o

# 多条件判断
obj-$(CONFIG_MY_HELLO) += hello.o
obj-$(CONFIG_MY_HELLO_DEBUG) += hello_debug.o

# 子目录递归
obj-$(CONFIG_MY_HELLO) += subdir/

# 子目录的 Makefile
obj-$(CONFIG_MY_HELLO) += hello_core.o
obj-$(CONFIG_MY_HELLO) += hello_io.o

展开说明:

  • CONFIG_MY_HELLO=y → obj-y += hello.o → 编译进内核
  • CONFIG_MY_HELLO=m → obj-m += hello.o → 编译为模块
  • CONFIG_MY_HELLO=n → 不生成任何编译指令 → 不编译

补充:驱动代码中的条件编译

#include <linux/module.h>
#include <linux/kernel.h>

#ifdef CONFIG_MY_HELLO_VERBOSE
#define DPRINTK(fmt, args...) printk(KERN_DEBUG "[HELLO] " fmt, ##args)
#else
#define DPRINTK(fmt, args...) do {} while(0)
#endif

#ifdef CONFIG_MY_HELLO
static int __init hello_init(void)
{
    printk(KERN_INFO "Hello driver initialized\n");
    DPRINTK("Verbose: module init\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Hello driver removed\n");
}

module_init(hello_init);
module_exit(hello_exit);
#endif

MODULE_LICENSE("GPL");
MODULE_AUTHOR("ErHu");
MODULE_DESCRIPTION("Hello driver for IMX6ULL");
上一篇 V4L2子系统10-USB摄像头枚举过程

下一篇 为什么Atomic-Context不能睡眠-任务栈模型极度详细版

为什么 Atomic Context 不能睡眠——中断使用被中断任务栈时的通用分析 本文只讨论通用的 Linux/类 U...