【linux内核专栏 09】内核模块与 kbuild

本篇定位 :你 BSP 天天写驱动,驱动要么编进内核要么做成模块(.o)。模块是 Linux 区别于裸机的关键------运行时加载/卸载代码,不改内核重编重启。本篇把 LKM 机制、Makefile/Kconfig、模块加载流程、符号导出、参数、依赖讲透。读完能写独立 .ko、能看懂内核 Makefile、能调模块加载失败。
模块是 Linux "动态扩展"的根基,你裸机没这能力

FreeRTOS/裸机:加功能 = 改源码 + 重编 + 重烧重启。Linux:模块(.ko)运行时 insmod 加载,rmmod 卸载,不用重启。这是宏内核保持单体内核性能的同时获得"类微内核扩展性"的关键。BSP 写驱动,90% 做成模块(开发快,出问题 rmmod 即卸),稳定后可编进内核。


一、内核模块(LKM)是什么

1.1 模块的本质

  • 一个 .ko 文件(内核对象,kernel object)
  • ELF 格式,包含:代码段(.text)、数据段、符号表、重定位信息
  • 运行时被内核动态链接进内核地址空间
  • 加载后和内核其他代码同等权限(S 态,可访问一切)

1.2 模块 vs 编进内核

方式 怎么做 何时用
编进内核(=y) make menuconfig 选 CONFIG_XXX=y,编译进 vmlinux 核心/必用/早期要用(根文件系统驱动)
模块(=m) 选 CONFIG_XXX=m,编成 .ko,运行时加载 多数驱动,可后装/可卸/省启动内存
不编(=n) 选 CONFIG_XXX=n 不需要

1.3 模块的生命周期

复制代码
源码(.c) 
  → Kbuild 编译 → .ko
  → insmod/modprobe 加载 
    → 内核分配内存,重定位,链接符号
    → 调 module_init → 模块活
  → 使用
  → rmmod 卸载
    → 调 module_exit → 清理
    → 释放内存

1.4 对照裸机

裸机/FreeRTOS Linux 模块
加功能 改源码重编重烧 insmod .ko
重启 必须 不用
代码权限 全权 全权(S 态)
卸载 不能 rmmod
开发调试 烧录慢 加载快,迭代快

模块让你 BSP 开发飞起来

你裸机改驱动要重编重烧,慢。Linux 写驱动做 .ko,insmod 加载,出错 rmmod 卸载改了再加载------秒级迭代。稳定后再决定编进内核还是留模块。这是 Linux BSP 开发效率的核心来源。


二、最简单的模块

2.1 hello.c

c 复制代码
#include <linux/module.h>
#include <linux/init.h>

static int __init hello_init(void) {
    printk(KERN_INFO "Hello, kernel module!\n");
    return 0;   // 0 成功,负数失败
}

