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加载模块
`lsmodgrep hello`查看模块是否已加载
`dmesgtail`查看 printk 日志
sudo rmmod hello卸载模块
`dmesgtail`查看卸载日志

执行顺序对应:

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 卸载。


Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