insmod 实操指南——从写一个内核模块到加载运行

本文讲清 insmod:它干什么、内核模块(.ko)是什么、insmodmodprobe 的区别、模块签名这个最常见的坑,并给一个可直接照抄跑通的 hello world 模块从编写→编译→加载→验证→卸载全流程。

前置:ring0/ring3、内核态概念见本人《x86 体系结构基础》篇。内核模块就是跑在 ring0、和内核同地址空间的代码。


一、insmod 是什么、内核模块又是什么

**内核模块(Loadable Kernel Module, LKM)**是一段可动态加载到内核地址空间、跑在 ring0 的代码,扩展内核功能而不需重启、不需重编内核。文件后缀 .ko(kernel object)。典型用途:设备驱动、文件系统、网络协议、安全 hook(如 alcor-chaos 的 libnvml-hijack/libixml-hijack 就是劫持库的内核态配合件)。

insmod(insert module)是用户态命令,把一个 .ko 文件加载进内核。它本质是调一次 init_module(2) / fini_module(2) 系统调用(或 finit_module 带 fd 的变体),让内核:

  1. 把模块二进制拷进内核地址空间。
  2. 重定位、解析符号依赖(模块用到的内核导出符号 EXPORT_SYMBOL)。
  3. 调用模块的 init 函数——init 返回 0 成功,返回非 0 则加载失败回滚。
  4. 注册到 /proc/modules/sys/module/
1
2
3
用户态 insmod foo.ko ──init_module() syscall──► 内核
内核: 拷贝.ko → 重定位/解析符号 → 调 module_init() → 成功则常驻 ring0
卸载: rmmod foo ──► 调 module_exit() → 释放

面试金句:“insmod 把 .ko 动态加载进内核 ring0 地址空间、跑模块 init、常驻内核;它就是 init_module 系统调用的用户态壳。内核模块 = 编译进内核功能的’热插拔版’,不重启扩展内核。”


二、insmod vs modprobe

insmod modprobe
输入 单个 .ko 文件路径 模块名(不带 .ko、不带路径)
依赖解析 不解析,缺依赖直接报错 自动从 /lib/modules/$(uname -r) 解析依赖、按序加载
来源 当前目录任意 .ko modules.dep 索引的标准模块目录
用途 加载自己编的、没装到标准目录的 .ko 加载发行版自带驱动(如 modprobe nfsd
  • 自己编译的 .ko(在当前目录)用 insmod ./hello.ko
  • 发行版已安装的模块modprobe <name>,它会读 modules.dep 把依赖一并装上。
  • insmod 不会自动装依赖——若你的模块 depends on 别的模块,要先 insmod 依赖。modprobe 解决这个。

辅助命令:

  • lsmod:列出已加载模块(读 /proc/modules)。
  • modinfo hello.ko:看模块元信息(license、version、依赖、parm 参数)。
  • rmmod hello:卸载模块(调 exit)。
  • dmesg | tail:看模块 printk 输出。

三、模块签名坑(最常见的"加载失败"原因)

启用了 Secure Boot 的系统(多数云主机/笔记本)内核要求模块有签名才能加载。报错形如:

1
2
3
insmod: ERROR: could not insert module hello.ko: Required key not available
# 或 dmesg:
PKCS7 signature not in .ko

因为 Secure Boot 链下,内核只信被 MOK(Machine Owner Key)签过的模块。解决:

  1. 生成自签名 MOKmokutil 导入你的公钥到内核信任链(一次性,需重启确认)。
  2. .ko 签名:scripts/sign-file sha256 privkey.pem cert.der hello.ko
  3. 或在测试机关闭 Secure Boot(BIOS 里关)最省事。

另一类坑是 ABI/版本不匹配.ko 是为某个内核版本编译的(用 vermagic 字符串标记),换内核版本加载会被拒(invalid module format / vermagic mismatch)。所以模块必须在当前运行的内核的 headers 下编译。容器/不同发行版交叉编译要特别注意 uname -r 对齐。

面试金句:“insmod 失败两大常见因:Secure Boot 要求模块签名(Required key not available,要 MOK 签名或关 Secure Boot);vermagic 不匹配(模块为别的内核版本编的,要当前内核 headers 重编)。”


四、实操:一个可跑通的 hello world 模块

在一台你能 sudo、且最好关了 Secure Boot的 Linux 测试机(VM 最方便)。生产机别玩。

4.1 装内核 headers(前提)

模块要用当前运行内核的 headers 编译:

1
2
3
4
5
sudo apt-get update
sudo apt-get install -y build-essential linux-headers-$(uname -r) kmod

# 确认 headers 装好(应有这个目录)
ls /lib/modules/$(uname -r)/build

容器里编译内核模块坑多(容器不共享内核源码树、且容器内一般没内核 headers),建议在宿主机/VM 上做。

4.2 写模块 hello.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// hello.c — 最小可加载内核模块
#include <linux/module.h> // 所有模块都要
#include <linux/kernel.h> // pr_info / KERN_INFO
#include <linux/init.h> // __init / __exit 宏

MODULE_LICENSE("GPL"); // 必填:声明 license,否则内核报 tainted 警告
MODULE_AUTHOR("you");
MODULE_DESCRIPTION("a hello world LKM demo");
MODULE_VERSION("0.1");

// 可传参: insmod hello.ko whom=World (见 4.6)
static char *whom = "World";
module_param(whom, charp, 0644); // charp=字符指针, 0644=sysfs 可读
MODULE_PARM_DESC(whom, "who to greet");

// 加载时调用, 返回 0 成功
static int __init hello_init(void)
{
pr_info("Hello, %s! (insmod loaded me)\n", whom);
pr_info(" running in kernel ring0, jiffies=%lu\n", jiffies);
return 0; // 非 0 则加载失败、自动回滚
}

// 卸载时调用
static void __exit hello_exit(void)
{
pr_info("Goodbye, %s. (rmmod unloaded me)\n", whom);
}

module_init(hello_init);
module_exit(hello_exit);

要点:

  • MODULE_LICENSE("GPL") 必填,否则用 GPL-only 导出符号会失败、内核被标记 tainted。
  • __init/__exit 提示编译器把这段放专用 section,加载后 init 段可释放省内存。
  • module_param 让模块接受参数,insmod hello.ko whom=World 传值,可在 /sys/module/hello/parameters/whom 读。
  • pr_info = printk(KERN_INFO ...),输出到内核环形缓冲区,用 dmesg 看。

4.3 写 Makefile

1
2
3
4
5
6
7
8
9
10
11
12
# Makefile — 用内核 kbuild 体系编译模块
obj-m += hello.o

# 当前内核源码/headers 树
KDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

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

clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
  • obj-m += hello.o 告诉 kbuild 把 hello.c 编成模块 hello.ko(文件名取自源文件名)。
  • make -C $(KDIR) M=$(PWD) modules:进内核树跑 kbuild 的 modules 目标,在当前目录产出 .ko
  • 多文件模块用 hello-objs := a.o b.o

4.4 编译

1
2
3
4
5
6
7
8
9
10
11
make
# 产出: hello.ko (还有 .mod.c/.o/.cmd 等中间件)

# 看元信息
modinfo hello.ko
# filename: hello.ko
# license: GPL
# version: 0.1
# depends:
# vermagic: 6.x.0 ... ← 必须和 uname -r 一致
# parm: whom: who to greet

4.5 加载 + 验证 + 卸载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 1) 加载(默认 whom=World)
sudo insmod ./hello.ko
# 或带参:
sudo insmod ./hello.ko whom=Kernel

