资讯详情

【linux内核专栏 09】内核模块与 kbuild

📅 2026/10/15 3:54:39 | 华诺云谱 👁 阅读
【linux内核专栏 09】内核模块与 kbuild
Linux 内核专栏 · 持续更新中本系列从底层原理到源码实战逐篇拆解内核机制。 想系统追更戳这里查看完整目录【Linux 内核专栏 00】总目录本篇定位:天天写驱动,驱动要么编进内核要么做成模块(.o)。模块是 Linux 区别于裸机的关键——运行时加载/卸载代码,不改内核重编重启。本篇把 LKM 机制、Makefile/Kconfig、模块加载流程、符号导出、参数、依赖讲透。读完能写独立 .ko、能看懂内核 Makefile、能调模块加载失败。模块是 Linux 动态扩展的根基,裸机没这能力FreeRTOS/裸机:加功能 改源码 重编 重烧重启。Linux:模块(.ko)运行时 insmod 加载,rmmod 卸载,不用重启。这是宏内核保持单体内核性能的同时获得类微内核扩展性的关键。BSP 写驱动,90% 做成模块(开发快,出问题 rmmod 即卸),稳定后可编进内核。一、内核模块(LKM)是什么1.1 模块的本质LKM 的本质就是把内核模块当成一个“运行时动态链接的 ELF 可重定位对象”由内核模块加载器完成解析、内存分配、符号绑定、重定位最后执行 module_init 并挂入内核模块链表。一个.ko文件(内核对象,kernel object)ELF 格式,包含:代码段(.text)、数据段、符号表、重定位信息运行时被内核动态链接进内核地址空间加载后和内核其他代码同等权限(S 态,可访问一切)1.2 模块 vs 编进内核方式怎么做何时用编进内核(y)make menuconfig 选CONFIG_XXXy,编译进 vmlinux核心/必用/早期要用(根文件系统驱动)模块(m)选CONFIG_XXXm,编成 .ko,运行时加载多数驱动,可后装/可卸/省启动内存不编(n)选CONFIG_XXXn不需要1.3 模块的生命周期源码(.c) → Kbuild 编译 → .ko → insmod/modprobe 加载 → 内核分配内存,重定位,链接符号 → 调 module_init → 模块活 → 使用 → rmmod 卸载 → 调 module_exit → 清理 → 释放内存1.4 对照裸机裸机/FreeRTOSLinux 模块加功能改源码重编重烧insmod .ko重启必须不用代码权限全权全权(S 态)卸载不能rmmod开发调试烧录慢加载快,迭代快模块让你 BSP 开发飞起来裸机改驱动要重编重烧,慢。Linux 写驱动做 .ko,insmod加载,出错rmmod卸载改了再加载——秒级迭代。稳定后再决定编进内核还是留模块。这是 Linux BSP 开发效率的核心来源。二、最简单的模块2.1 hello.c#includelinux/module.h#includelinux/init.hstaticint__inithello_init(void){printk(KERN_INFOHello, kernel module!\n);return0;// 0 成功,负数失败}staticvoid__exithello_exit(void){printk(KERN_INFOGoodbye, kernel module!\n);}module_init(hello_init);module_exit(hello_exit);MODULE_LICENSE(GPL);// 必须,否则内核警告/不能用 GPL 符号MODULE_AUTHOR(XXX);MODULE_DESCRIPTION(A simple hello module);MODULE_VERSION(1.0);2.2 关键点__init:标记初始化函数,加载完进.init.text,加载后释放(省内存)__exit:标记退出函数,只有 rmmod 时才调;若编进内核(y)则不编译(永远不卸载)module_init/module_exit:注册加载/卸载入口MODULE_LICENSE(GPL):必须,否则:① 内核警告 “tainted”;② 不能调EXPORT_SYMBOL_GPL的符号printk:内核打印(不是 printf),走 dmesg2.3 __init 的内存优化staticint__inithello_init(void){...}__init把函数放.init.text段模块加载完,.init段释放(初始化函数只调一次,留着浪费)编进内核的__init在启动后由free_initmem()释放只对加载时调一次的函数用,常驻函数不能标三、Makefile(kbuild)3.1 最简 Makefileobj-m hello.o all: make -C /lib/modules/$(shell uname -r)/build M$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M$(PWD) cleanobj-m:要编成模块的目标(m);obj-y编进内核-C .../build:进内核源码树(用它的 kbuild 系统)M$(PWD):模块源码在外部目录(外部模块)3.2 多文件模块obj-m mydriver.o mydriver-objs : main.o irq.o buffer.o # mydriver.ko 由三个文件组成3.3 kbuild 变量变量含义obj-m编成模块obj-y编进内核obj-n不编xxx-objs复合模块的组成文件ccflags-y额外编译选项3.4 编译流程make → 内核 Kbuild 系统 → hello.c → hello.o(GCC) → hello.o → hello.ko(链接 加 modpost 生成符号信息)3.5 内核内模块 vs 外部模块树内(in-tree)外部(out-of-tree)位置内核源码drivers/...任意目录Kconfig有无(自己写 Makefile)谁用内核官方驱动你 BSP 自研驱动编译make modulesmake -C 内核 M外部嵌入式视角:BSP 驱动通常先外部,后树内开发期:驱动放外部目录,insmod 迭代快。产品化:把驱动合进内核源码树(drivers/…),配 Kconfig/Makefile,编进 BSP SDK。这是 BSP 工作流:外部开发 → 树内合入。四、模块加载机制(insmod 内部)4.1 insmod 做了什么insmod hello.ko → 用户态 insmod 工具 → 读 hello.ko 到内存 → syscall init_module(mod_image, len, params) → 内核: 1. 校验 ELF 头/魔数 2. 分配模块内存(module 结构 代码/数据) 3. 重定位(把符号地址填进去) 4. 解决未定义符号(查内核符号表 已加载模块) 5. 调 module_init() 6. 挂到全局模块链表4.2 符号解析 simplify_symbols模块里的符号分几类:本地符号模块内部可见地址在本模块内解析全局定义符号模块自己导出的函数/变量未定义符号引用内核或其他模块导出的符号如 printk、kmalloc、request_irq导出符号通过 EXPORT_SYMBOL() / EXPORT_SYMBOL_GPL() 暴露给其他模块。内核遍历模块符号表对每个未定义符号在内核符号表 kallsyms 和已加载模块的导出符号表中查找。未定义符号解析失败 → 加载失败(Unknown symbol)看失败:dmesg或modinfo4.3 重定位 apply_relocations符号解析只是把符号名映射成地址重定位才是把地址真正填进代码里。内核遍历 .rela.* / .rel.* 重定位节对每个重定位项做locationtarget_section_baserela-r_offset;symbol_addrresolve_symbol(rela);*locationsymbol_addrrela-r_addend;// 简化理解具体怎么修改指令取决于架构x86_64R_X86_64_64、R_X86_64_PC32 等ARM64R_AARCH64_CALL26、R_AARCH64_ADR_PREL_PG_HI21 等RISC-VR_RISCV_32、R_RISCV_PCREL_HI20、R_RISCV_PCREL_LO12_I 等。这也是为什么 .ko 必须针对特定内核版本和架构编译跨版本/跨架构基本不可用。4.4 执行初始化 do_init_module重定位完成后内核调用模块的 module_init 函数staticint__initmy_driver_init(void){// 注册字符设备、platform driver、中断、GPIO、I2C/SPI 设备等return0;}module_init(my_driver_init);如果初始化失败内核会回滚释放内存、注销已注册资源、卸载模块。成功后模块状态置为 MODULE_STATE_LIVE挂入全局模块链表lsmod 就能看到。4.5 释放 init 段模块加载完成后.init.text / .init.data 不再需要内核会释放这部分内存。所以驱动里把只在加载时用到的代码标成 __init能节省内核内存。4.6 modprobe vs insmodinsmodmodprobe输入.ko 全路径模块名(自动找)依赖不处理自动装依赖配置无读 /etc/modprobe.d/用途调试/手动生产insmod /lib/modules/.../hello.ko# 直接加载modprobe hello# 自动找 装依赖modprobe-rhello# 卸载(含依赖)insmod 比较原始读取 .ko 二进制直接调用 init_module() 系统调用。modprobe 更智能先读 /lib/modules/$(uname -r)/modules.dep递归加载依赖模块再调用 insmod。现代内核还有 finit_module()可以传入文件描述符减少一次性拷贝整个 .ko 的开销。4.7 卸载(rmmod)rmmod hello# 卸载(模块没在用才能卸)lsmod# 看已加载模块模块被使用(如设备打开)→ 卸载失败卸载调module_exit,清理资源(注销设备/释放内存/解中断)module_exit 必须彻底清理,否则内存泄漏/资源占用rmmod / modprobe -r 调用 delete_module 系统调用检查引用计数若仍被使用则拒绝卸载调用 module_exit 函数释放中断、注销设备、free 内存从模块链表移除释放模块占用的内核内存。module_exit 要和 module_init 对称init 里注册了什么,exit 必须注销什么:注册设备→注销,申请中断→释放,申请内存→释放,申请资源→释放资源。你裸机没有卸载概念(代码一直在),Linux 模块能卸,卸载清理是必修课。漏清理 卸载后内核崩(悬挂指针)。4.8 自动加载机制嵌入式或桌面系统里模块不一定手动 insmod。常见自动加载路径udev / mdev 收到 uevent 后调用 modprobe内核 kmod 机制在需要时触发用户态 modprobe驱动通过 MODULE_DEVICE_TABLE() 声明支持的设备 IDudev 据此匹配模块。4.9 嵌入式 / 驱动开发侧的注意点在板子上做驱动或底层开发时LKM 有几个实际坑.ko 必须和板子内核版本、配置、架构完全匹配否则报 Invalid module format交叉编译时要使用目标内核的 Module.symvers 和 .config驱动里不能用 libc只能用 kmalloc/kfree、printk、devm_* 等内核接口初始化函数不能长时间阻塞硬件探测建议用 devm_ 系列做资源管理调试时优先看 dmesg / journalctl -k模块 Oops 往往直接暴露空指针、非法地址、未导出符号若开启 CONFIG_MODULE_SIG未签名模块会被拒绝加载。4.10 总结LKM 不是“把代码直接塞进内核”而是内核里跑了一套精简的动态链接器它读入 ELF .ko校验版本和签名用 vmalloc 分配内核内存通过 kallsyms 解析符号按架构规则应用重定位执行 module_init最后把模块挂入内核模块链表卸载时反向执行 module_exit 并释放资源。五、符号导出(EXPORT_SYMBOL)5.1 为什么需要导出模块 A 要调模块 B 的函数,或模块要调内核函数——必须导出才能被其他模块用。内核默认不导出所有符号(防模块乱用内部接口)。5.2 两种导出// 在提供符号的代码里voidmy_func(intx){...}EXPORT_SYMBOL(my_func);// 任何模块可用intmy_gpl_func(void){...}EXPORT_SYMBOL_GPL(my_gpl_func);// 只有 GPL 许可模块可用EXPORT_SYMBOL:任何许可模块可用EXPORT_SYMBOL_GPL:只有MODULE_LICENSE(GPL)的模块可用(防闭源白嫖)导出后符号进内核符号表(/proc/kallsyms)5.3 看符号cat/proc/kallsyms|grepmy_func# 00000000abcdef00 r my_func [hello] # 模块 hello 的符号cat/proc/kallsyms|grepprintk# ffffff8001234560 T printk # 内核符号5.4 模块间依赖模块 B 依赖模块 A 的符号 → 加载顺序:A 先,B 后 modprobe 自动处理(insmod 不处理,报 Unknown symbol) 卸载顺序:B 先,A 后(被依赖的不能先卸)EXPORT_SYMBOL 是模块间协作的接口BSP 把公用驱动(如平台驱动底座)的接口EXPORT_SYMBOL,上层驱动调。这是 Linux 模块化设计。注意 GPL 导出:你若用闭源模块,只能调EXPORT_SYMBOL(非 GPL)的符号。六、模块参数6.1 传参给模块#includelinux/moduleparam.hstaticintdebug0;module_param(debug,int,0644);// 参数名,类型,权限MODULE_PARM_DESC(debug,Enable debug (0/1));staticchar*namedefault;module_param(name,charp,0644);staticintports[4]{0};staticintnum_ports;module_param_array(ports,int,num_ports,0644);// 数组参数6.2 加载时传insmod hello.kodebug1nametestmodprobe hellodebug1nametest# 持久化(开机自动加载带参)echooptions hello debug1/etc/modprobe.d/hello.conf6.3 运行时改权限含 0220/0644 的参数,可在/sys/module/name/parameters/改:echo1/sys/module/hello/parameters/debugcat/sys/module/hello/parameters/debug6.4 对照 FreeRTOS 配置FreeRTOSLinux 模块配置编译宏模块参数改重编运行时改持久烧录/etc/modprobe.d嵌入式视角:模块参数替代编译宏FreeRTOS 调参要改 config 宏重编。Linux 模块参数运行时传,不改代码不重编。调试利器:加 debug1 打印详情,调好关掉。产品化用 modprobe.d 持久化。七、模块信息与工具7.1 modinfomodinfo hello.ko# filename: hello.ko# license: GPL# author: Zhaoyuhan# description: A simple hello module# version: 1.0# srcversion: ABC123...# depends:# vermagic: 6.6.0 SMP mod_unload modversions riscv64vermagic:版本魔术,模块编译时的内核版本/配置,必须和运行内核匹配,否则加载失败depends:依赖的其他模块7.2 常用工具工具作用lsmod列已加载模块(读 /proc/modules)insmod加载 .kormmod卸载modprobe智能加载(带依赖)modprobe -r智能卸载modinfo看 .ko 信息depmod生成模块依赖索引(modules.dep)7.3 /proc/modules 和 /sys/modulecat/proc/modules# hello 16384 0 - Live 0x... (O)ls/sys/module/# hello/ 里面有 parameters/ sections/ ...八、模块的版本检查8.1 vermagic模块编译时记录内核版本/SMP/抢占等配置,生成 vermagic加载时内核比对 vermagic,不匹配拒绝加载防用别的内核的模块导致崩8.2 强制加载(危险)insmod--forcehello.ko# 跳过 vermagic 检查极度危险(符号地址可能不对 → 崩)只在确认兼容时用8.3 CONFIG_MODVERSIONS开启后,每个导出符号有 CRC 校验模块记录用到的符号 CRC,加载时比对比 vermagic 更细(符号级)九、模块的依赖与 depmod9.1 depmod 生成索引depmod-a# 扫 /lib/modules/$(uname -r)/,生成 modules.depmodules.dep 记录模块→依赖映射modprobe 读它决定加载顺序9.2 模块依赖文件# /lib/modules/$(uname -r)/modules.dep /kernel/drivers/net/mydriver.ko: /kernel/drivers/net/mylib.komydriver 依赖 mylib,modprobe mydriver 会先装 mylib十、内核 Kconfig(配置系统)10.1 Kconfig 语法树内模块用 Kconfig 描述配置选项,menuconfig 里显示:# drivers/my/Kconfig config MY_DRIVER tristate My device driver depends on ARCH_RISCV help This is my device driver. M: module, Y: built-in, N: exclude.tristate:三态(y/m/n)depends on:依赖其他配置select:选中本项自动选某项help:帮助文本10.2 menuconfig 流程makemenuconfig# 进菜单# 选 MY_DRIVER mmake# 编内核makemodules_install# 装 .ko 到 /lib/modules/10.3 .configmenuconfig 的选择存.configCONFIG_MY_DRIVERm→ Makefile 读到obj-mCONFIG_MY_DRIVERy→obj-y10.4 BSP 的配置工作流1. 写驱动 .c 2. 加 Kconfig(配 tristate 选项) 3. 加 Makefile(obj-$(CONFIG_MY_DRIVER) my.o) 4. make menuconfig 选 m 5. make modules → my.ko 6. 部署 .ko,insmodKconfig/Makefile 是你 BSP 必会BSP 加驱动,要改三处:① 源码 .c;② Kconfig(让人能在 menuconfig 选);③ Makefile(告诉 kbuild 怎么编)。这是树内驱动标准流程,面试必问,实战必做。十一、模块开发调试技巧11.1 printk 分级printk(KERN_EMERGpanic 级\n);// 0 最高printk(KERN_ALERTalert\n);// 1printk(KERN_CRITcrit\n);// 2printk(KERN_ERRerror\n);// 3printk(KERN_WARNINGwarn\n);// 4printk(KERN_NOTICEnotice\n);// 5printk(KERN_INFOinfo\n);// 6printk(KERN_DEBUGdebug\n);// 7 最低控制台只显示 console_loglevel 的dmesg看全部调模块:printk(KERN_INFO xxx)dmesg | tail11.2 pr_xxx / dev_xxx(推荐)pr_info(hello\n);// printk(KERN_INFO ...)pr_err(fail: %d\n,ret);pr_debug(debug\n);// 默认不打印,开 DEBUG 才打dev_info(pdev-dev,probe ok\n);// 带设备信息dev_err(pdev-dev,probe fail\n);11.3 加载失败排查insmod my.ko# Failed: ...dmesg|tail# hello: Unknown symbol some_func# → some_func 没导出,或模块没写 GPL# vermagic 不匹配# hello: version magic 5.15.0 should be 6.6.0# → 模块和内核版本不符,重编11.4 模块崩溃模块崩 内核崩(模块在内核态)崩了看 oops(16 篇),addr2line 定位开发时开 lockdep/KASAN(07 篇)嵌入式视角:模块崩和你裸机崩一样你裸机崩 → 死机。Linux 模块崩 → 内核 oops(可能死可能活)。模块代码和内核同等权限,bug 会拖垮整个内核。所以模块要严谨:① 资源对称(init/exit);② 中断上下文不睡;③ 指针校验。你裸机的严谨习惯直接用。十二、DM(设备模型)与模块的关系模块是代码载体,设备模型(10 篇)是组织框架:模块(.ko) ──加载── 注册驱动到 bus ──probe── 绑定设备 ──rmmod── 检查设备是否在用 在用 → 拒绝卸载 没用 → remove → 卸载模块卸载前,内核检查模块是否被引用(设备在用 引用 0)引用计数由设备模型自动管(try_module_get/module_put)设备模型管模块引用计数你不用手动管模块被几个设备用——设备模型的try_module_get/module_put自动增减引用。模块卸载时引用 0,内核拒绝卸载,防悬挂。这是 10 篇设备模型对模块的保护。十三、对照总表概念FreeRTOS/裸机Linux 模块加功能改源码重编重烧insmod .ko运行时加载❌✅卸载❌rmmod代码权限全权全权(S 态)函数导出链接器自动EXPORT_SYMBOL配置参数编译宏module_param配置系统无Kconfig/menuconfig构建系统Makefile/IDEKbuild崩溃影响死机内核 oops/死迭代速度慢(烧录)快(insmod)十四、本篇小结模块(.ko):ELF,运行时动态链接进内核,insmod 加载/rmmod 卸载,免重启模块 vs 编进:m 模块(灵活)/y 编进(必需)/n 不编模块入口:module_init/module_exit,__init/__exit标记省内存MODULE_LICENSE(GPL)必须,否则不能用 GPL 符号Makefile:obj-m xxx.o,多文件用xxx-objs,-C 内核 M外部加载流程:insmod → init_module syscall → ELF 校验 → 重定位 → 符号解析 → module_initmodprobe比 insmod 智能(自动依赖),生产用 modprobeEXPORT_SYMBOL/EXPORT_SYMBOL_GPL:导出符号给其他模块用,GPL 导出只给 GPL 模块module_param:运行时配参,/sys/module/…/parameters/ 可改Kconfig:tristate/depends on/select,menuconfig 选,存 .configvermagic:版本魔术,模块和内核必须匹配(否则加载失败)module_exit 必须对称清理init 注册的资源模块崩 内核崩,严谨度同裸机速查表想干啥用什么写模块入口module_init(fn) module_exit(fn)标记 init/exit 省内存__init / __exit声明许可MODULE_LICENSE(“GPL”)编模块 Makefileobj-m hello.o多文件模块xxx-objs : a.o b.o加载模块insmod hello.ko / modprobe hello卸载rmmod hello / modprobe -r hello列已加载lsmod看模块信息modinfo hello.ko导出符号EXPORT_SYMBOL(fn) / EXPORT_SYMBOL_GPL(fn)看内核符号cat /proc/kallsyms | grep xxx模块参数module_param(name, type, perm)改运行时参数echo val /sys/module/xxx/parameters/name持久加载带参/etc/modprobe.d/xxx.conf 里 options加 Kconfigconfig XXX \n tristate “desc”加 Makefileobj-$(CONFIG_XXX) xxx.o看加载失败原因dmesg | tail打印pr_info/pr_err/dev_info✨ 感谢你的阅读 ✨技术之路漫漫分享是为了更好地交流。如果本文的内容对你有启发希望能得到你的支持 点赞|⭐ 收藏| 关注如果你在调试过程中遇到了其他问题欢迎在评论区 留言我们一起探讨。关注我一起交流底层开发的那些事儿
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