学习笔记:Linux 驱动模块 (.ko) 全解
前置知识:在 Linux 的世界里,我们通常把 内核层(Kernel) 和 驱动层(Driver) 视为一体,统称为内核态 ;而把 用户层 和 应用层 统称为用户态。驱动其实是"寄生"在内核里的。内核是框架,而驱动是具体实现。
1. 身份揭秘:.ko 到底是什么?
1.1 名字的由来
- 全称 :K ernel Object(内核对象)。
- 后缀 :
.ko。
1.2 它是什么?(生活类比)
如果把 Linux 内核比作一辆正在高速公路上行驶的汽车:
- 内核 (zImage/uImage):是汽车的主体(引擎、底盘、车轮),出厂时就焊死在一起了。
- 应用程序 (App):是车里的乘客。
- 驱动模块 (.ko) :是**"USB 车载配件"**(比如一个外接的行车记录仪,或者车载空气净化器)。
特点:
- 即插即用 :你不需要把车停下来拆开发动机,在车跑的过程中插上(
insmod)就能用。 - 附属品:它不能独立运行,必须==依附于汽车(内核)==才能工作。你不能把行车记录仪扔在地上让它自己跑。
- 权限极高:它一旦插上,就成了汽车电路的一部分,如果它短路了(代码写错了),整辆车都会熄火(内核崩溃/Kernel Panic)。
1.3 技术定义
从文件格式上讲,.ko 文件本质上是一种 ELF (Executable and Linkable Format) 文件。
它和你在 Ubuntu 上看到的 .o 文件(目标文件)非常像,但多了一些"自我介绍"的信息(比如它支持哪个版本的内核、它的依赖关系等),专门用于让内核加载器识别。
2. 前世:.ko 是怎么变出来的?(编译原理)
很多初学者最大的困惑是:"为什么编译驱动不能像编译 hello.c 那样简单?"
2.1 编译的核心差异
- 编译 APP:只需要标准 C 库(stdio.h 等)。
- 编译驱动 :坚决不能 用标准 C 库!驱动直接运行在内核里,它需要的是内核源码 提供的数据结构和函数定义(如
struct file,printk)。
所以,编译 .ko 必须手里有一份**"对应版本的内核源码"**。
2.2 编译过程详解
假设你有 driver.c,编译成 driver.ko 经历了以下步骤(Makefile 帮你做的):
-
预处理 & 编译 (.c -> .o)
交叉编译器(gcc)读取内核源码的头文件,把 C 代码翻译成汇编,再转成机器码(.o 文件)。
此时,代码里的 printk(内核提供的 )只是一个空标签,还不知道 printk 函数具体在内存的哪里。
-
生成版本信息 (.mod.c -> .mod.o)
构建系统会自动生成一个 driver.mod.c 文件,里面记录了:"我是为 Linux 4.1.15 版本编译的,我依赖哪些符号"。然后把它也编译成 .mod.o。
-
链接 (.o + .mod.o -> .ko)
链接器(ld)把你的驱动逻辑 (.o) 和版本信息 (.mod.o) 打包在一起,生成最终的 driver.ko。
总结 :
.ko= 你的代码逻辑 + 专门给内核看的"入职简历"(版本校验信息)。
3. 今生:insmod 的原理(加载机制)
当你输入 insmod driver.ko 并回车时,就像给新员工办理入职手续。这个过程在内核里称为 "动态链接"。
3.1 insmod 干了四件大事:
第一步:读取文件 (Read)
insmod 命令(其实是调用了 finit_module 系统调用)把 driver.ko 文件从磁盘(或 Flash)里原封不动地读到了内存中。
第二步:地址重定位 (Relocation) ------ 最核心步骤
这是最难理解的,请仔细看:
- 问题 :驱动代码里有一些跳转指令(比如
if判断跳转),在编译的时候,编译器不知道驱动会被放在内存的哪个位置(因为内存是动态分配的)。编译器只能填个相对偏移量(比如:往后跳 100 步)。 - 解决 :内核给驱动分配了一块真实的物理内存地址(比如
0xC0001000)。然后,内核里的加载器会修改驱动代码中的地址,把那些"相对偏移"全部改成绝对真实的内存地址。- 类比:你住酒店。预订时你只知道是"标准间"(相对概念),只有办理入住(insmod)时,前台(内核)才会告诉你具体是"305 房间"(绝对地址)。
第三步:符号解析 (Symbol Resolution)
-
问题 :你的驱动里调用了
printk。但printk的代码是在内核里的,你的驱动怎么找到它? -
解决:内核有一张"公开导出符号表"(Exported Symbol Table),里面记录了 printk 在内存 0x80045000 的位置。
加载时,内核会把你的驱动里所有调用 printk 的地方,直接填上 0x80045000 这个地址。
- 类比:新员工(驱动)入职,问老板:"打印机(printk)在哪里?" 老板指了一下:"在走廊尽头(地址)。"
第四步:执行初始化 (Init)
一切准备就绪,内核找到你代码中标记为 module_init 的函数(比如 my_driver_init),执行它。
- 这就相当于新员工入职后的"自我介绍"。通常在这里注册设备号、初始化 GPIO 等。
4. 为什么要用 .ko?为什么不直接编进内核?
你可能会问:"为什么不把所有驱动都直接编译到 zImage 里?这样开机就有了,不用 insmod,多省事?"
| 比较维度 | 静态编译 (zImage) | 动态模块 (.ko) |
|---|---|---|
| 形象比喻 | 你的手臂(长在身上) | 手里的锤子(拿起来用) |
| 优点 | 启动速度快,不需要文件系统支持 | 灵活!修改驱动只需重编 .ko (1秒),不用重编内核 (10分钟) |
| 缺点 | 内核体积巨大;每次修改都要重启刷机 | 启动时需要脚本加载 |
| 适用场景 | 必须有的驱动(如文件系统、LCD驱动) | 调试阶段的驱动、非必要的设备(如USB设备) |
对于初学者:
永远使用 .ko 方式开发! 想象一下,如果你每改一行代码,都要重新编译整个内核并烧写到板子上,你需要 20 分钟才能看一次运行结果;而用 .ko,只需要 make -> cp -> insmod,10 秒钟搞定。
5. 极简总结(复习小抄)
.ko是什么 :Linux 的动态链接库 ,相当于 Windows 的.dll。- 编译前提 :必须指定内核源码路径,不能用普通 GCC 编译。
insmod原理 :- 搬运:把文件搬到内存。
- 改地址:告诉驱动它住在内存哪里。
- 找帮手:帮驱动找到内核函数(如 printk)的地址。
- 跑函数 :运行
module_init里的代码。
- 卸载 :
rmmod命令会执行module_exit函数,并释放内存,就像把 USB 拔掉一样。