# 2) 确认已加载
lsmod | grep hello # 出现一行 hello ...
cat /proc/modules | grep hello

# 3) 看输出
sudo dmesg | tail
# Hello, Kernel! (insmod loaded me)
# running in kernel ring0, jiffies=...

# 4) 看模块参数(sysfs)
cat /sys/module/hello/parameters/whom # Kernel

# 5) 卸载(名字不带 .ko)
sudo rmmod hello
sudo dmesg | tail
# Goodbye, Kernel. (rmmod unloaded me)

到这一步,一个完整的"写模块→编译→insmod→rmmod"闭环就跑通了。

4.6 常见报错与排查

报错 原因 解决
Required key not available / PKCS7 signature not in .ko Secure Boot 要求签名 关 Secure Boot,或 MOK 签名 .ko(§三)
Invalid module format / vermagic mismatch .ko 为别的内核版本编 装当前 uname -r 的 headers 重编 make
Unknown symbol in module / dmesg disagrees about version of symbol 用了未导出/版本不符的内核符号 modinfonm 看依赖;只 EXPORT_SYMBOL 导出的符号才可用
Operation not permitted 非 root / 容器无 CAP_SYS_MODULE sudo;容器要给 --privilegedCAP_SYS_MODULE(生产别给)
init 返回非 0 模块 init 失败 看 dmesg 自打日志,修逻辑
Module already in use(rmmod) 模块被引用计数不为 0 先停掉使用它的进程/驱动,lsmod 看 Used by 列

排查命令:

1
2
3
4
modinfo hello.ko                    # vermagic / depends / parm
lsmod | grep hello # 是否加载、被谁引用
sudo dmesg | tail -30 # 模块 printk + 内核加载日志
cat /sys/module/hello/parameters/* # 运行时参数

五、进阶:模块能做什么、安全提醒

模块能干的事(ring0 权限):

  • 注册字符/块设备(alloc_chrdev_region + cdev_add)—— 自定义设备驱动。
  • 注册文件系统(register_filesystem)、网络协议、netfilter hook。
  • 实现 /proc//sys 接口(proc_create/kobject_create)和用户态交互。
  • hook 系统调用表 / kprobe(需 EXPORT_SYMBOL,部分被限制)。
  • alcor-chaos 风格的内核态劫持/故障注入配合件。

安全提醒:

  • 模块 = 完全信任的内核态代码,写错会 kernel panic、死机、数据损坏。生产加载要签名 + 审计。
  • 容器给 CAP_SYS_MODULE 等于给宿主内核权,生产容器绝不给
  • 别在生产机 insmod 未审计 .ko,等于执行任意 ring0 代码。
  • 模块卸载前提是引用计数为 0;强行 rmmod --force 可能崩。

面试金句:“内核模块有 ring0 全权,能注册设备/文件系统/netfilter、hook 系统调用,写错就 panic;所以生产模块必须签名+审计,容器绝不给 CAP_SYS_MODULE。insmod 本质是给内核执行任意 ring0 代码的入口,故权限管控是命门。”


六、面试速答清单

Q1:insmod 干什么、原理?

.ko 内核模块动态加载进内核 ring0 地址空间。它调 init_module/finit_module 系统调用,内核拷贝二进制、重定位、解析 EXPORT_SYMBOL 依赖、调模块 init,成功则常驻。模块 = 编译进内核功能的"热插拔版",不重启扩展内核。

Q2:insmod 和 modprobe 区别?

insmod 加载单个 .ko、不解析依赖(缺依赖就报错),用于自己编的没装到标准目录的模块。modprobe 输入模块名、自动从 /lib/modules/$(uname -r)modules.dep 解析依赖按序加载,用于发行版自带模块。卸载都可用 rmmod,但 modprobe 有 -r 递归卸依赖。

Q3:insmod 报 Required key not available 怎么办?

Secure Boot 开启时内核只信 MOK 签过的模块。要么关 Secure Boot(测试机),要么生成 MOK 用 mokutil 导入公钥、用内核 scripts/sign-file.ko 签名。另一类 invalid module format 是 vermagic 不匹配——用当前 uname -r 的 headers 重编。

Q4:写一个最小内核模块要哪些要素?

#include <linux/module.h>MODULE_LICENSE("GPL")(必填否则 tainted/符号失效)、module_init(fn) + module_exit(fn)、init 返回 0 成功否则回滚。可选 module_param 收参、__init/__exit 段优化。Makefile 用 kbuild:obj-m += hello.o + make -C /lib/modules/$(uname -r)/build M=$(PWD) modules

Q5:怎么验证模块加载成功?

lsmod | grep hellocat /proc/modules 看是否列出;dmesg | tail 看 init 的 pr_info 输出;/sys/module/hello/parameters/ 看运行时参数。卸载 rmmod hello 后 exit 的 printk 也会进 dmesg。

Q6:内核模块有哪些安全风险?

模块跑在 ring0 拥有内核全权,能注册设备/文件系统/netfilter、hook syscall,写错 kernel panic、死机、数据损坏。所以生产模块必须签名+审计;容器给 CAP_SYS_MODULE 等于给宿主内核任意执行权,生产容器绝不给。insmod 本质是"给内核注入任意 ring0 代码"的入口,权限管控是命门。

Q7:模块卸载失败 Module already in use

模块引用计数不为 0(有进程/驱动在用)。lsmod 的 “Used by” 列看谁引用,先停掉使用者再 rmmod。别用 rmmod --force,可能崩。模块 init 里建的资源(proc/sysfs、设备号)要在 exit 里对应释放,否则卸载也失败。


七、一张图收口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
编写:  hello.c (MODULE_LICENSE + module_init/exit + pr_info) + Makefile (obj-m + kbuild)

▼ make -C /lib/modules/$(uname -r)/build M=$(PWD) modules
编译: hello.ko (vermagic 必须匹配 uname -r)

▼ sudo insmod ./hello.ko (或 insmod hello.ko whom=Kernel)
加载: 内核 init_module() → 拷贝/重定位/解析 EXPORT_SYMBOL → 调 hello_init → 常驻 ring0

验证: lsmod | grep hello ; dmesg | tail ; /sys/module/hello/parameters/whom

▼ sudo rmmod hello
卸载: 调 hello_exit → pr_info "Goodbye" → 释放 (引用计数为0才行)
坑: Secure Boot 要 MOK 签名 / vermagic 不匹配要重编 / 用未导出符号 Unknown symbol
权限: 需 root + CAP_SYS_MODULE; 模块=ring0 全权, 生产签名+审计, 容器不给

主线一句话:insmod 把 .ko 经 init_module 系统调用加载进内核 ring0、跑模块 init、常驻内核,是"不重启扩展内核"的热插拔入口;自己编的用 insmod(不解析依赖)、发行版用 modprobe(自动解析依赖);最大坑是 Secure Boot 要 MOK 签名、vermagic 要匹配当前内核 headers;模块有 ring0 全权,生产必须签名审计、容器绝不给 CAP_SYS_MODULE。 把加载原理、insmod/modprobe 区别、签名坑、实操全流程讲顺,insmod 这关就稳了。


参考资料

  • Linux Kernel Module Programming Guide(Documentation/admin-guide/modules.rst
  • man: insmod(8), modprobe(8), rmmod(8), lsmod(8), modinfo(8), init_module(2), finit_module(2)
  • Documentation/admin-guide/module-signing.rst(模块签名)
  • 与本人《x86 体系结构基础》《操作系统接口》篇交叉对照