写这个标题其实挺贴切的:一个.c文件,经过几行命令,就变成能被内核“捡起来”运行的.ko模块,整个过程确实有点“变身”的意思。刚接触内核模块的人,最容易卡住的地方就是编译这一步——明明照着书敲了代码,一make全是报错;要么编译过了,insmod又提示版本不对。这些问题十有八九不是代码本身写错了,而是没搞懂内核模块的编译机制。
这篇文章我就从实际动手的角度,把“代码到可加载模块”这条路完整走一遍,把背后的原理、Makefile的写法、常见的坑一次性说清楚。适合刚入门Linux内核开发、或者被模块编译折磨过的同学参考。
1. 模块的本质与编译的特殊性
1.1 模块为什么不能像普通程序一样编译
先看一个最基础的事实:普通C程序编译,用的是gcc hello.c -o hello,链接的是glibc,生成的是ELF可执行文件,运行在用户态。内核模块完全不是这套逻辑,它虽然也是C语言写的,但运行在内核态,不能链接glibc,不能调用printf、malloc这类用户态库函数,它的“运行时环境”是内核本身。
这意味着模块编译时,需要一份完整的内核头文件、内核编译配置,以及一套专属于内核的编译规则——也就是内核源码树里的Kbuild体系。说白了,模块不是独立编译的,它是“依附”于某个内核版本、按该内核的配置来编译的。这也是为什么模块有严格的版本匹配要求:你在5.15内核上编译出来的.ko,拿到6.1内核上大概率加载不了。
打个比方:普通程序像是用一套通用模具做出来的标准零件,放哪台机器上都能用;内核模块则是根据某台机器的具体型号、具体配置单独浇铸的零件,换一台机器可能就装不进去了。
1.2 可加载模块到底“加载”了什么
从本质上讲,insmod hello.ko做的事,是把编译好的二进制代码复制进内核地址空间,然后执行模块里的初始化函数(module_init指定的函数),把模块注册到内核的某个子系统里。rmmod则反过来,执行清理函数(module_exit指定的函数),把资源释放掉。
所以“变身术”的核心,就是把人类可读的C源码,变成内核可执行的二进制代码,并且保留足够的信息(如许可证声明、依赖关系、版本魔数),让内核愿意接受它。这个目标决定了编译过程的所有细节——为什么需要-D__KERNEL__、为什么需要-DMODULE、为什么需要-fno-pic等等,后面都会遇到。
1.3 编译前的三条硬性前提
在实际敲命令之前,先确认三件事,省得后面白折腾:
- 确认内核版本:
uname -r,比如输出5.15.0-91-generic。后续所有操作都要围绕这个版本。 - 确认内核头文件已安装:Ubuntu/Debian下是
linux-headers-$(uname -r),CentOS/RHEL/Fedora下是kernel-devel。没装的话,/lib/modules/$(uname -r)/build这个软链接是不存在的,编译会直接挂掉。 - 确认工具链可用:需要
gcc、make。注意gcc版本不能太新或太老,与内核版本相差过大可能出现奇怪的语法错误。比如老内核(2.6.x)用gcc 12编译,基本必挂。
这三条是地基,地基不稳后面所有步骤都白搭。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 搭建编译环境与准备代码骨架
2.1 最小可用的模块代码
先写一个最简单的模块,功能就两件事:加载时打印一句话,卸载时再打印一句话。这段代码是所有内核模块的“Hello World”,也是验证整个编译链路的试金石。
c复制#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, kernel module!\n");
return 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");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple hello module");
几个细节值得说道说道:
__init和__exit这两个宏,会把对应的函数放到特定的代码段里。__init函数在内核初始化完成后,占用的内存会被释放掉——这是内核节省内存的一个小技巧。printk是内核态的“printf”,但它不是直接输出到终端,而是输出到内核日志缓冲区。用dmesg或者journalctl -k才能看到。MODULE_LICENSE("GPL")不是摆设。如果声明非GPL,某些内核符号(以EXPORT_SYMBOL_GPL导出的)就无法使用,还会在内核日志里看到module license 'unspecified' taints kernel的警告。建议写模块一律声明GPL。return 0代表初始化成功;返回负数代表失败,比如return -ENOMEM表示内存分配失败,insmod会给出对应的错误提示。
2.2 头文件安装:Ubuntu/Debian系实操
在Ubuntu上,我一般这样操作:
bash复制sudo apt update
sudo apt install build-essential linux-headers-$(uname -r)
build-essential装了gcc、make等基础工具,linux-headers-$(uname -r)装的是当前内核版本对应的头文件。
装完之后验证一下:
bash复制ls -l /lib/modules/$(uname -r)/build
如果能看到软链接指向/usr/src/linux-headers-$(uname -r),就说明环境没问题。这里有个细节:/lib/modules/$(uname -r)/build是编译模块时Makefile要用的标准路径,几乎所有内核模块的Makefile最终都会回到这里取头文件和配置。它在,编译就能跑;它不在,后面全是No such file or directory。
2.3 头文件安装:CentOS/RHEL/Fedora系实操
红帽系的命令是:
bash复制sudo yum install kernel-devel-$(uname -r) gcc make
# 或者新版本用 dnf
sudo dnf install kernel-devel-$(uname -r) gcc make
装的路径一般是/usr/src/kernels/$(uname -r)。同样,/lib/modules/$(uname -r)/build这个软链接也会指向这个目录。
红帽系有个坑:kernel-devel的版本必须和当前运行的uname -r完全一致。如果系统刚做过内核升级但没重启,uname -r还是旧版本号,而yum默认安装的是新版本的kernel-devel,两者对不上,编译必挂。解决办法是明确指定版本号,或者重启进新内核再装。
2.4 判断内核头文件是否匹配的小技巧
教大家一个快速判断头文件是否可用的方法:直接看/lib/modules/$(uname -r)/build/.config是否存在。.config文件是编译内核或模块时最重要的配置文件,定义了哪些功能被开启、哪些被关闭。模块的编译必须基于这个配置,否则编出来的.ko加载时会出现各种奇怪的符号问题。
bash复制ls -l /lib/modules/$(uname -r)/build/.config
在没有源码的情况下,发行版打包的kernel-devel/linux-headers包里会提供这份.config(或者通过/proc/config.gz生成)。如果这个文件不存在,模块编译时Kbuild会认为内核配置未知,编出来的东西基本不能用。
3. Makefile的编写与内核Kbuild机制
3.1 为什么不能直接gcc编译
有人可能会想:既然就是几个C文件,我直接gcc -c hello.c生成.o行不行?行,但得到的.o不能被insmod加载。原因是内核模块不是简单的.o文件,它是ELF relocatable格式,但带着内核特有的段信息、.modinfo段、__versions段,这些只有在Kbuild体系下编译才会生成。
modinfo hello.ko能看到的信息——模块名、描述、作者、许可证、依赖、版本魔数——就藏在.modinfo段里。insmod加载模块时会检查这些信息,尤其是版本魔数(vermagic)和依赖的符号版本,不匹配就拒绝加载。这些都不是手写gcc命令能做出来的。
还有一点:模块编译时有很多必要的编译选项,比如-DMODULE、-D__KERNEL__,还有-fno-pic、-fno-stack-protector这类体系结构相关的选项。手敲容易漏,漏一个就是奇怪的报错。
3.2 标准Makefile模板与逐行解析
标准的单文件模块Makefile长这样:
makefile复制obj-m += hello.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
逐行看:
obj-m += hello.o:这是Kbuild的变量。obj-m表示以模块方式编译的目标,hello.o是目标名,Kbuild会自动找hello.c来编译。如果你有多个文件组成一个模块,比如hello_main.c和hello_func.c,则需要写hello-objs := hello_main.o hello_func.o,Kbuild会先把这些.o链接成hello.o,再生成hello.ko。KDIR := /lib/modules/$(shell uname -r)/build:指定内核源码树(或头文件树)的路径。uname -r会取当前内核版本号,/lib/modules/x.x.x/build软链接指向头文件目录,Kbuild就在这个目录下找到编译模块所需的整套规则和配置。all目标:调用$(MAKE) -C $(KDIR) M=$(PWD) modules。-C表示切换到KDIR目录下执行make;M=$(PWD)告诉Kbuild,要编译的外部模块源码在M指定的目录下;modules是Kbuild的目标,表示编译所有obj-m声明的模块。clean目标:同样切换到KDIR下,执行Kbuild的clean规则,清理M目录下的编译产物。
这里有个常见误解:很多人以为make是在当前目录下编译,其实真正的编译动作是在KDIR目录下发生的,M=$(PWD)只是告诉Kbuild“我的模块源码放在这里”。理解这一点,后面遇到各种奇怪的路径报错就能找到方向了。
3.3 多文件模块的Makefile写法
当模块拆成多个源文件时,Makefile要稍微加一行:
makefile复制obj-m += mydriver.o
mydriver-objs := main.o ring_buf.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
这里mydriver-objs是Kbuild的另一个约定:<模块名>-objs列出组成该模块的所有.o文件。Kbuild会先把main.c、ring_buf.c编译成目标文件,然后链接成mydriver.o,最后生成mydriver.ko。
很多人不理解:为什么obj-m里写的是mydriver.o,而不是mydriver.ko?因为mydriver.o是链接的中间产物,真正的模块文件名是mydriver.ko。Kbuild的规则用obj-m声明的是“最终模块的link对象”,它倒推去找源码和组成部分。这个约定不了解就会困惑,了解了就会发现Kbuild的设计其实挺顺手的。
3.4 向Kbuild传递自定义编译选项
有时需要向模块源码传递宏定义,或者调整编译选项,可以在Makefile里加这几行:
makefile复制ccflags-y := -DDEBUG -I$(src)/include
# 或者
CFLAGS_hello.o := -O0
ccflags-y对所有编译单元生效,CFLAGS_hello.o只对hello.o生效。我用-DDEBUG做动态调试开关的经验是:在源码里用pr_debug,编译时加-DDEBUG才会输出,这样同一个模块在调试版和发布版之间切换,只需要改Makefile,不用改代码。
-I$(src)/include用于指定头文件搜索路径。注意这里用的是$(src)而不是$(PWD),因为Kbuild的规则里,当前目录不一定是源码目录,$(src)才是Kbuild提供的源码目录变量。写死$(PWD)在多目录构建时可能会出问题。
4. 完整编译过程与产物分析
4.1 从make到ko的完整日志解读
在源码目录下执行make,正常的输出大致如下:
text复制make -C /lib/modules/5.15.0-91-generic/build M=/home/user/hello modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-91-generic'
CC [M] /home/user/hello/hello.o
MODPOST /home/user/hello/Module.symvers
CC [M] /home/user/hello/hello.mod.o
LD [M] /home/user/hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-91-generic'
四行核心输出对应四个阶段:
CC [M] hello.o:编译C源码,生成目标文件。[M]表示这是作为外部模块编译的。MODPOST Module.symvers:modpost阶段,扫描所有目标文件,检查符号引用是否满足,生成模块间依赖信息和符号版本信息。这一步做了版本魔数的校验,如果内核配置和编译环境不匹配,报错会在这里出现。CC [M] hello.mod.o:编译一个小的辅助目标文件,它包含了模块的.modinfo信息,比如许可证、依赖模块列表、 vermagic字符串。这个文件是自动生成的,不需要你操心。LD [M] hello.ko:把hello.o和hello.mod.o链接成最终的hello.ko。
编译完,目录下会出现一堆文件。常用的有:
hello.ko:最终的可加载模块二进制hello.o:模块的编译目标文件hello.mod.c:自动生成的C文件,包含模块信息hello.mod.o:编译hello.mod.c得到的目标文件Module.symvers:导出的符号版本信息,多模块互相依赖时很重要modules.order:模块构建顺序记录
其中.ko其实是个复合文件,相当于hello.o加上.modinfo段和__versions段的合体。可以用readelf去查看它的各个段,能发现不少有意思的细节。
4.2 用modinfo和file验证模块产物
编译完先别急着加载,用两个命令检查一下:
bash复制file hello.ko
modinfo hello.ko
file的输出大致是:
text复制hello.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=..., not stripped
modinfo的输出里,重点看这几项:
text复制vermagic: 5.15.0-91-generic SMP mod_unload modversions
vermagic就是版本魔数,前半部分是内核版本号,后面跟着内核编译的配置特性(SMP、抢占模式、是否支持模块卸载等)。模块加载时,内核会把这个字符串和当前内核的配置做比对,不一致就拒绝加载。这也是为什么uname -r显示的版本号看起来一样,但模块在其他电脑上还是加载不了——因为对方的vermagic后半部分(比如preempt、smp配置)可能不同。
如果modinfo输出里depends一行显示有依赖模块,说明这个模块依赖其他模块,insmod之前必须先加载依赖。模块间依赖在编译时就会被记录到.modinfo段里。
4.3 交叉编译场景下的make参数
如果是在x86主机上给ARM开发板交叉编译模块,Makefile里的KDIR就不能用/lib/modules/$(uname -r)/build了,要指向交叉编译工具链对应的内核源码树,并指定架构和交叉编译器前缀:
makefile复制obj-m += hello.o
KDIR := /path/to/arm/kernel/source
CROSS_COMPILE := arm-linux-gnueabihf-
ARCH := arm
all:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
交叉编译最容易踩的坑是主机架构的内核头文件和目标架构不一致。很多人图省事在x86主机上直接编ARM模块,结果链接器用的还是x86的规则,编出来的东西在ARM板上加载时报Exec format error。正确做法是用目标平台的完整内核源码树,用交叉编译器前缀来编译。这个坑我踩过不止一次,每次都要检查ARCH和CROSS_COMPILE有没有传对。
4.4 静态编译进内核与模块编译的区别
顺带说一下:同样是.c文件,编译进内核(obj-y)和编译成模块(obj-m),虽然在Kbuild里只差一个字母,但产物和使用方式完全不同。
obj-y会把代码直接链接进vmlinux,随内核启动而加载,没有动态加载/卸载的能力;obj-m则生成独立的.ko文件,可以随时insmod/rmmod。调试驱动的阶段,用模块方式可以大幅缩短开发周期——改代码后只需重新编译模块、重新加载,不需要重启整个系统。但是稳定后要投入生产设备时,很多场景会选择静态编进内核,减少启动时加载模块的开销和潜在失败点。
5. 加载、卸载与运行验证
5.1 insmod/rmmod/lsmod三连
编译产物没问题,接下来就是验证“变身”是否成功:
bash复制sudo insmod hello.ko
没有任何输出,就是好消息。查看模块是否加载成功:
bash复制lsmod | grep hello
输出里能看到模块名、占用内存大小、被谁使用:
text复制hello 16384 0
然后看内核日志,确认初始化函数执行了:
bash复制dmesg | tail -5
能看到一行:
text复制[ 1234.567890] Hello, kernel module!
验证完,卸载模块:
bash复制sudo rmmod hello
再看日志,确认清理函数执行了:
text复制[ 5678.123456] Goodbye, kernel module!
这一整套流程走通,说明从代码到可加载模块的链路是完全通畅的。
5.2 printk的级别与输出重定向
用过printk的人会发现,printk(KERN_INFO "...")有时候在终端上看不到输出,得靠dmesg才能看到。这是因为内核日志有级别控制,KERN_INFO的级别是6,低于默认的控制台级别,所以不会直接打到终端上。
常见的几种级别:
KERN_EMERG(0):紧急事件,系统不可用KERN_ALERT(1):必须立即处理KERN_CRIT(2):严重错误KERN_ERR(3):错误情况KERN_WARNING(4):警告KERN_NOTICE(5):正常但重要KERN_INFO(6):信息性消息KERN_DEBUG(7):调试消息
调试模块时我习惯用pr_info或pr_debug,配合echo "file hello.c +p" > /sys/kernel/debug/dynamic_debug/control来做动态调试。当然,前提是内核配置了CONFIG_DYNAMIC_DEBUG,并且模块编译时带上了-DDEBUG或使用了pr_debug。这套机制比加一堆printk再删掉要优雅得多。
5.3 模块参数传递
模块还支持在加载时传递参数。比如在代码里声明:
c复制static int count = 1;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "number of times to print");
static int __init hello_init(void)
{
int i;
for (i = 0; i < count; i++)
printk(KERN_INFO "Hello, kernel module! (%d/%d)\n", i + 1, count);
return 0;
}
加载时就可以这样传参:
bash复制sudo insmod hello.ko count=5
0644是/sys/module/hello/parameters/count这个文件在sysfs中的权限设置,有这个文件,就可以在模块运行期间通过echo 10 > /sys/module/hello/parameters/count动态修改参数值。这个特性在调试和运行时调优中非常实用,比如调整驱动的工作模式、限流阈值等,不用重新编译和加载模块。
5.4 自动加载与依赖管理
生产环境里,很少手动insmod。更常见的做法是把.ko复制到/lib/modules/$(uname -r)/extra/目录下,运行depmod -a更新模块依赖关系,然后通过modprobe hello来加载。
modprobe和insmod的区别在于:modprobe会解析.ko里的depends字段,自动先把依赖模块加载好,再加载目标模块;insmod是“一根筋”,不管依赖,加载失败就报错。所以凡是模块之间有关系,尽量用modprobe。
有时候编译出来的模块文件名和模块内部声明的名字不一致(比如文件叫hello_v2.ko,但MODULE_NAME是hello),modprobe可能会找不到。modprobe按模块名搜索,不按文件名搜索。同一个模块有多个版本共存时,文件放置和命名就特别容易踩坑,建议统一命名规则。
6. 常见编译错误与排查实录
6.1 “No rule to make target”与build软链接问题
头文件没装或者路径不对,最常见的表现就是:
text复制make[1]: *** No rule to make target 'modules'. Stop.
或者:
text复制/bin/sh: 1: cd: can't cd to /lib/modules/5.15.0-91-generic/build
这两个本质上都是同一个问题:KDIR指向的目录不存在或无效。排查路径:
bash复制ls -l /lib/modules/$(uname -r)/build
如果提示软链接失效,或者目录不存在,就按第2节的方法重新安装对应版本的linux-headers或kernel-devel。还有一种情况是内核版本号看错了——注意uname -r的输出可能包含-generic后缀(如5.15.0-91-generic),安装头文件时必须带全。
6.2 “version magic”不匹配
加载模块时报这个错,在开发中很常见:
text复制insmod: ERROR: could not insert module hello.ko: Invalid module format
查看日志:
text复制[ 123.456789] hello: version magic '5.15.0-91-generic SMP mod_unload modversions' should be '5.15.0-89-generic SMP mod_unload modversions'
意思很直白:模块是针对5.15.0-91编译的,但你当前运行的内核是5.15.0-89,版本魔数对不上。
解决办法:
- 重启进
5.15.0-91内核,保持模块和内核版本一致; - 或者用
5.15.0-89对应的linux-headers重新编译模块。
注意,不要试图用modprobe --force或者修改vermagic来强行加载(虽然网上有一些“绕过版本检查”的做法)。内核模块和内核版本不匹配,轻则模块行为异常,重则内核崩溃。这不是“技术不够”的问题,而是安全底线问题——我在生产环境见过有人强行加载版本不匹配的驱动模块,结果内核直接panic,整个业务中断,教训非常深刻。
6.3 “Unknown symbol”与Module.symvers
如果模块里用到了某个没有被当前内核导出的符号,编译时可能不会报错(因为头文件有声明),但加载时会报:
text复制hello: Unknown symbol do_something (err -2)
这种问题的根源一般有三个:
- 该符号在其他模块里定义,编译时通过
Module.symvers记录了依赖,但运行时没有先加载那个模块; - 该符号根本没有被
EXPORT_SYMBOL导出,你的模块无法使用; - 该符号以
EXPORT_SYMBOL_GPL导出,而你的模块没有声明MODULE_LICENSE("GPL")。
排查思路:先确认符号在哪定义的,再用grep在内核源码里查EXPORT_SYMBOL。如果确实没有被导出,只有两个选择:改代码不用这个符号,或者给内核打补丁导出它(不推荐随意这么做)。如果只是依赖其他模块,检查编译时有没有生成Module.symvers,并且把依赖模块一起加载就行。
6.4 编译通过但insmod后模块未生效
这种情况最头疼:insmod没报错,但lsmod里看不到,dmesg也没有输出。常见原因是初始化函数没执行到return 0就返回了错误。比如:
c复制static int __init hello_init(void)
{
int ret;
ret = some_register();
return ret; // 如果ret不是0,模块加载会失败,但insmod的报错可能被吞掉
}
排查方法还是看dmesg。有时候printk的级别太低,终端上没显示,但dmesg里一定有。养成写完insmod立刻dmesg | tail -20的习惯,能省掉大量排查时间。
另一个隐蔽问题是同名模块已加载。如果系统里已经有一个hello模块,你再insmod hello.ko会报File exists。处理办法是先rmmod hello,或者用modprobe -r hello卸载旧模块。
6.5 编译告警与强制忽略
内核编译默认-Werror不是全程开启的,但很多内核版本的Kbuild会对某些特定的告警开启-Werror。遇到编译告警时,不要急着加-Wno-error忽略,先看告警的实质内容。
我遇到过的几种典型告警:
warning: function declaration isn't a prototype:说明函数声明不够规范,可能在老编译器上没问题,新编译器上直接报错;warning: format '%d' expects argument of type 'int':printk格式串和参数类型不匹配,这在64位系统上很容易引发严重bug(比如size_t用了%d);warning: 'xxx' defined but not used:定义了但没使用,多半是代码结构有问题。
这些告警里,format相关的必须修,因为这不是风格问题,是潜在的内核崩溃源。其他风格类告警可以暂时容忍,但建议作为后续优化项。
6.6 动态调试与printk级别调整
最后分享一个调试技巧:编译时加-DDEBUG开启pr_debug,然后在内核命令行或运行时动态调整日志级别。运行时调整:
bash复制echo 8 > /proc/sys/kernel/printk
printk级别为8时,所有调试信息都会打到控制台。但不建议长时间开着,日志量太大会淹没关键信息,还会影响系统性能。
更精细的做法是使用动态调试。确认内核开了CONFIG_DYNAMIC_DEBUG后:
bash复制sudo mount -t debugfs none /sys/kernel/debug
echo "module hello +p" > /sys/kernel/debug/dynamic_debug/control
这样只开启hello模块的pr_debug输出,其他模块不受影响。生产环境排查问题的时候,这套组合拳(模块级动态调试+printk级别控制)比反复改代码编译要高效得多。
7. 从编译到发布的进阶思考
代码能编译、能加载,只是内核模块开发的起点。真正要投入到实际项目中,下面几件事早晚要面对。
- 签名机制:启用UEFI Secure Boot的系统,加载未签名的模块会被拒绝。需要用
mokutil导入自定义密钥,或者用内核提供的scripts/sign-file工具对模块签名。这个坑在桌面Linux上不明显,但在启用了Secure Boot的工作站和服务器上非常普遍。 - 内存安全:模块运行在内核态,越界访问或空指针解引用会直接导致整个系统崩溃,而不像用户态程序只是段错误。写代码时对每次内存访问都要敏感,多用
kzalloc、检查返回值、避免在内核态使用不安全的字符串操作函数。 - 并发考虑:内核模块运行在可抢占、多核并发的环境中,对共享资源的访问必须有锁或原子操作保护。很多模块发布后出现的随机崩溃,根因都是并发访问没有保护。
- 版本迭代:内核API变化很快,一个模块在旧内核上编译通过,换新内核可能就编译失败。比如
proc_create的签名在多个内核版本间变过多次,register_chrdev相关接口也调整过。写模块时尽量封装一层底层的兼容层,或者参考头文件里的兼容宏来适配不同版本。
这些内容每一个展开都能单独写一篇长文。对初学者来说,先把编译链路跑通,把加载卸载流程走熟,再逐步扩展到更复杂的功能,是最务实的路径。“变身术”的第一步,永远是让代码变成能被内核接纳的.ko,这一步稳了,后面的路才会顺。