static void __exit hello_exit(void) {
    printk(KERN_INFO "Goodbye, kernel module!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");          // 必须,否则内核警告/不能用 GPL 符号
MODULE_AUTHOR("XXX");
MODULE_DESCRIPTION("A simple hello module");
MODULE_VERSION("1.0");

2.2 关键点

  • __init:标记初始化函数,加载完进 .init.text,加载后释放(省内存)
  • __exit:标记退出函数,只有 rmmod 时才调;若编进内核(=y)则不编译(永远不卸载)
  • module_init/module_exit:注册加载/卸载入口
  • MODULE_LICENSE("GPL"):必须 ,否则:① 内核警告 "tainted";② 不能调 EXPORT_SYMBOL_GPL 的符号
  • printk:内核打印(不是 printf),走 dmesg

2.3 __init 的内存优化

c 复制代码
static int __init hello_init(void) { ... }
  • __init 把函数放 .init.text 段
  • 模块加载完,.init 段释放(初始化函数只调一次,留着浪费)
  • 编进内核的 __init 在启动后由 free_initmem() 释放
  • 只对"加载时调一次"的函数用,常驻函数不能标

三、Makefile(kbuild)

3.1 最简 Makefile

makefile 复制代码
obj-m += hello.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
  • obj-m:要编成模块的目标(=m);obj-y 编进内核
  • -C .../build:进内核源码树(用它的 kbuild 系统)
  • M=$(PWD):模块源码在外部目录(外部模块)

3.2 多文件模块

makefile 复制代码
obj-m += mydriver.o
mydriver-objs := main.o irq.o buffer.o   # mydriver.ko 由三个文件组成

3.3 kbuild 变量

变量 含义
obj-m 编成模块
obj-y 编进内核
obj-n 不编
xxx-objs 复合模块的组成文件
ccflags-y 额外编译选项

3.4 编译流程

复制代码
make
  → 内核 Kbuild 系统
  → hello.c → hello.o(GCC)
  → hello.o → hello.ko(链接 + 加 modpost 生成符号信息)

3.5 内核内模块 vs 外部模块

树内(in-tree) 外部(out-of-tree)
位置 内核源码 drivers/... 任意目录
Kconfig 有 无(自己写 Makefile)
谁用 内核官方驱动 你 BSP 自研驱动
编译 make modules make -C 内核 M=外部

嵌入式视角:BSP 驱动通常先外部,后树内

开发期:驱动放外部目录,insmod 迭代快。产品化:把驱动合进内核源码树(drivers/...),配 Kconfig/Makefile,编进 BSP SDK。这是 BSP 工作流:外部开发 → 树内合入。


四、模块加载机制(insmod 内部)

4.1 insmod 做了什么

复制代码
insmod hello.ko
  → 用户态 insmod 工具
  → 读 hello.ko 到内存
  → syscall init_module(mod_image, len, params)
  → 内核:
      1. 校验 ELF 头/魔数
      2. 分配模块内存(module 结构 + 代码/数据)
      3. 重定位(把符号地址填进去)
      4. 解决未定义符号(查内核符号表 + 已加载模块)
      5. 调 module_init()
      6. 挂到全局模块链表

4.2 符号解析

  • 模块可调:① 内核导出的符号(EXPORT_SYMBOL);② 已加载模块导出的符号;③ 自身符号
  • 未定义符号解析失败 → 加载失败(Unknown symbol)
  • 看失败:dmesg 或 modinfo

4.3 modprobe vs insmod

insmod modprobe
输入 .ko 全路径 模块名(自动找)
依赖 不处理 自动装依赖
配置 无 读 /etc/modprobe.d/
用途 调试/手动 生产
bash 复制代码
insmod /lib/modules/.../hello.ko        # 直接加载
modprobe hello                          # 自动找 + 装依赖
modprobe -r hello                       # 卸载(含依赖)

4.4 卸载(rmmod)

bash 复制代码
rmmod hello            # 卸载(模块没在用才能卸)
lsmod                  # 看已加载模块
  • 模块被使用(如设备打开)→ 卸载失败
  • 卸载调 module_exit,清理资源(注销设备/释放内存/解中断)
  • module_exit 必须彻底清理,否则内存泄漏/资源占用

module_exit 要和 module_init 对称

init 里注册了什么,exit 必须注销什么:注册设备→注销,申请中断→释放,申请内存→释放,申请资源→释放资源。你裸机没有"卸载"概念(代码一直在),Linux 模块能卸,卸载清理是必修课。漏清理 = 卸载后内核崩(悬挂指针)。


五、符号导出(EXPORT_SYMBOL)

5.1 为什么需要导出

模块 A 要调模块 B 的函数,或模块要调内核函数------必须导出才能被其他模块用。内核默认不导出所有符号(防模块乱用内部接口)。

5.2 两种导出

c 复制代码
// 在提供符号的代码里
void my_func(int x) { ... }
EXPORT_SYMBOL(my_func);          // 任何模块可用

int my_gpl_func(void) { ... }
EXPORT_SYMBOL_GPL(my_gpl_func);  // 只有 GPL 许可模块可用
  • EXPORT_SYMBOL:任何许可模块可用
  • EXPORT_SYMBOL_GPL:只有 MODULE_LICENSE("GPL") 的模块可用(防闭源白嫖)
  • 导出后符号进内核符号表(/proc/kallsyms)

5.3 看符号

bash 复制代码
cat /proc/kallsyms | grep my_func
# 00000000abcdef00 r my_func  [hello]   # 模块 hello 的符号

cat /proc/kallsyms | grep printk
# ffffff8001234560 T printk             # 内核符号

5.4 模块间依赖

复制代码
模块 B 依赖模块 A 的符号 → 加载顺序:A 先,B 后
modprobe 自动处理(insmod 不处理,报 Unknown symbol)
卸载顺序:B 先,A 后(被依赖的不能先卸)

EXPORT_SYMBOL 是模块间协作的接口

BSP 把公用驱动(如平台驱动底座)的接口 EXPORT_SYMBOL,上层驱动调。这是 Linux 模块化设计。注意 GPL 导出 :你若用闭源模块,只能调 EXPORT_SYMBOL(非 GPL)的符号。


六、模块参数

6.1 传参给模块

c 复制代码
#include <linux/moduleparam.h>

static int debug = 0;
module_param(debug, int, 0644);              // 参数名,类型,权限
MODULE_PARM_DESC(debug, "Enable debug (0/1)");

static char *name = "default";
module_param(name, charp, 0644);

static int ports[4] = {0};
static int num_ports;
module_param_array(ports, int, &num_ports, 0644);  // 数组参数

6.2 加载时传

bash 复制代码
insmod hello.ko debug=1 name="test"
modprobe hello debug=1 name="test"

# 持久化(开机自动加载带参)
echo "options hello debug=1" > /etc/modprobe.d/hello.conf

6.3 运行时改

权限含 0220/0644 的参数,可在 /sys/module/<name>/parameters/ 改:

bash 复制代码
echo 1 > /sys/module/hello/parameters/debug
cat /sys/module/hello/parameters/debug

6.4 对照 FreeRTOS 配置

FreeRTOS Linux 模块
配置 编译宏 模块参数
改 重编 运行时改
持久 烧录 /etc/modprobe.d

嵌入式视角:模块参数替代编译宏

FreeRTOS 调参要改 config 宏重编。Linux 模块参数运行时传,不改代码不重编。调试利器:加 debug=1 打印详情,调好关掉。产品化用 modprobe.d 持久化。


七、模块信息与工具

7.1 modinfo

bash 复制代码
modinfo hello.ko
# filename:       hello.ko
# license:        GPL
# author:         Zhaoyuhan
# description:    A simple hello module
# version:        1.0
# srcversion:     ABC123...
# depends:        
# vermagic:       6.6.0 SMP mod_unload modversions riscv64
  • vermagic:版本魔术,模块编译时的内核版本/配置,必须和运行内核匹配,否则加载失败
  • depends:依赖的其他模块

7.2 常用工具

工具 作用
lsmod 列已加载模块(读 /proc/modules)
insmod 加载 .ko
rmmod 卸载
modprobe 智能加载(带依赖)
modprobe -r 智能卸载
modinfo 看 .ko 信息
depmod 生成模块依赖索引(modules.dep)

7.3 /proc/modules 和 /sys/module

bash 复制代码
cat /proc/modules
# hello 16384 0 - Live 0x... (O)

ls /sys/module/
# hello/ 里面有 parameters/ sections/ ...

八、模块的版本检查

8.1 vermagic

  • 模块编译时记录内核版本/SMP/抢占等配置,生成 vermagic
  • 加载时内核比对 vermagic,不匹配拒绝加载
  • 防"用别的内核的模块"导致崩

8.2 强制加载(危险)

bash 复制代码
insmod --force hello.ko   # 跳过 vermagic 检查
  • 极度危险(符号地址可能不对 → 崩)
  • 只在确认兼容时用

8.3 CONFIG_MODVERSIONS

  • 开启后,每个导出符号有 CRC 校验
  • 模块记录用到的符号 CRC,加载时比对
  • 比 vermagic 更细(符号级)

九、模块的依赖与 depmod

9.1 depmod 生成索引

bash 复制代码
depmod -a   # 扫 /lib/modules/$(uname -r)/,生成 modules.dep
  • modules.dep 记录"模块→依赖"映射
  • modprobe 读它决定加载顺序

9.2 模块依赖文件

复制代码
# /lib/modules/$(uname -r)/modules.dep
/kernel/drivers/net/mydriver.ko: /kernel/drivers/net/mylib.ko
  • mydriver 依赖 mylib,modprobe mydriver 会先装 mylib

十、内核 Kconfig(配置系统)

10.1 Kconfig 语法

树内模块用 Kconfig 描述配置选项,menuconfig 里显示:

复制代码
# drivers/my/Kconfig
config MY_DRIVER
    tristate "My device driver"
    depends on ARCH_RISCV
    help
      This is my device driver.
      M: module, Y: built-in, N: exclude.
  • tristate:三态(y/m/n)
  • depends on:依赖其他配置
  • select:选中本项自动选某项
  • help:帮助文本
bash 复制代码
make menuconfig        # 进菜单
# 选 MY_DRIVER = m
make                   # 编内核
make modules_install   # 装 .ko 到 /lib/modules/

10.3 .config

  • menuconfig 的选择存 .config
  • CONFIG_MY_DRIVER=m → Makefile 读到 obj-m
  • CONFIG_MY_DRIVER=y → obj-y

10.4 BSP 的配置工作流

复制代码
1. 写驱动 .c
2. 加 Kconfig(配 tristate 选项)
3. 加 Makefile(obj-$(CONFIG_MY_DRIVER) += my.o)
4. make menuconfig 选 =m
5. make modules → my.ko
6. 部署 .ko,insmod

Kconfig/Makefile 是你 BSP 必会

BSP 加驱动,要改三处:① 源码 .c;② Kconfig(让人能在 menuconfig 选);③ Makefile(告诉 kbuild 怎么编)。这是树内驱动标准流程,面试必问,实战必做。


十一、模块开发调试技巧

11.1 printk 分级

c 复制代码
printk(KERN_EMERG   "panic 级\n");    // 0 最高
printk(KERN_ALERT   "alert\n");        // 1
printk(KERN_CRIT    "crit\n");         // 2
printk(KERN_ERR     "error\n");        // 3
printk(KERN_WARNING "warn\n");         // 4
printk(KERN_NOTICE  "notice\n");       // 5
printk(KERN_INFO    "info\n");         // 6
printk(KERN_DEBUG   "debug\n");        // 7 最低
  • 控制台只显示 >= console_loglevel 的
  • dmesg 看全部
  • 调模块:printk(KERN_INFO "xxx") + dmesg | tail

11.2 pr_xxx / dev_xxx(推荐)

c 复制代码
pr_info("hello\n");            // = printk(KERN_INFO ...)
pr_err("fail: %d\n", ret);
pr_debug("debug\n");           // 默认不打印,开 DEBUG 才打

dev_info(&pdev->dev, "probe ok\n");   // 带设备信息
dev_err(&pdev->dev, "probe fail\n");

11.3 加载失败排查

bash 复制代码
insmod my.ko
# Failed: ...

dmesg | tail
# hello: Unknown symbol some_func
# → some_func 没导出,或模块没写 GPL

# vermagic 不匹配
# hello: version magic '5.15.0' should be '6.6.0'
# → 模块和内核版本不符,重编

11.4 模块崩溃

  • 模块崩 = 内核崩(模块在内核态)
  • 崩了看 oops(16 篇),addr2line 定位
  • 开发时开 lockdep/KASAN(07 篇)

嵌入式视角:模块崩和你裸机崩一样

你裸机崩 → 死机。Linux 模块崩 → 内核 oops(可能死可能活)。模块代码和内核同等权限,bug 会拖垮整个内核。所以模块要严谨:① 资源对称(init/exit);② 中断上下文不睡;③ 指针校验。你裸机的严谨习惯直接用。


十二、DM(设备模型)与模块的关系

模块是代码载体,设备模型(10 篇)是组织框架:

复制代码
模块(.ko) ──加载──> 注册驱动到 bus
                  ──probe──> 绑定设备
                  ──rmmod──> 检查设备是否在用
                            在用 → 拒绝卸载
                            没用 → remove → 卸载
  • 模块卸载前,内核检查"模块是否被引用"(设备在用 = 引用 > 0)
  • 引用计数由设备模型自动管(try_module_get/module_put)

设备模型管模块引用计数

你不用手动管"模块被几个设备用"------设备模型的 try_module_get/module_put 自动增减引用。模块卸载时引用 > 0,内核拒绝卸载,防悬挂。这是 10 篇设备模型对模块的保护。


十三、对照总表

概念 FreeRTOS/裸机 Linux 模块
加功能 改源码重编重烧 insmod .ko
运行时加载 ❌ ✅
卸载 ❌ rmmod
代码权限 全权 全权(S 态)
函数导出 链接器自动 EXPORT_SYMBOL
配置参数 编译宏 module_param
配置系统 无 Kconfig/menuconfig
构建系统 Makefile/IDE Kbuild
崩溃影响 死机 内核 oops/死
迭代速度 慢(烧录) 快(insmod)

十四、本篇小结

  • 模块(.ko):ELF,运行时动态链接进内核,insmod 加载/rmmod 卸载,免重启
  • 模块 vs 编进:=m 模块(灵活)/=y 编进(必需)/=n 不编
  • 模块入口:module_init/module_exit,__init/__exit 标记省内存
  • MODULE_LICENSE("GPL") 必须,否则不能用 GPL 符号
  • Makefile :obj-m += xxx.o,多文件用 xxx-objs,-C 内核 M=外部
  • 加载流程:insmod → init_module syscall → ELF 校验 → 重定位 → 符号解析 → module_init
  • modprobe 比 insmod 智能(自动依赖),生产用 modprobe
  • EXPORT_SYMBOL/EXPORT_SYMBOL_GPL:导出符号给其他模块用,GPL 导出只给 GPL 模块
  • module_param:运行时配参,/sys/module/.../parameters/ 可改
  • Kconfig :tristate/depends on/select,menuconfig 选,存 .config
  • vermagic:版本魔术,模块和内核必须匹配(否则加载失败)
  • module_exit 必须对称清理 init 注册的资源
  • 模块崩 = 内核崩,严谨度同裸机

速查表

想干啥 用什么
写模块入口 module_init(fn) + module_exit(fn)
标记 init/exit 省内存 __init / __exit
声明许可 MODULE_LICENSE("GPL")
编模块 Makefile obj-m += hello.o
多文件模块 xxx-objs := a.o b.o
加载模块 insmod hello.ko / modprobe hello
卸载 rmmod hello / modprobe -r hello
列已加载 lsmod
看模块信息 modinfo hello.ko
导出符号 EXPORT_SYMBOL(fn) / EXPORT_SYMBOL_GPL(fn)
看内核符号 cat /proc/kallsyms | grep xxx
模块参数 module_param(name, type, perm)
改运行时参数 echo val > /sys/module/xxx/parameters/name
持久加载带参 /etc/modprobe.d/xxx.conf 里 options
加 Kconfig config XXX \n tristate "desc"
加 Makefile obj-$(CONFIG_XXX) += xxx.o
看加载失败原因 dmesg | tail
打印 pr_info/pr_err/dev_info

💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。

如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。


相关推荐
代码山河1 小时前
Java的前世今生:从Oak语言到Java 23的发展史
java·学习·架构·教程·面向对象·项目
新鲜势力呀1 小时前
PHP 文件上传系统实战:从上传卡顿到分片上传 + 秒传 + 云存储架构完整优化方案
开发语言·架构·php
流浪0011 小时前
Linux系统篇43——线程(八) 线程的数据不一致问题,从 ticket-- 的非原子性说起
linux·操作系统·线程·线程安全·锁
Είναι η κοπέλα1 小时前
WSL2 部署 AI 开发环境:内核级 Linux + GPU 直通
linux·运维·人工智能
其实防守也摸鱼1 小时前
网安自测题:掌握核心知识点的实用练习
linux·运维·服务器·前端·数据库·sql·xss
木白CPP1 小时前
[QNX] 深入理解 Resource Manager 接口设计
linux·服务器·数据库
AlfredZhao1 小时前
别再 rm 日志了:Linux 下安全清空日志文件的正确姿势
linux
星恒随风1 小时前
Linux开发工具详解(二):Git版本控制、GitHub协作与GDB调试实战
linux·笔记·git·学习·github
Mortalbreeze1 小时前
MySQL 基础篇(五):数据操作基础 —— CRUD
linux·服务器·数据库·mysql