03. Linux内核喂饭版教程
Part 3:Makefile 怎么把 hello.c 编译成 hello.ko
这一节解决一个问题:
为什么内核模块不能直接
gcc hello.c,而要用内核的 Kbuild 构建系统?
资料里也把 Makefile、动态加载、.ko 编译作为 Linux 内核模块入门的核心内容。
1. 先看目录结构
假设我们有一个最小模块目录:
hello_module/
├── hello.c
└── Makefile
hello.c 就是上一节的代码:
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk("hello module init\n");
return 0;
}
static void __exit hello_exit(void)
{
printk("hello module exit\n");
}
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);
现在问题是:怎么把它变成:
hello.ko
2. 最小 Makefile
先看完整 Makefile:
obj-m += hello.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
注意:
all:和clean:下面那两行前面必须是 Tab,不能是普通空格。
这是很多人第一次写 Makefile 最容易错的地方。
3. 逐行解释
3.1 obj-m += hello.o
obj-m += hello.o
这句最关键。
它的意思是:
把
hello.o编译成一个可加载内核模块。
最后会生成:
hello.ko
这里你容易疑惑:
为什么写 hello.o,不是 hello.c?
因为 Kbuild 的规则是:
hello.c
↓
hello.o
↓
hello.ko
你告诉 Kbuild:
我要把 hello.o 作为模块目标
Kbuild 会自动去找对应的:
hello.c
然后编译。
3.2 KDIR := /lib/modules/$(shell uname -r)/build
KDIR := /lib/modules/$(shell uname -r)/build
这句表示:
找到当前正在运行的 Linux 内核对应的构建目录。
uname -r 会输出当前内核版本,比如:
uname -r
可能得到:
5.15.0-91-generic
所以:
/lib/modules/$(uname -r)/build
实际就是:
/lib/modules/5.15.0-91-generic/build
这个目录里有当前内核的构建环境、头文件、配置和 Kbuild 规则。
3.3 PWD := $(shell pwd)
PWD := $(shell pwd)
这句表示:
当前模块源码所在目录。
比如你现在在:
/home/yu/hello_module
那么:
PWD = /home/yu/hello_module
3.4 $(MAKE) -C $(KDIR) M=$(PWD) modules
$(MAKE) -C $(KDIR) M=$(PWD) modules
这是整个 Makefile 里最重要的一行。
拆开看:
$(MAKE)
表示调用 make。
-C $(KDIR)
表示:
先切换到内核构建目录。
也就是进入:
/lib/modules/$(uname -r)/build
M=$(PWD)
表示:
当前这个目录是外部模块目录,请帮我编译这里面的模块。
modules
表示:
构建内核模块。
所以整句意思是:
进入当前内核的构建目录,
但模块源码在我当前目录,
请使用内核 Kbuild 系统帮我编译模块。
4. 为什么不能直接 gcc hello.c
普通 C 程序可以:
gcc main.c -o main
./main
因为它是用户态程序:
main()
↓
链接 glibc
↓
生成普通 ELF 可执行文件
但内核模块不行。
内核模块需要:
匹配当前内核版本
使用内核头文件
使用内核编译参数
处理内核符号
生成 .ko 模块文件
带上模块元信息
所以不能直接:
gcc hello.c -o hello
也不能:
./hello.ko
正确路径是:
hello.c
↓
Kbuild 编译
↓
hello.o
↓
modpost 处理模块信息
↓
hello.ko
↓
insmod 加载进内核
5. 编译命令
在目录下执行:
make
成功后可能生成很多文件:
hello.ko
hello.o
hello.mod
hello.mod.c
hello.mod.o
modules.order
Module.symvers
你先只关心:
hello.ko
它就是最终可以加载的内核模块。
6. 清理命令
执行:
make clean
会清理中间文件:
hello.o
hello.ko
hello.mod
hello.mod.c
hello.mod.o
modules.order
Module.symvers
7. 编译后怎么验证
完整命令链路是:
make
ls -l *.ko
sudo dmesg -C
sudo insmod hello.ko
lsmod | grep hello
dmesg | tail
sudo rmmod hello
dmesg | tail
对应含义:
| 命令 | 作用 | |
|---|---|---|
make | 编译生成 .ko | |
ls -l *.ko | 查看模块文件是否生成 | |
sudo dmesg -C | 清空旧内核日志 | |
sudo insmod hello.ko | 加载模块 | |
| `lsmod | grep hello` | 查看模块是否已加载 |
| `dmesg | tail` | 查看 printk 日志 |
sudo rmmod hello | 卸载模块 | |
| `dmesg | tail` | 查看卸载日志 |
执行顺序对应:
make
↓
生成 hello.ko
↓
insmod
↓
hello_init 执行
↓
dmesg 看到 init 日志
↓
rmmod
↓
hello_exit 执行
↓
dmesg 看到 exit 日志
8. 常见报错
8.1 找不到 build 目录
如果报:
/lib/modules/xxx/build: No such file or directory
说明当前内核头文件或构建环境没装。
Ubuntu/Debian 通常需要:
sudo apt install linux-headers-$(uname -r)
8.2 missing separator
如果报:
Makefile:7: *** missing separator. Stop.
大概率是:
Makefile 命令行前面用了空格
解决:
$(MAKE)那两行前面必须是 Tab。
8.3 Operation not permitted
如果:
sudo insmod hello.ko
报权限问题,常见原因:
没有 root 权限
Secure Boot 阻止未签名内核模块
内核安全策略限制
面试里一般不用展开 Secure Boot,知道模块加载需要 root 权限即可。
9. 和项目的关系
你项目里的内核模块也一样,不是普通 gcc 编译。
大体也是:
cpu_stat_collector.c
softirq_collector.c
↓
Makefile / Kbuild
↓
cpu_stat_collector.ko
softirq_collector.ko
↓
insmod 加载
↓
注册字符设备
↓
Worker open + mmap 读取
所以 hello 模块只是第一步。
真正项目链路是:
内核模块 .c
↓
Kbuild 编译成 .ko
↓
insmod 加载进内核
↓
module_init 注册字符设备 / 初始化缓冲区
↓
Worker mmap 读取 CPU / softirq 数据
↓
MetricCollector 聚合到 MonitorInfo
↓
gRPC Push 到 Manager
项目 Worker 端本来就是混合使用 /proc、字符设备、mmap 和 eBPF map 采集指标,然后聚合成 Protobuf MonitorInfo 上报 Manager。
10. 这一节最小记忆版
内核模块不能直接 gcc 编译。
要用 Linux 内核的 Kbuild 系统编译。
Makefile 核心是:
obj-m += hello.o
编译命令核心是:
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
-C 表示进入内核构建目录。
M=$(pwd) 表示当前目录是外部模块源码目录。
modules 表示构建模块。
最终生成 hello.ko。
hello.ko 不能直接运行,只能用 insmod 加载。
11. 面试版回答
面试官问:Linux 内核模块是怎么编译成 .ko 的?
你可以答:
内核模块不能像普通用户态程序一样直接用
gcc编译,因为它要匹配当前内核版本、使用内核头文件和 Kbuild 规则,并生成可被内核加载的.ko文件。外部模块通常在 Makefile 里写obj-m += hello.o,表示把hello.o作为可加载模块目标;然后通过make -C /lib/modules/$(uname -r)/build M=$(pwd) modules调用当前运行内核的构建系统来编译。最终会生成hello.ko,再通过insmod加载进内核,通过rmmod卸载。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)