news 2026/4/22 8:27:26

嵌入式Linux模块开发(5)——内核调试技术

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
嵌入式Linux模块开发(5)——内核调试技术

嵌入式Linux模块开发(5)——内核调试技术

仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里!欢迎各位大佬观摩!喜欢的话点个⭐!

仓库地址:https://github.com/Awesome-Embedded-Learning-Studio/imx-forge

静态网页:https://awesome-embedded-learning-studio.github.io/imx-forge/

在黑暗中寻找光亮

在前面的章节中,我们学习了内核打印、模块机制等基础知识。但说实话,知道怎么打印消息,和知道怎么高效调试,是两回事。当你写的驱动加载失败、系统崩溃、或者行为异常时,你需要的是一套系统的调试方法论。这一章,我们想带你成为内核调试专家。

调试驱动真的和调试用户空间程序很不一样。你不能用gdb直接attach上去,不能随便printf,出了问题经常就是整个系统死掉。这些限制让内核调试变得特别有挑战性,但一旦你掌握了正确的方法,那种把bug从黑暗中揪出来的成就感也是无与伦比的。

dmesg:内核的日记本

dmesg是你最重要的调试工具,没有之一。内核把所有消息写入一个环形缓冲区,dmesg就是查看这个缓冲区的窗口。不管你遇到什么问题,第一步永远是看dmesg。

最基本的用法就是直接运行dmesg查看所有内核消息,但输出会非常多,滚屏都滚不完。更实用的方式是用管道配合tail命令,只看最后几行:

dmesg|tail-20

这会显示最近20条消息,通常就是你刚刚操作产生的输出。如果你想知道系统启动时的消息,可以用head命令:

dmesg|head-20

持续监控新消息是调试动态问题的利器:

dmesg-w

加上-w参数后,dmesg会像tail -f一样持续显示新的内核消息。这在调试驱动加载、设备初始化等过程时特别有用。你可以在一个终端运行dmesg -w,在另一个终端加载驱动,实时观察内核的反应。

如果你想监控特定模块的消息,可以结合grep使用:

dmesg-w|grepmy_driver

这样你就只会看到包含"my_driver"的消息,不会被其他无关信息干扰。这个技巧在复杂系统中调试特定驱动时非常有价值。

清空日志缓冲区是另一个实用的技巧。当你调试了好几轮,日志已经乱成一团的时候,可以先清空再测试,这样输出更清晰:

dmesg-cinsmod my_driver.kodmesg

但要注意,dmesg -c会清空整个缓冲区,不仅仅是过滤,所以如果你想保留之前的日志,要先备份。

时间戳显示让日志分析更方便。默认的时间戳是从系统启动开始的秒数,不太直观。加上-T参数后,时间戳会变成人类可读的格式:

dmesg-T

输出会变成类似"[Wed Apr 16 10:30:45 2026] my_driver: Device loaded"这样的格式,方便你理解事件发生的时间顺序。

如果你只想看错误和警告,可以用-l参数过滤:

dmesg-lerr,warn

这个命令只显示错误和警告级别的消息,忽略info和debug。当你只关心问题时,这个过滤功能能帮你快速定位。

内核日志级别控制

在前面讲内核打印的时候,我们提到了日志级别。现在我们深入理解一下这个机制,因为它直接影响你能看到什么调试信息。

你可以通过以下命令查看当前的日志级别设置:

cat/proc/sys/kernel/printk

输出类似"4 4 1 7"这样四个数字。第一个数字是控制台日志级别,决定了哪些消息显示到控制台。默认值通常是4,这意味着KERN_WARNING及更高级别的消息会显示到控制台,而KERN_NOTICE和KERN_INFO会被过滤掉。

如果你想看到所有消息,包括DEBUG级别的,可以这样设置:

echo8>/proc/sys/kernel/printk

设置是立即生效的,不需要重启。但这个设置是临时的,重启后会恢复默认值。如果想永久设置,可以修改/etc/sysctl.conf文件,添加"kernel.printk = 8"这一行,然后运行sysctl -p使配置生效。

或者你可以在内核启动参数中设置,编辑/etc/default/grub文件,在GRUB_CMDLINE_LINUX中添加"printk.time=1 printk=8",然后运行update-grub更新grub配置。这种方式适合需要永久保持特定日志级别的场景。

