ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux内核驱动模块:编译、加载失败排查与调试实战

Linux内核驱动模块:编译、加载失败排查与调试实战 简介在Linux系统中驱动往往以内核模块.ko的形式存在它允许内核按需加载硬件支持代码而不必预先集成所有设备。理解模块与内核的关系、总线匹配机制以及模块生命周期是排查各类驱动问题的基础。实际工程中无论是驱动加载失败、模块冲突还是开机阶段卡死都常常需要借助modprobe、dmesg、内核启动参数等工具进行隔离验证。例如nouveau与NVIDIA官方驱动的冲突、BIOS固件阶段报错、模块卸载时提示in use这些现象背后都有清晰的排查链路。掌握内核模块的编译环境配置、动态加载机制以及调试信息读取方法能大幅提升驱动问题的定位效率。本文从驱动、模块、内核三者的关系出发结合典型故障场景系统梳理驱动加载失败与调试实践为嵌入式开发者、运维人员提供可落地的排查思路。 看到drivers_drivers_linux_Kernel_这个标题我第一反应是这是把三个最能让人头秃的关键字堆在一起了。驱动、Linux、内核每个词背后都藏着一堆为什么开机又卡了为什么模块又编译不过的深夜故事。我最近刚好帮人处理过一台 DELL 工作站开机卡死的故障屏幕长期停留在一行loading BIOS drivers...最后排查下来问题既不在 BIOS 也不在系统盘而是内核模块加载阶段出了岔子。这篇文章就借这个场景把 Linux 内核驱动这条线完整捋一遍驱动是什么、模块怎么编译、怎么装载和卸载、加载失败时如何定位以及拿到崩溃现场后怎么读日志。适合刚接触 Linux 内核模块的开发、运维和嵌入式学习者也适合那些被驱动问题折磨了一晚上、正在犹豫要不要重装系统的人。1. 先把驱动这个词在 Linux 内核里到底指什么搞清楚1.1 驱动、模块、内核三者的从属关系很多人把驱动理解成一个安装包双击安装就完事这是 Windows 给的习惯。Linux 里更常见的说法是内核模块也就是.ko文件。你可以把 Linux 内核想象成一家公司的总部大楼里面水电、网络、门禁这些基础设施由几个核心部门管着而驱动就是每个具体设备的驻场专员比如网卡专员、显卡专员、声卡专员。内核模块则是这些专员的工牌和工位需要的时候把工牌挂上去人就能进楼干活不需要了把工牌摘下来楼里的一切照常运转。这套设计解决了一个关键问题内核本身不需要认识世界上所有的硬件。Linux 发行版那么多不可能每台机器都把所有硬件驱动编进内核里。于是内核保留了运行时加载代码的能力让驱动以模块形式存在按需加载。这就是驱动和内核最核心的关系——驱动是内核功能的插件模块是驱动的载体。值得注意的一点并不是所有驱动都以模块形式存在。有些驱动直接编译进内核镜像built-in用CONFIG_XXXy控制另一些则是模块用CONFIG_XXXm控制。编译进内核的驱动开机就在没有卸载一说模块则可以在系统运行期间动态加载、卸载。如果你在/lib/modules/$(uname -r)/kernel/drivers/下翻过目录会发现里面按net、scsi、gpu、usb等子目录分门别类那些.ko、.ko.xz文件全是可加载的驱动模块。1.2 为什么说加载驱动其实是把代码送进内核用户态程序运行在 CPU 的非特权级别访问硬件端口、操作设备寄存器这些事是想都不要想的。内核则运行在特权级别对硬件有完全控制权。驱动要做的事情——读写寄存器、处理中断、DMA 传输——全都依赖特权操作所以驱动代码必须进入内核态执行。那加载驱动到底在干什么本质上是在内核地址空间里完成一次动态链接。.ko文件不是普通的可执行程序它是一段内核代码和 Windows 下的.sys驱动类似但加载方式更加动态。当你执行insmod xxx.ko时内核会把这个文件里的代码段和数据段映射进内核地址空间解析符号引用然后调用模块里的初始化函数。从那之后这段代码就和内核本身运行在同一个特权级、同一个内存空间里。这解释了一个让很多人困惑的现象普通程序崩溃最多是段错误、core dump系统还活着但驱动一旦崩溃往往是整个系统直接挂掉。因为驱动是内核空间的一部分in-kernel 代码出错内核没有能力像保护用户态进程一样保护自己。所以每次我调试驱动都习惯先开虚拟机别拿自己的主力机硬扛。1.3 一个设备怎么找到自己的驱动总线匹配机制Linux 内核里设备驱动的匹配并不是靠安装顺序而是靠一套总线模型。bus、device、driver三者之间是解耦关系总线维护设备列表和驱动列表当一个设备出现时内核会遍历驱动列表查看驱动的 ID 表里有没有匹配这个设备的硬件 ID。以 PCI 设备为例驱动代码里会声明一张pci_device_id表static const struct pci_device_id my_pci_ids[] { { PCI_DEVICE(0x1234, 0x5678) }, { 0 } }; MODULE_DEVICE_TABLE(pci, my_pci_ids);0x1234是厂商 ID0x5678是设备 ID。设备插上后lspci -nn能看到类似1234:5678的编号内核正是根据这个编号把设备和驱动配对起来。MODULE_DEVICE_TABLE这个宏非常关键它不只是给内核运行时看的还会在模块编译时生成一段别名信息供modprobe在用户态做自动化加载。也就是说模块还没装进内核用户态的模块工具就已经知道这个驱动支持哪些设备了。这个机制搞明白之后很多怪事就能解释了为什么刚插上一个 USB 设备系统自动就把驱动加载了因为udev收到内核上报的设备事件查到了模块别名调用modprobe加载了对应模块。如果你手动insmod一个模块而设备 ID 不匹配加载过程也不会报错只是驱动绑定不到任何设备上等于白干。1.4 同一个词两个世界Linux 内核和计算 kernel聊到这必须插一个容易踩晕的词kernel。在 Linux 语境里kernel 指的是操作系统内核但在 GPU 编程、AI 编译器领域kernel 是核函数指一段跑在设备端GPU、NPU的代码。比如昇腾平台的 Ascend C 自定义算子开发让你写一个tanhcustom算子通常要分 kernel 侧代码和 host 侧代码kernel 侧定义设备上执行的并行计算逻辑host 侧负责任务调度、内存分配、调用 kernel。很多做 AI 算子的人第一次看到编写 kernel 侧代码这个要求时会误以为要改 Linux 内核其实完全不是一回事。但这两个kernel并非毫无关联。无论你在设备端写多少算子最终把任务喂给设备的还是底层的内核驱动。Ascend 设备有自己的内核驱动模块用户态通过设备节点提交任务驱动负责搬运描述符、同步中断。理解了 Linux 驱动的工作方式再去看这些异构计算栈的任务提交模型会顺很多。这也是我把这个区分放在最前面的原因——后面我们讨论的所有内容都是围绕 Linux 内核这个kernel展开的。2. 从零编译一个内核模块Kbuild、Makefile 与头文件匹配法则2.1 环境准备内核头文件版本必须完全一致编译内核模块的第一步不是写代码而是装对头文件。内核模块不同于用户态程序它依赖当前内核的各类结构体定义、宏、函数符号。如果你的头文件版本和当前运行的内核版本不一致编译出来的模块基本没法加载运气好报version magic不匹配运气差直接导致内核崩溃。先确认当前版本uname -rUbuntu/Debian 系统对应安装sudo apt install linux-headers-$(uname -r)安装之后/lib/modules/$(uname -r)/build这个符号链接就指向了头文件目录。如果这个链接不存在说明头文件没装或者装错版本。一个常见的坑是系统自动更新了内核你还在旧内核上编译然后没重启uname -r显示的仍旧是旧版本但/usr/src下已经多了一堆新头文件。这时候一定以uname -r为准安装和它严格匹配的 headers 包。还有一个隐蔽问题你装的是内核源码而不是内核头文件。源码通常是一整个 Linux tree但编译模块真正需要的是配置好的 build 目录包含Makefile、Kconfig、include/config、Module.symvers等。如果你发现/lib/modules/$(uname -r)/build存在但里面缺少.config文件说明内核头文件包装得不完整编译模块时会报出一堆无法解析的符号。2.2 最小模块代码与 Makefile 模板写一个最简单的内核模块代码量很少但每一行都值得解释#include linux/init.h #include linux/module.h static int __init hello_init(void) { pr_info(hello module loaded\n); return 0; } static void __exit hello_exit(void) { pr_info(hello module unloaded\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(your-name); MODULE_DESCRIPTION(A minimal kernel module);module_init和module_exit声明了模块加载和卸载时回调的函数。__init宏表示该函数只在初始化阶段使用内存可以被内核回收__exit宏表示该函数在模块卸载时才需要。MODULE_LICENSE(GPL)不只是声明还和符号可见性相关如果你引用了只对 GPL 模块导出的内核符号没有这句就编译不通过。对应的 Makefile 使用 Kbuild 体系不能想当然地写一个普通.c - .o的规则obj-m : hello_module.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) cleanobj-m : hello_module.o告诉 Kbuild这个模块由hello_module.c编译而来。make -C是进入内核 build 目录执行modules目标同时通过M$(PWD)指明你的模块源码目录。这种写法等于把编译控制权完全交给了内核的构建系统自己只负责提供源码。我第一次从用户态 Makefile 切过来时很不适应总觉得绕了一层其实这正是内核为了统一编译规则做的设计。编译成功后目录下会出现hello_module.ko。可以用file命令看一眼它和普通.so文件格式不一样.ko是 ELF relocatable 类型专门为内核运行时的动态重定位准备的。2.3 实测报错building kernel modules 的完整排查链路网上很多人搜过error: an error occurred while performing the step: building kernel modules这是第三方脚本比如某些驱动厂商的安装脚本调用make modules失败时报出的笼统提示。它并没有告诉你真正原因真正原因藏在输出日志里。我自己的排查习惯是顺着下面这条链路走第一步确认uname -r和 headers 是否匹配。如果系统刚升级完内核还没重启uname -r还是旧版本但/lib/modules/下可能已经存在多个版本目录。安装脚本有时会错误地指定到最新的头文件目录从而编译出和当前内核不匹配的模块。第二步检查/lib/modules/$(uname -r)/build和/usr/src/linux-headers-$(uname -r)两个目录是否完整。缺少Makefile、Module.symvers或.config时编译过程会中断但错误提示千奇百怪有时只是一个No rule to make target。第三步把整个编译日志重定向到文件里看不要只看屏幕最后一行make -C /lib/modules/$(uname -r)/build M$PWD modules build.log 21然后搜索error:和fatal error:关键字。相当一部分 case 是缺少linux/xxx.h头文件这通常意味着 headers 包没装全。第四步看 gcc 版本。新版内核比如 6.x对编译器版本有最低要求老旧的 gcc 会直接报too old反过来过新的编译器也可能因为内核代码的兼容性而报错。内核社区推荐的编译工具链版本可以参考当前内核源码Documentation/process/changes.rst里的要求。第五步也是最容易被忽略的检查目录路径里有没有中文、空格或者没有写权限。Kbuild 对路径中的特殊字符很敏感我的一个同事把驱动源码放在共享网盘目录下编译一直报错拷到本地/home立刻就好。2.4 module_init 与 module_exit驱动的生命周期与资源管理加载一个模块内核会做几件固定的事验证模块签名或 taint 状态、分配模块内存、解析依赖符号、调用module_init指向的函数。一个合格的 init 函数应该完成设备的注册和资源的申请比如注册字符设备、申请中断、映射 IO 内存、创建 sysfs 节点。任何一步失败都应该goto到对应的错误处理分支逐步释放已经申请到的资源。我在 review 驱动代码时最常看到的问题就是 init 函数里申请了多个资源中间一步失败后直接return -ENOMEM前面申请的资源全部泄漏。内核空间不像用户态进程退出后系统不会帮你回收资源这些泄漏会一直留在系统里直到重启。module_exit做的事情正好相反把设备注销、释放中断、解除 IO 映射、销毁 sysfs 节点。这里有一个经验不要在 exit 函数里做太多耗时操作因为模块卸载的过程会持有模块锁做慢了会影响系统整体响应。现代驱动开发很少直接写module_init更多用module_platform_driver、module_pci_driver这类封装宏它们把 bus 注册逻辑封装好了。但无论用哪种宏最终都会落到module_init上理解生命周期仍然是一切的基础。3. 装载、卸载与开机自启insmod、modprobe、黑名单的完整链路3.1 insmod 与 modprobe 的差别依赖解析是关键insmod和modprobe都能加载内核模块但设计哲学完全不同。insmod是一个非常笨的工具你给它一个.ko文件的路径它就把这个文件加载进内核。如果这个模块依赖其他模块而依赖的模块还没加载insmod会直接报 Unknown symbol。它不会帮你解析依赖也不会自动去标准模块目录里找文件。modprobe则聪明得多。它只接受模块名然后到/lib/modules/$(uname -r)/目录里结合modules.dep文件查找模块同时解析依赖顺序把依赖模块先加载进来。modules.dep不是内核自动生成的而是由depmod这个工具维护。任何第三方驱动编译安装后如果没有跑depmodmodprobe就会找不到它但insmod指定完整路径仍能加载。所以调试自己刚编译的模块时insmod其实更直接因为它绕开了依赖数据库适合快速验证但如果你希望系统能自动化管理这个模块就必须把它拷贝到标准模块目录然后执行depmod -a更新依赖。这也是很多驱动安装包最后一步总要跑一遍depmod的原因。3.2 模块参数、modprobe.d 配置文件与 sysfs 的关联驱动代码里可以通过module_param宏导出参数让用户在加载时配置行为static int debug_level 0; module_param(debug_level, int, 0644);第三个参数是权限位。如果是0644表示用户可以通过 sysfs 在运行时查看和修改如果是0则表示该参数只在加载时有效运行时不可见。加载时传参有两种方式insmod mydriver.ko debug_level2 modprobe mydriver debug_level2如果想要持久化配置可以写配置文件# /etc/modprobe.d/mydriver.conf options mydriver debug_level2 blacklist nouveaumodprobe.d目录里的文件会在modprobe加载模块时被统一读取。这个机制非常实用比如你想调整某个驱动的默认参数又不想改代码写一行options就行。模块加载后你会在/sys/module/模块名/parameters/下看到这些参数文件。写1到某个参数文件等于在运行时修改参数。但需要注意驱动代码可以限制参数是否支持运行时修改权限位0444的情况下即使 root 也不能直接写文件。3.3 网卡驱动与开机不自启的真实处理方式网上有人搜linux怎么让网卡不开机自启这个问题需要拆成两层看网卡驱动层和网络配置层。如果只是不想让某个网卡的驱动模块开机自动加载可以用 modprobe 黑名单# /etc/modprobe.d/blacklist.conf blacklist r8168然后更新 initramfs 让黑名单在早期启动阶段就生效sudo update-initramfs -u但很多人的真实需求其实是我网卡不要自动联网或者某个网卡接口不要自动启用这跟驱动加载根本不是一回事。网络接口的管理由NetworkManager或systemd-networkd负责驱动加载之后网卡只是被内核识别了真正让接口获得 IP、开始通信的是网络服务。如果你的网卡被 NetworkManager 自动接管最简单的方式是nmcli device disconnect enp3s0 nmcli device set enp3s0 managed no或者直接关掉系统里的网络服务。所以我处理这类问题前一定会先问一句你到底是觉得模块不该加载还是接口不该启用这两者差了十万八千里。3.4 模块卸载失败Module is in use 的定位方法rmmod mydriver时如果提示Module mydriver is in use说明有代码路径正在引用这个模块最常见的是设备正在被打开。先用lsmod看 Use countlsmod | grep mydriverUse count 不为 0 时需要找到是谁在引用。/sys/module/mydriver/holders/目录下列出的模块就是持有引用的模块也可能是某个进程正占用着设备节点。用fuser -v /dev/mydriver或lsof找到占用进程把它停掉再卸载。还有一个我踩过的坑如果你把模块内嵌到别的模块里或者模块之间互相引用卸载顺序错误也会导致 in use。这种依赖关系靠代码结构判断得顺着holders一层层查。反过来rmmod之前先把使用它的模块卸载是一个通用的解法。4. 驱动加载失败现场nouveau/nvidia、开机卡死与 UEFI boot drivers 报错4.1 the nouveau kernel driver is currently 到底在说什么NVIDIA 显卡在 Linux 下一直是一大痛点原因在于两个驱动争抢同一块硬件。nouveau是开源社区通过逆向工程实现的 NVIDIA 显卡驱动默认集成在大多数发行版内核里nvidia是 NVIDIA 官方的闭源驱动。两者都要控制 GPU但没法共存。安装 NVIDIA 官方驱动时脚本会检查当前是否加载了nouveau模块如果加载了就会出现The Nouveau kernel driver is currently in use by your system这样的提示。处理办法是把nouveau拉黑cat /etc/modprobe.d/blacklist-nouveau.conf EOF blacklist nouveau options nouveau modeset0 EOF sudo update-initramfs -u这里有一条关键经验blacklist只是告诉modprobe不要自动加载nouveau但如果你已经装过开源驱动并且模块被编译进 initramfs 里重启后它还是会被加载。所以要update-initramfs -u重新打包 initramfs同时建议安装官方驱动后再重建一遍确保nvidia模块的依赖关系进入启动环境。很多时候驱动现象看起来是版本冲突实质是设备所有权冲突。两块驱动都认为自己是 GPU 的合法控制者。这个思路可以推广到任何硬件当一个设备绑定到错误驱动时不要急着卸载先看lsmod、看lspci -k的kernel driver in use字段确认谁占用了设备。4.2 loading BIOS drivers 卡住不是驱动坏是等待超时文章开头提到的那台 DELL 工作站就是典型的loading bios drivers卡死。这块文字其实是 BIOS 固件在 POST 阶段加载内置驱动比如 RAID 卡 Option ROM、网卡 PXE ROM时的提示。如果卡在这一行不动绝大多数情况不是驱动文件坏了而是某个固件驱动在等待硬件响应一直超时。我当时的排查步骤是这样的拔掉所有外设只剩键盘屏幕看能否通过 POST。结果是能过说明问题出在某块扩展卡或某个设备上。进 BIOS 关闭 PXE 启动项、关闭网络唤醒、关闭 RAID 模式改成 AHCI逐一排除。系统依然卡住时把目光转到 Linux 内核启动参数上。取消quiet splash让内核打印每一条启动信息最终看到卡在ahci模块初始化问题定位到 SATA 控制器的兼容性。临时启动时在 GRUB 命令行加modprobe.blacklistahci验证确认后升级 BIOS 固件解决。这个 case 说明所谓加载 BIOS drivers 不动很可能是固件和内核驱动叠加出来的问题。固件阶段加载了某个 Option ROM内核接管后又要初始化同一个设备两者配合不上就卡死。遇到这类问题建议先把启动日志完整拍下来一条一条看绝大多数卡住的位置都会明确告诉你停在了哪个驱动的初始化里。4.3 UEFI0116 报错固件阶段的 boot driver 问题UEFI0116: One or more boot drivers have reported issue(s). Check the drivers...这个报错更特殊它出现在操作系统接管之前的 UEFI 固件阶段。很多用户搜这个词是因为开机先看到了这条提示进系统后又伴随驱动异常。这里要搞清楚时序现代 x86 机器启动分三个阶段——UEFI 固件阶段、bootloader 阶段、内核阶段。UEFI 阶段的驱动和 Linux 内核驱动完全不同它们是写在固件里的 UEFI driver或加载自 Option ROM 的扩展驱动。UEFI0116 的意思是某个 boot driver 在StartImage时返回了错误状态但系统没有直接放弃启动。常见的触发源包括 RAID 卡、网卡 PXE 模块、TPM 配置错误、外接 PCIe 设备固件太旧。处理思路也很固定先记录触发前的硬件变化最近有没有加过新设备然后进 BIOS 关闭不必要的启动项再考虑升级设备固件。这类问题严格说不属于 Linux 内核驱动范围但如果内核驱动和固件驱动同时操作同一设备现象会纠缠在一起。所以我通常会把固件阶段报错和内核阶段报错分开看前面靠 BIOS 日志和硬件排查后面靠dmesg。4.4 用启动参数和黑名单控制模块加载顺序驱动加载顺序出问题时最粗暴但最有效的控制手段是内核启动参数。在 GRUB 菜单按e编辑启动项在内核行追加参数临时生效确认有效后写入/etc/default/grub再update-grub持久化。常用参数modprobe.blacklistmodule1,module2禁止这些模块自动加载nomodeset内核加载显示驱动时不做模式设置常用于 NVIDIA/AMD 显卡黑屏问题pcinoacpi禁用 ACPI 管理 PCI 设备处理某些老硬件中断冲突acpioff彻底关闭 ACPI不到万不得已不推荐以nomodeset为例很多人只知道它能解决开机黑屏但不知道原理显卡驱动在加载时会做显示模式切换如果切换失败可能导致屏幕无输出。nomodeset让驱动不执行模式设置保留 BIOS 设定好的显示模式操作系统就能先跑起来再慢慢装正确的驱动。明白了这个机制后你就能举一反三推测其他启动参数的使用场景。对于 initramfs 阶段的模块加载/etc/initramfs-tools/modules文件也可以控制哪些模块在早期被装入。这个文件在需要强制加载某个驱动进行磁盘解密、根文件系统挂载时非常有用。顺序问题本质上要靠依赖关系约束内核模块系统不会严格按字母顺序加载而是等设备出现后再匹配。理解了这一点你会发现很多加载顺序焦虑是多余的只要你的驱动正确注册了设备 ID 表设备出现时自然会绑定不用硬排顺序。5. 内核驱动调试三板斧dmesg、debugfs 与崩溃现场5.1 dmesg 的正确打开方式从环形缓冲区到系统日志驱动调试的第一现场永远是dmesg。它会打印内核环形缓冲区中的日志包括驱动加载时pr_info、dev_err输出的消息以及内核检测到的硬件信息。常用的定位命令dmesg -l err # 只看错误级别 dmesg -l warn # 只看警告 dmesg --follow # 实时跟踪类似 tail -f journalctl -k -b # 当前启动的内核日志 journalctl -k -b -1 # 上次启动的内核日志有一个经验很多人不知道重启之后再看dmesg环形缓冲区已经被清空你丢失了上次启动的早期信息。但journalctl -k -b -1能看到上一次启动的内核日志前提是 systemd-journald 在之前那次启动中已经持久化了日志。所以遇到反复重启才能复现的问题别只盯着当前的dmesg看用journalctl -k回顾历史记录往往有意外收获。dmesg显示不全时需要关注几个可能的原因早期启动阶段环形缓冲区很小部分日志被覆盖quiet启动参数会抑制部分控制台输出但日志还是会写进环形缓冲区某些驱动用了dev_dbg级别的输出默认不打印需要开启动态调试才能看到。5.2 /sys/kernel/debug 为空debugfs 与动态调试很多人打开/sys/kernel/debug发现是空的或者目录不存在。这个目录是 debugfs 的挂载点很多内核子系统会通过它导出调试信息。但它不像/proc、/sys那样默认始终挂载需要手动挂载sudo mount -t debugfs none /sys/kernel/debug如果挂载时报 unknown filesystem type debugfs说明内核没开CONFIG_DEBUG_FS。发行版内核一般默认开启但精简内核或某些嵌入式内核会关掉需要重新编译内核开启这个选项。/sys/kernel/debug里面最有价值的工具之一是dynamic_debug。内核里大量dev_dbg、pr_debug输出在编译时默认被裁剪但如果你开了CONFIG_DYNAMIC_DEBUG运行时可动态打开echo module mydriver p /sys/kernel/debug/dynamic_debug/control这个命令会打开mydriver模块里所有动态调试点的输出。排查驱动逻辑时这条命令比到处加printk再重编译高效得多。我一般会先打开动态调试看驱动有没有进入关键路径再决定要不要加额外日志。另外/sys/kernel/tracing旧版本是/sys/kernel/debug/tracing是 tracefs 的挂载点配合trace-cmd或直接写echo function_graph current_tracer可以追踪函数调用。内核驱动卡死、死锁问题排查到这个层面基本就是进阶玩家的领域了。5.3 驱动崩了怎么办Oops、panic 与 call trace 阅读驱动崩溃时最常见的关键词是Oops。它是内核发出的我在内核态执行时遇到了非法操作的警告但系统不一定立刻死掉。如果 Oops 发生在进程上下文通常该进程被杀死系统还能继续运转如果发生在中断上下文或内核关键路径就可能升级为panic整个系统直接停机。一份典型的 Oops 日志长这样BUG: unable to handle kernel NULL pointer dereference at 0000000000000010 PGD 800000000000000 P4D ... Oops: 0000 [#1] SMP NOPTI CPU: 2 PID: 1234 Comm: mydriver Tainted: P RIP: 0010:my_func0x10/0x100 [mydriver]阅读重点是RIP行它会告诉你崩溃发生在哪个函数、哪个地址偏移。配合addr2line可以把地址翻译成源码行号addr2line -e mydriver.ko my_func0x10但要注意ko 文件的地址需要先做重定位计算直接用偏移往往对不上。更稳妥的办法是打开CONFIG_DEBUG_INFO编译模块然后用crash工具或gdb加载 vmlinux 分析。Tainted标记也值得一看它表示内核被污染了——一般是因为加载了没有 GPL 许可、或没有签名的模块。P代表私有模块、G代表 GPL 模块、E代表未签名的模块。内核之所以要记录 taint是因为一旦加载了来路不明的模块崩溃时很难判断问题出在官方代码还是第三方代码里。遇到 Oops 但系统还活着时第一件事是把整个日志完整保存下来dmesg的尾部就是 call trace。Call trace 里每一行都会显示调用链从崩溃点往前回溯能快速定位是驱动自己的逻辑错误还是被上层调用方错误地触达了。5.4 用 vscode clangd 搭一个能跳转的内核源码阅读环境最后分享一下我读内核源码和驱动代码的开发环境。内核代码量太大符号太多直接用 vscode 默认的 IntelliSense 打开整个源码树会卡成幻灯片。我现在的方案是clangd compile_commands.json。先保证内核源码里有编译数据库最省事的方式是依赖gen_compile_commands.pymake LLVM1 defconfig scripts/clang-tools/gen_compile_commands.py生成compile_commands.json后在 vscode 里安装 clangd 插件禁用自带的 C/C IntelliSenseclangd 会自动读取编译数据库提供全内核的跳转、查找引用、补全。有一个关键细节如果只想看代码而不编译用make LLVM1 defconfig生成一个默认配置就好不要用发行版自带 config否则gen_compile_commands.py编译单元太多生成过程会等很久。如果你是排查某个具体模块的代码可以缩小范围只编译这个模块再生成数据库make Mdrivers/net/ethernet/intel LLVM1 scripts/clang-tools/gen_compile_commands.py drivers/net/ethernet/intel这样 clangd 加载范围小响应速度也会快很多。配合内核源码里的.clang-format文件代码风格检查也能和上游保持一致。这个环境搭好之后读驱动源码的效率至少提升一倍尤其追查内核 API 的调用关系时跳转比 grep 不知道高到哪里去了。我自己用这套组合排查过不少驱动问题包括本文前面提到的网卡驱动加载失败、nouveau 冲突以及一次 SATA 控制器在固件阶段的初始化超时。回过头看这些问题的共性其实不是某个驱动写得有多烂而是设备、内核配置、用户空间工具三者之间的版本和状态没有对齐。如果你也正在被驱动问题折磨我的建议永远是先看dmesg再看lsmod然后用启动参数隔离验证最后才考虑重编译或者重装。内核驱动这东西一旦摸清它的脾气其实比想象中讲道理。本文还有配套的精品资源点击获取
返回列表