最近又踩了一次buildroot下modprobe: can't open 'modules.dep': No such file or directory的坑,正好趁这个机会把这个问题彻底掰开揉碎讲一遍。
场景很典型:我用buildroot给一块ARM开发板定制系统,内核和rootfs都是同一套构建流程产出的,板子起来之后无线网卡驱动没自动加载。我手动执行modprobe想把驱动拉起来,结果shell直接甩了这句话。第一反应是驱动模块没编出来,但去/lib/modules/目录一看,.ko文件明明躺在那里,模块本身没有任何问题,问题就出在modules.dep这一层。
如果你也遇到同样的报错,先别急着怀疑模块代码、工具链或者内核配置,绝大多数情况下是构建流程、rootfs打包和内核版本匹配这三件事里有一环没对上。下面我从报错本身的含义讲起,把完整的排查思路和修复手段都过一遍。
1. 先读懂这个报错:modprobe在找什么,为什么找不到
1.1 modules.dep是什么
modules.dep是内核模块的依赖关系描述文件,由depmod工具扫描指定目录下所有.ko文件后生成,位置固定在内核模块目录下:
/lib/modules/$(uname -r)/modules.dep文件内容长这样:
/kernel/drivers/net/wireless/xxx/xxx.ko: /kernel/drivers/net/wireless/yyy/yyy.ko: /kernel/drivers/net/wireless/xxx/xxx.ko每一行是一个模块路径加冒号,冒号后面列出它依赖的其他模块。第二行的意思是:加载yyy.ko之前,必须先加载xxx.ko。这个文件描述的是/lib/modules/$(uname -r)/目录下的相对关系,路径不写绝对路径前缀,因为modprobe会自动以模块目录作为根目录去解析。
除了modules.dep,depmod还会生成modules.alias、modules.symbols、modules.builtin等文件,分别服务于模块别名匹配、符号查找和编入内核的模块记录。modprobe在解析"你要加载什么模块"这个问题时,依赖的就是这一整套元数据,其中modules.dep是核心。
1.2 modprobe和insmod的区别
很多人只知道modprobe是加载模块的命令,但不知道它和insmod的本质差异。
insmod的行为非常直白:给你指定路径的.ko文件,加载它。它是sys_init_module系统调用的一个薄封装,不解析依赖,不查别名,不做任何智能处理。如果模块A依赖模块B,而你只insmod A.ko,内核会直接报Unknown symbol,因为符号解析不到。
modprobe则完全不同。它做的事情可以拆成三步:第一,根据模块名在modules.alias和modules.dep里定位真正的.ko文件路径;第二,解析这个模块依赖了哪些其他模块,递归地把依赖先加载好;第三,再加载目标模块。整个过程都依赖modules.dep提供依赖关系信息。所以一旦modules.dep缺失,modprobe第一步就卡死了,直接报出标题里那个错。
打个比方:insmod是你手动把每个零件按顺序装到机器上,modprobe是拿着装配图自动装。modules.dep就是那张装配图。
1.3 命令本身缺没缺,这是两码事
这里要厘清一个容易混淆的点。报错信息modprobe: can't open 'modules.dep': No such file or directory意味着modprobe命令本身是存在的、可执行的,它在运行过程中找不到目标文件。
如果系统里压根没有modprobe命令,shell会提示的是modprobe: not found或者command not found,那才是"缺少命令"。
buildroot里modprobe命令通常来自两个地方:
- busybox:buildroot默认集成busybox,busybox的
modprobeapplet默认是开启的,提供基本的模块加载能力; - kmod:如果启用了
BR2_PACKAGE_KMOD_TOOLS,会安装完整的modprobe和depmod,功能比busybox版本更全,对模块依赖的处理也更完善。
如果你用到的是精简过的busybox配置,恰好把CONFIG_MODPROBE关掉了,那就会出现命令缺失。这个可以通过make busybox-menuconfig勾选回modprobe解决,或者干脆启用kmod工具,两条路都能拿到modprobe命令。
2. buildroot里谁负责生成modules.dep:一条容易被忽视的流水线
2.1 从内核配置到模块落地的完整步骤
在buildroot中,构建内核和生成rootfs镜像是一个串联流程。Linux内核包的安装步骤会做几件事:
- 编译内核镜像(
zImage/Image等)和设备树; - 如果内核配置了
CONFIG_MODULES=y,执行make modules_install,把所有以<M>状态编译的模块安装到output/target/lib/modules/<release>/目录下; - 调用主机端的
depmod工具,以output/target为根目录,为<release>这个版本生成modules.dep和配套元数据文件; - 之后buildroot再根据你选择的rootfs格式(ext4、cpio、tar等),把整个
output/target目录打包成最终镜像。
步骤2和步骤3不是恒定执行的,它们有一个前提条件:内核配置里打开了CONFIG_MODULES=y。如果你用make linux-menuconfig进入内核配置后,在General setup里没有勾选Enable loadable module support,那么无论你在驱动目录里选了多少<M>,这些模块都不会被编译,更不会进入rootfs。buildroot在构建linux包时会去读取内核的.config文件,检测到CONFIG_MODULES=y才会执行模块安装和depmod,这两步是绑定在一起的。
2.2 改过内核配置,为什么modules.dep还是没生成
这个坑我踩过不止一次,属于buildroot使用者的高频翻车点。
你执行make linux-menuconfig,把某个驱动从<*>改成了<M>,保存退出,然后直接执行make想重新打包rootfs。结果发现要么模块没进rootfs,要么进了但没有modules.dep,modprobe照样报错。
原因在于:buildroot对linux包有自己的一套依赖判断逻辑,它会比对源码状态、配置文件、构建产物的时间戳。在有些情况下,单纯跑顶层make并不会重新触发linux包的安装步骤,特别是当你只是改了内核配置,而没有触发源码目录变化时。buildroot可能认为linux包"没有需要重做的部分",直接跳过了安装环节,自然也不会执行modules_install和depmod。
解决方法很简单,用make linux-rebuild强制重建linux包:
make linux-rebuild这个命令会强制重新执行linux包的编译和安装流程,包括重新安装内核模块、重新生成modules.dep。执行完之后再跑一次完整的make,让buildroot把最新的output/target打包成rootfs镜像。这一套组合基本能解决"改了配置但模块依赖文件不刷新"的问题。
2.3 外部内核模块包也是重灾区
如果你的驱动不是内核自带的,而是以buildroot外部包的形式加入(常见的做法是写一个xxx.mk,在编译后调用内核的模块构建机制),那modules.dep是否生成就取决于你在这个包里的安装脚本怎么写。
很多从旧工程或者网上抄来的.mk文件,只做了模块安装,没有顺手调用depmod。典型的写法是:
define MYDRV_INSTALL_TARGET_CMDS $(MAKE) -C $(@D) modules_install \ INSTALL_MOD_PATH=$(TARGET_DIR) endef这样模块文件确实被放进了/lib/modules/<release>/,但modules.dep可能没有被更新,甚至完全缺失。正确做法是在安装步骤里追加一次depmod调用,或者在.mk里使用buildroot提供的kernel-module基础设施,让框架自动处理依赖文件。业内很多驱动仓库的buildroot集成包就是这么处理的,但不同buildroot版本的变量名和工具路径有差异,直接照抄网上代码时要格外注意版本适配。
3. 现场排查:从板上现象反推问题出在哪一环
遇到这类报错,不要急着重建系统,也别一头扎进驱动源码里。按顺序做三个检查,几分钟就能把问题范围缩小到具体环节。
3.1 第一步:确认modprobe是谁提供的
在开发板的终端上执行:
which modprobe ls -l /sbin/modprobe /bin/modprobe 2>/dev/null正常情况下你应该能看到busybox或者kmod提供的符号链接。如果shell提示command not found,说明rootfs里压根没带modprobe命令,那就需要回buildroot里把busybox的modprobeapplet打开,或者启用kmod工具。
如果命令存在,继续下一步。
3.2 第二步:对比uname -r和/lib/modules
这一步是整个排查链路里最容易被忽略的,但也是最高频的根因。在开发板上执行:
uname -r ls /lib/modules/对比两个命令的输出。如果uname -r显示的内核版本和/lib/modules/下的目录名不一致,那modules.dep缺失只是表象,真正的问题是rootfs里的模块版本和实际启动的内核版本对不上。
举个例子:buildroot构建内核时带了CONFIG_LOCALVERSION="-custom",那么模块会安装到/lib/modules/5.10.0-custom/,depmod生成的也是这个目录下的modules.dep。但如果bootloader加载的Image是另一个版本,比如5.10.0,那么内核运行时的uname -r是5.10.0,modprobe会去/lib/modules/5.10.0/找modules.dep。目录都不存在,自然报can't open 'modules.dep'。
3.3 第三步:回到主机看output/target
如果板子和buildroot构建环境是同一套,直接看构建机上的output/target目录:
ls -l output/target/lib/modules/ find output/target/lib/modules -name 'modules.dep'这里会出现三种情况:
output/target下没有modules.dep,说明问题出在buildroot构建环节,需要检查内核配置和构建流程;output/target下有modules.dep,但最终烧录到板子上的rootfs镜像里没有,说明问题出在打包或者烧写环节,比如rootfs镜像大小限制导致lib/modules被裁剪,或者烧写时漏掉了这个目录;output/target和板上都有modules.dep,但modprobe依然报错,那就要检查uname -r版本是否匹配,以及rootfs是否以只读方式挂载导致模块目录不可读。
3.4 用一张对照表快速判断根因
| 现场现象 | 大概率根因 | 解决方向 |
|---|---|---|
/sbin/modprobe不存在 | busybox/kmod未启用modprobe | make busybox-menuconfig勾选modprobe,或启用BR2_PACKAGE_KMOD_TOOLS |
uname -r与/lib/modules不一致 | 内核镜像与rootfs版本不配套 | 统一内核版本,或调整CONFIG_LOCALVERSION后重新构建 |
output/target下没有modules.dep | 构建流程没执行depmod | make linux-rebuild后再执行完整make |
output/target有,但镜像里没有 | 打包/烧写环节丢了lib/modules | 检查rootfs大小限制、分区布局和烧写内容 |
modules.dep存在但modprobe仍报错 | rootfs只读挂载或模块目录权限问题 | 重新以读写方式挂载rootfs,确认目录权限 |
4. 标准修复流程:让buildroot重新生成完整的模块目录
如果是buildroot一体构建的环境,推荐走正常构建流程修复,不建议在开发板上长期靠手工维护模块文件。标准流程分四步。
4.1 确认内核开启模块支持
执行:
make linux-menuconfig进入菜单后确认这个选项是开启状态:
General setup -> [*] Enable loadable module support然后把需要动态加载的驱动(无线网卡、蓝牙、USB转串口等)选成<M>。保存退出。这一步是后续所有流程的前提,CONFIG_MODULES不开启,模块目录和modules.dep都不会生成。
4.2 强制重建linux包
这里注意,不要直接执行make linux或者直接执行顶层make,它们不一定能强制刷新模块安装和depmod。用:
make linux-rebuild这个命令会强制重建linux包,重新执行模块安装和depmod。构建完成后立即检查:
ls -l output/target/lib/modules/*/modules.dep如果能看到modules.dep文件,说明这一步已经打通。如果还是没有,回头检查内核配置里的CONFIG_MODULES是否真的生效了,有时候你改了配置但没保存到.config,buildroot读到的还是旧值。
4.3 重新打包完整rootfs镜像
linux-rebuild只是更新了output/target中间目录,最终烧录用的镜像还没更新。接着执行:
makebuildroot会重新执行rootfs打包逻辑,产出新的系统镜像。烧录之前建议先解包或挂载镜像确认一下lib/modules/<版本>/modules.dep确实在,这一步能避免烧完才发现问题白折腾一轮。
4.4 版本一致性的最终验证
烧录后启动进入系统,执行:
uname -r ls /lib/modules/确认两者一致后,再执行:
depmod -a modprobe <你的模块名>不再报错就算彻底解决。depmod -a在部分系统上可以随手跑一下,它能根据当前运行的内核版本重新生成依赖文件,修复一些历史遗留的元数据缺失问题。
5. 快速兜底方案:开发板上手工生成modules.dep
如果只是想临时验证某个驱动能不能挂载,不想为这事重新构建整个系统,可以在开发板上手工生成modules.dep。这套方案适合快速验证,但不建议作为长期依赖的手段。
5.1 板子上有depmod的情况
如果rootfs里带了depmod(busybox提供了depmodapplet,或者启用了kmod tools),直接在开发板上执行:
depmod -a它会扫描当前内核版本对应的模块目录,生成modules.dep及其他元数据文件,然后modprobe就能正常工作了。这是最快、最省事的验证路径。
5.2 板子上没有depmod的交叉生成方式
如果目标系统里没带depmod命令,可以在构建主机上利用buildroot生成的主机工具完成。在buildroot根目录执行:
./output/host/sbin/depmod -a -b ./output/target <内核版本号>注意<内核版本号>必须和开发板上uname -r的输出保持一致。执行完检查:
ls -l output/target/lib/modules/<版本>/modules.dep确认文件存在后,再跑一次完整make刷新rootfs镜像,重新烧录即可。
这里要提醒一句:output/target是buildroot的中间目录,后续只要某个包触发了重建,这个目录可能会被还原。所以手工在output/target里生成的modules.dep只能算是应急补丁,不能作为可持续的方案。想彻底解决,还是得回到第4节的构建流程,让脚本自己产出正确的依赖文件。
5.3 如果连depmod都用不了,最后一招
如果板上和主机上都没有depmod可以用,还有一个临时办法:直接用insmod配合模块完整路径加载:
insmod /lib/modules/5.10.0/kernel/drivers/net/wireless/xxx/xxx.koinsmod不读取modules.dep,所以能够绕开这个错误。代价是你必须自己保证模块的依赖顺序,假设模块A依赖模块B,就得先加载B再加载A。模块少的时候临时用一下没问题,模块一多,手动维护依赖顺序就会变成一场灾难,所以只适合应急,不适合长期使用。
6. 这个坑的其他打开方式:版本后缀、外部包、全内编
6.1 内核LOCALVERSION导致版本不匹配
内核的release字符串不一定等于内核源码版本。buildroot在编译内核时,如果配置了CONFIG_LOCALVERSION="-test1",内核源码的include/config/kernel.release文件里会记录类似5.10.0-test1的字符串,模块安装时会以这个字符串为目录名,depmod也是针对它生成的。
如果你的启动环境和构建环境对不上,比如bootloader加载了一个没有该后缀的Image,那么内核运行时的uname -r就是5.10.0,和/lib/modules/5.10.0-test1/不匹配,modprobe自然找不到modules.dep。排查这类问题,除了uname -r,还可以用cat /proc/version或者strings命令查看内核镜像里的实际版本信息。
6.2 外部驱动包只拷贝.ko不跑depmod
这个我在第2.3节已经提过。在buildroot中集成第三方驱动时,如果只是把编译好的.ko文件通过普通的文件拷入方式放进rootfs,而没有走模块依赖体系,就会产出"模块存在但依赖元数据缺失"的状态。
正确的做法是在.mk的install步骤里补上depmod调用,或者直接使用buildroot的kernel-module宏来声明这个包的模块属性,让框架在构建阶段统一处理。网上很多开源项目的buildroot集成代码已经在这么做了,直接拿来用之前要确认和你的buildroot版本兼容。
6.3 一条偷懒且保险的思路:把驱动直接编进内核
如果这块驱动不需要在运行时卸载和重载,也没有复杂的替换需求,可以在内核配置里把对应选项从<M>改成<*>,直接编进内核镜像。这样就不存在模块加载的问题,modules.dep的坑自然也就绕开了。
代价是内核镜像会变大,而且驱动初始化阶段的错误可能会导致整个系统启动失败,排错会比模块方式更麻烦。这个权衡要根据实际项目需求来做,没有绝对的对错。
以上是我在buildroot项目里和modules.dep搏斗几次之后沉淀下来的完整排查路径。现在再遇到这类报错,我的第一反应已经不是重新编译驱动,而是先跑uname -r和ls /lib/modules/做对比,再决定回构建环境处理还是临时在板上补一步depmod。这套方法能帮我把定位时间控制在几分钟以内,如果你手头板子的驱动模块比较多,或者经常在多个内核版本之间切换,建议一开始就把depmod步骤固化到构建脚本里,不要等到烧录到板子上才来填坑。