模块的编译与加载:从Makefile到内核空间

说实话,模块编译这块儿,我当年刚入门时也踩过不少坑。明明代码写得没问题,一加载就报错,那种感觉真的很抓狂。后来才明白,内核模块的编译和普通程序完全是两码事。今天我就把这块儿掰开了揉碎了讲给你听。

一、Makefile编写——模块编译的“施工图纸”

写Makefile,说白了就是告诉内核构建系统:我要编译什么、用什么编译、编译成啥样。我个人习惯把Makefile分成两部分:变量定义编译规则

核心要点:内核模块的Makefile不是直接调用gcc,而是通过内核的Kbuild系统来编译。

来看一个最基础的Makefile模板:

# 最简单的单文件模块
obj-m := hello.o

# 如果有多个源文件,这样写
# hello-objs := main.o helper.o
# 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

这里有个关键点:obj-m 表示编译成模块,obj-y 才是编译进内核。我曾经见过有人把 obj-m 写成 obj-y,结果模块死活编译不出来——嗯,这种低级错误我也犯过。

小技巧:如果你在交叉编译,记得设置 ARCHCROSS_COMPILE 变量。比如:

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

二、模块编译过程——到底发生了什么?

你执行 make 的时候,背后其实发生了这么几件事:

  1. Kbuild系统读取你的Makefile,找到 obj-m 指定的模块
  2. 进入内核源码目录,加载内核的配置和头文件
  3. 编译你的 .c 文件,生成 .o 目标文件
  4. 链接成 .ko 内核模块文件

为什么会这么复杂?因为内核模块必须和当前内核版本、配置完全匹配。你想想看,内核在运行时管理着整个系统的资源,一个不匹配的模块加载进去,轻则崩溃,重则数据丢失。

下面这张图展示了整个编译流程:

内核模块编译流程 Makefile解析 读取obj-m/obj-y 进入内核源码 加载配置和头文件 编译.c文件 生成.o目标文件 链接成.ko模块 最终内核模块文件 ⚠ 版本检查:必须匹配 uname -r 查看当前内核 ✅ 生成.ko文件 可以加载到内核了

三、insmod/lsmod/rmmod——模块管理的三剑客

模块编译好了,怎么用?这三个命令你得烂熟于心。

3.1 insmod——加载模块

sudo insmod hello.ko

就这么简单?嗯,但前提是你的模块没有依赖问题。如果模块依赖其他模块,你得先加载依赖。我曾经在调试一个USB驱动时,忘了先加载usbcore模块,结果insmod直接报错:Unknown symbol

注意:insmod需要root权限。而且它不会自动处理依赖关系,这点和modprobe不同。

3.2 lsmod——查看已加载模块

lsmod

这个命令会列出所有已加载的模块,包括:

列名 含义 示例
Module 模块名称 hello
Size 模块占用内存大小 16384
Used by 被哪些模块或进程使用 0

你看,Used by 这一列特别重要。如果显示不为0,说明有其他模块或进程正在使用它,这时候你rmmod会失败。

3.3 rmmod——卸载模块

sudo rmmod hello

卸载时要注意:

  • 模块不能被其他模块依赖
  • 模块不能被用户空间程序使用
  • 模块的退出函数必须正确执行

调试技巧:如果rmmod失败,用 lsmod | grep 模块名 看看引用计数。我曾经遇到过模块引用计数不为0,查了半天发现是另一个驱动在调用它的接口。

四、模块依赖与自动加载——让系统帮你干活

实际项目中,很少有模块是孤立的。比如一个USB摄像头驱动,可能依赖 videodevusbcorev4l2 等多个模块。

模块依赖信息存在哪里?在 /lib/modules/$(uname -r)/modules.dep 文件里。这个文件由 depmod 命令生成。

# 手动更新依赖
sudo depmod -a

# 查看某个模块的依赖
modinfo -F depends hello.ko

自动加载怎么实现?有两种方式:

  1. /etc/modules:把模块名写进去,系统启动时自动加载
  2. udev规则:当检测到特定硬件时,自动加载对应驱动

实战经验:我在做嵌入式项目时,经常把模块放在 /lib/modules/$(uname -r)/extra/ 目录下,然后运行 depmod -a。这样 modprobe 就能自动处理依赖了。说白了,modprobeinsmod 智能得多,它会自动加载所有依赖。

最后送你一句话:模块编译和加载,看似简单,但每个细节都可能让你翻车。多动手、多踩坑,慢慢就熟练了。


专注资料整理