我们来对比一下不同日志级别下的实际效果。当日志级别等于4(默认)时,加载模块后只显示EMERGENCY、ALERT、CRITICAL、ERROR、WARNING这几个级别的消息,INFO、NOTICE、DEBUG都没有显示。但当你把日志级别设置为8后重新加载模块,NOTICE和INFO级别的消息也会显示出来。

这个对比清楚地说明了日志级别控制的重要性。很多时候你以为代码有问题,其实只是日志级别过滤掉了你的调试输出。

动态调试:运行时控制打印

传统的调试方式需要在编译时决定是否包含调试代码,但Linux内核提供了更强大的机制:动态调试。这个功能允许你在运行时控制哪些pr_debug()和dev_dbg()消息被打印,而不需要重新编译模块。说实话,这个特性真的让调试效率提升了一个档次。

首先你要确认内核是否支持动态调试:

cat/boot/config-$(uname-r)|grepDYNAMIC_DEBUG

或者

zcat /proc/config.gz|grepDYNAMIC_DEBUG

你应该看到CONFIG_DYNAMIC_DEBUG=y和CONFIG_DYNAMIC_DEBUG_CORE=y这两个配置项。如果你的内核没有启用动态调试,那就有点麻烦了,要么重新编译内核,要么只能用传统的调试方法。

动态调试的使用很简单。首先查看可用的调试点:

cat/sys/kernel/debug/dynamic_debug/control|head-20

这会显示前20个动态调试点,每行包含文件名、函数名、行号和格式化字符串。你可以用grep过滤特定模块的调试点:

cat/sys/kernel/debug/dynamic_debug/control|grepmy_module

启用调试也很简单:

echo"module my_module +p">/sys/kernel/debug/dynamic_debug/control

这会启用my_module模块的所有调试打印。你也可以只启用特定文件或函数的调试:

echo"file my_driver.c +p">/sys/kernel/debug/dynamic_debug/controlecho"func my_read +p">/sys/kernel/debug/dynamic_debug/control

甚至可以精确到某一行:

echo"file my_driver.c line 78 +p">/sys/kernel/debug/dynamic_debug/control

禁用调试就是把+p改成-p:

echo"module my_module -p">/sys/kernel/debug/dynamic_debug/control

查看当前状态也很方便:

cat/sys/kernel/debug/dynamic_debug/control|grep"=p"

这会显示所有已启用的调试点,你可以快速确认你的调试设置是否生效。

动态调试的高级用法包括组合条件和添加自定义前缀。你可以在一条命令中指定多个条件:

echo"file my_driver.c func my_read +p">/sys/kernel/debug/dynamic_debug/control

这会只启用my_driver.c文件中my_read函数的调试。你还可以启用调试并添加自定义前缀:

echo"file my_driver.c +p\"[MY_DEBUG]\"">/sys/kernel/debug/dynamic_debug/control

这样所有来自my_driver.c的调试消息都会加上[MY_DEBUG]前缀,方便在日志中识别。

常见问题排查流程

我们总结了一些常见问题的排查流程,这些是我们踩过无数坑后沉淀下来的经验。

模块加载失败是最常见的问题之一。症状通常是insmod报错"Invalid module format"。排查步骤是这样的:首先查看内核日志,dmesg | tail -20会告诉你具体是什么问题。然后检查内核版本匹配,uname -r和modinfo my_module.ko | grep vermagic应该一致,如果不一致就是版本不匹配。接着检查模块依赖,modinfo my_module.ko | grep depends会显示模块依赖的其他模块,确保这些依赖模块已经加载。最后如果都不行,就重新编译,make clean && make -C /lib/modules/( u n a m e − r ) / b u i l d M = (uname -r)/build M=(unamer)/buildM=(pwd) modules。

模块加载成功但设备不工作也很常见。这种情况下驱动加载了,但操作设备时没反应。排查步骤:首先检查设备注册,cat /proc/devices | grep my_module确认设备号已经注册,ls -l /dev/my_device确认设备节点存在。然后检查模块参数,cat /sys/module/my_module/parameters/*可以看到当前参数值,cat /sys/module/my_module/refcnt可以看引用计数。接着检查内核消息,dmesg | grep my_module看有没有错误或警告。最后测试设备,echo “test” > /dev/my_device和cat /dev/my_device看设备是否响应。如果还不行,cat /sys/module/my_module/initstate检查模块初始化状态。

系统崩溃是最令人头疼的问题。症状是系统冻结或重启,dmesg可能都看不了。这种情况下你要想办法保存崩溃日志。如果系统还能勉强操作,dmesg > crash_log.txt先保存日志。然后分析崩溃位置,dmesg | grep -A 30 "Oops|Panic"查看完整的崩溃信息,dmesg | grep "Call Trace"查看调用栈。常见的崩溃原因有空指针解引用、内存访问越界、死锁、栈溢出等。如果你有ksymoops工具,可以用它解析符号:ksymoops < crash_log.txt。

内存泄漏是个隐蔽的问题,症状是系统运行一段时间后内存不足,free -h显示内存持续减少。排查步骤:首先查看模块内存使用,cat /proc/modules | grep my_module会显示模块占用的内存。然后用slabtop查看slab分配器,slabtop | grep my_module看模块相关的slab缓存。接着启用内核内存调试,在内核启动参数中添加slab_debug或slub_debug。最后使用kmemleak(需要启用CONFIG_DEBUG_KMEMLEAK),echo scan > /sys/kernel/debug/kmemleak扫描内存泄漏,cat /sys/kernel/debug/kmemleak查看泄漏报告。

内核调试工具箱

除了dmesg和动态调试,还有很多强大的内核调试工具值得了解。

trace-cmd是ftrace的前端,用于追踪内核函数调用。安装很简单,apt-get install trace-cmd。记录函数调用:

trace-cmd record-pfunction-gdo_sys_open

然后查看记录:

trace-cmd report

或者实时显示:

trace-cmd show

这个工具在分析内核调用路径时非常有用,比如你想知道某个系统调用都经过了哪些内核函数。

perf是性能分析工具,用于性能分析和热点定位。记录性能数据:

perf record-g./my_test_program

然后查看报告:

perf report

或者实时监控:

perftop

perf会显示CPU时间花在哪些函数上,帮你找到性能瓶颈。

crash是内核崩溃分析工具,用于分析内核崩溃转储。安装后,你可以加载内核调试符号:

crash /usr/lib/debug/lib/modules/$(uname-r)/vmlinux /proc/kcore

然后使用各种命令分析崩溃。bt查看回溯,ps查看进程列表,mod -s my_module查看模块信息。这个工具在分析系统崩溃时非常有价值,当然前提是你的系统配置了kdump并且生成了崩溃转储。

kgdb是内核调试器,用于远程调试内核,类似gdb。你需要在内核启动参数中添加kgdboc=ttyS0,115200 kgdbwait,然后在另一个终端运行gdb:

gdb vmlinux(gdb)target remote /dev/ttyS0(gdb)breakmy_function(gdb)continue

这样你就可以像调试用户空间程序一样调试内核了,包括设置断点、单步执行、查看变量等。

/proc和/sys文件系统是运行时查看内核状态的窗口。你可以查看模块信息、设备信息、内存信息、中断信息等:

cat/proc/modulesls/sys/module/cat/proc/devicesls/sys/class/cat/proc/meminfocat/proc/iomemcat/proc/interrupts

这些伪文件提供了丰富的内核状态信息,是调试时的重要参考。

调试技巧和最佳实践

最后我们分享一些调试技巧和最佳实践,这些是我们多年踩坑总结出来的经验。

渐进式调试很重要。不要一次性打印太多信息,要循序渐进。你应该先打印最基本的信息,确认代码执行到了哪里,然后逐步增加详细的调试信息。如果一开始就打印一大堆,反而会被信息淹没,找不到关键问题。

使用标识符很实用。在调试消息中添加唯一标识符,方便搜索和过滤。你可以定义一个DEBUG_TAG宏,在每条调试消息前加上文件名、函数名、行号等信息。这样当你grep日志时,能快速定位到具体的代码位置。

条件编译可以避免生产环境的性能影响。使用#ifdef DEBUG包围详细的调试代码,或者使用pr_debug(),它只在定义了DEBUG时才编译。这样你的发布版本可以完全不含调试代码,既减小了体积,又避免了性能损失。

断言检查能帮你尽早发现错误。使用BUG_ON()和WARN_ON()检测关键错误,比如检查指针有效性、检查状态错误等。BUG_ON()会触发oops,WARN_ON()只会打印警告栈回溯。在开发阶段,这些检查能帮你快速发现逻辑错误。

dump_stack()是理解执行路径的好工具。当某些异常情况发生时,打印调用栈能帮你了解代码是怎么执行到这里的。我们在调试时经常加这个,看看实际的调用路径是否和预期一致。

hex dump调试在处理二进制数据时很有用。使用print_hex_dump()查看二进制数据的内容,可以按十六进制和ASCII两种格式显示,特别适合调试协议、数据结构等问题。

实战案例

让我们通过一个实际案例,综合运用这些调试技巧。问题是驱动加载成功,但读取设备时返回错误。我们的调试过程是这样的:

首先确认问题现象,加载驱动后cat /dev/my_device报错"read error: Invalid argument"。然后查看内核日志,dmesg | tail -20显示read函数被调用了,但返回了错误码。接着启用动态调试,echo “module my_driver +p” > /sys/kernel/debug/dynamic_debug/control,再次cat /dev/my_device,这次看到了更详细的调试输出。

查看代码后发现问题:read函数检查count参数时,如果大于1024就直接返回-EINVAL,而不是调整大小。修复方法是调整count值而不是返回错误。重新编译加载后测试,cat /dev/my_device成功输出数据,问题解决。

这个小案例展示了完整的调试流程:确认现象、查看日志、启用调试、分析代码、修复验证。虽然问题很简单,但这个流程对复杂问题同样适用。

下一步

到这里,我们已经把内核调试技术讲完了。你现在应该有一套系统的调试方法论,知道从哪里开始排查问题,用什么工具,怎么分析日志。这些技能在后面的驱动开发中会不断用到,因为说实话,没有谁能一次写出完美无缺的驱动,调试是开发过程的重要组成部分。

接下来我们要开始真正的字符设备驱动开发了。我们会从最简单的虚拟设备开始,逐步过渡到真实的硬件驱动。准备好了吗?让我们开始吧。


相关阅读

  1. 深入理解Linux模块——模块参数与内核调试:让模块"活"起来的魔法 - 相似度 100%
  2. 深入理解Linux模块——内核模块编译与加载详解:从 Makefile 到 insmod 的完整旅程 - 相似度 100%
  3. 嵌入式Linux驱动开发(3)——内核模块机制 - Linux 的插件系统 - 相似度 100%
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/22 8:23:03

Phi-3.5-Mini-Instruct区块链:智能合约解释+ABI调用说明+Gas优化建议

Phi-3.5-Mini-Instruct区块链&#xff1a;智能合约解释ABI调用说明Gas优化建议 1. 区块链智能合约基础概念 1.1 什么是智能合约 智能合约是运行在区块链上的自执行程序代码&#xff0c;它定义了参与方之间的协议条款&#xff0c;并在满足预设条件时自动执行相应操作。与传统…

作者头像 李华
网站建设 2026/4/22 8:20:18

React与ChatGPT Turbo构建智能文本改写工具

1. 项目概述&#xff1a;基于React与ChatGPT Turbo的智能改写工具开发在内容创作爆炸式增长的今天&#xff0c;原创性已成为核心竞争力。作为前端开发者&#xff0c;我经常需要处理技术文档的多种表述方式&#xff0c;手动改写既耗时又难以保证质量。最近利用React和ChatGPT Tu…

作者头像 李华
网站建设 2026/4/22 8:15:43

终极指南:如何在SketchUp中实现完美的STL导入导出

终极指南&#xff1a;如何在SketchUp中实现完美的STL导入导出 【免费下载链接】sketchup-stl A SketchUp Ruby Extension that adds STL (STereoLithography) file format import and export. 项目地址: https://gitcode.com/gh_mirrors/sk/sketchup-stl 你是否正在寻找…

作者头像 李华
网站建设 2026/4/22 8:15:14

5步精通B站视频转文字:开源工具的智能工作流重构指南

5步精通B站视频转文字&#xff1a;开源工具的智能工作流重构指南 【免费下载链接】bili2text Bilibili视频转文字&#xff0c;一步到位&#xff0c;输入链接即可使用 项目地址: https://gitcode.com/gh_mirrors/bi/bili2text 清晨的阳光透过窗帘缝隙&#xff0c;洒在电脑…

作者头像 李华