Linux内核调试实战指南:从工具选型到故障排查全攻略 最近我把这些年跑内核问题攒下来的调试笔记重新翻了出来准备整理成一个持续更新的专栏导航。内核调试这个领域最大的问题是资料太散官方文档讲原理邮件列表讲案例工具自带的 man page 讲参数到了现场往往是三者凑不到一起去。特别是碰到系统直接 hang 死、内存静默被踩、驱动莫名丢中断这类问题如果方向不对光靠打日志能把人打崩溃。这篇内容就是把我整理导航时的思路、工具选型、真实排查路径和踩过的坑一起铺开讲清楚给正在学 Linux 内核、做驱动开发或者维护嵌入式系统的朋友一份能直接落地的参考地图。我整理了相当长一段时间之后发现能解决问题的其实不是某个单一工具而是“症状—子系统—工具—动作”之间的映射关系。你面对的问题类型不一样选用的调试模块和调试工具组合就完全不一样。所以这份导航不打算做成工具清单堆砌而是按“调试模块、调试工具、实战案例”三条主线来组织。下面我会先把专栏的整体分类讲清楚然后带大家过一遍核心工具的选型逻辑再用几个高频故障场景走一遍标准动作最后把环境搭建和排坑细节都摊开来说。1. 专栏的整体定位与目录设计思路1.1 为什么需要一份“会持续更新”的内核调试导航内核调试的知识有一个很别扭的特点它的保质期是有限的。Linux 内核版本迭代太快5.x 里能用的 tracefs 事件名到 6.x 可能就改了旧版 perf 的脚本到新版上会提示语法失效连 kprobe 的 event 文件格式都变过几次。我早年记的很多笔记后来照着敲发现输出对不上最后才意识到不是命令错了是内核版本变了。所以这份导航从设计之初就确定要持续更新。每一篇调试笔记都要写清楚内核版本、架构、工具版本方便读者对照。遇到接口变化老经验会标注过期保留历史价值的同时不让读者踩坑。在这个领域“持续更新”不是编辑部的口号而是技术内容本身的基本要求。还有一层原因内核调试的现场往往很贵。生产环境出问题每一分钟都在烧钱你不可能现场翻源码、试参数。所以导航必须做到“按图索骥”看到什么现象能快速定位到该用哪个调试模块、哪套工具链、按什么顺序执行。我整理这份导航本质上就是在给自己做一张能在紧张时刻快速查阅的作战地图。1.2 三大栏目怎么划分调试模块、调试工具、实战案例专栏目录我分了三大块。第一块是“调试模块”指的是内核自身提供的调试机制和内核模块比如 lockdep 死锁检测、KASAN 内存检测、KCSAN 数据竞争检测、tracefs/debugfs 这些子系统它们是内核内置的能力不开对应编译选项根本用不上。第二块是“调试工具”指的是运行在内核之外的辅助程序包括 perf、ftrace 的 trace-cmd、crash、gdb/kgdb、BCC/bpftrace也包括 sysrq、dmesg 这类基础救命手段。第三块是“实战案例”每个案例都对应一个具体症状从现象、排查到最终定位完整走一遍。打个比方调试工具是医生手里的听诊器调试模块是设备自带的自检灯实战案例就是过往的病历本。听诊器告诉你哪里可能有杂音自检灯告诉你哪个部件报警病历本让你知道类似症状以前是怎么确诊的。三者缺一不可。这种分类最大的好处是入口清晰。读者遇到问题能从案例库找到相似症状再看引导指向哪个调试模块最后用对应工具复盘。不会出现新手一上来就抱着 crash 工具读 vmcore结果连 512 字节物理地址怎么换算都搞不清的尴尬情况。1.3 不同基础的读者怎么使用这份导航新手我建议先走“现象→信息收集”这条路。系统出问题了先别急着编译内核、挂 gdb第一动作是把 dmesg、/var/log/messages、串口日志完整保存下来然后用sysrq拿到任务栈再用perf top粗看热点。这套流程不是最炫的但能覆盖大多数问题。有经验的开发者可以深入到“机制→插桩”这一层。当你怀疑某个函数执行路径不对时用 ftrace 的 function_graph 看调用关系用 kprobe 在关键函数上挂探针都比盲打日志高效得多。到了高级阶段就需要搭建 QEMU 实验环境配合 KGDB/JTAG 做交互式调试再用 crash 做离线分析形成完整的闭环。我建议每个读者从自己的实际场景出发先在导航里定位“我现在是哪一类问题”再进入对应小节。如果只是驱动开发可以先看设备中断和内存映射相关案例如果是性能调优直接跳到调度延迟和 CPU 热点部分。这套导航不是教科书不需要从头读到尾。2. 内核调试工具全景观测、追踪、回放三类工具怎么选2.1 先搞清楚调试需求再选工具我见过太多人一开 perf 就去抓系统忙了一下午数据是一大堆可不知道自己要找什么。选工具之前一定要先回答两个问题症状是什么最可疑的内核子系统是哪个这里我把常见问题分成四大类系统无响应、性能下降、内存破坏、驱动功能异常。每一类对应的排查路径完全不同。系统无响应优先看中断、软锁、自旋锁性能下降优先看调度、锁竞争、IO 等待内存破坏优先开 KASAN/kmemleak驱动异常优先看中断请求、DMA 映射、设备树配置。问题类型典型症状首选工具组合系统无响应/Hang串口无输出、屏幕冻结、看门狗触发sysrq dmesg crash 分析 vmcore性能下降CPU 单核飙高、延迟变大、吞吐降低top/vmstat perf ftrace内存破坏随机崩溃、数据结构被改写、UAFKASAN kmemleak crash驱动异常设备不工作、中断频繁、数据错乱dmesg /proc/interrupts kprobe把问题归类之后工具选择就顺理成章了。没有必要每个工具都精通但一定要知道每个工具能回答哪一类问题。2.2 运行态观测工具从 top 到 sysrq运行态观测是排查的第一步。top、vmstat、iostat、pidstat、sar这些工具看起来基础但它们能快速帮你建立全局印象哪个 CPU 忙、哪个进程在 D 状态、IO 是否卡住。我习惯先把这些数据存一份再开始深挖因为很多内核问题只有在高负载下才会暴露现场数据没了就再也找不回来。/proc/interrupts和/proc/softirqs是容易被忽视的好东西。驱动丢中断、中断风暴这类问题第一眼就要看这两个文件。比如某个网卡的中断次数在几十毫秒内飙升说明中断处理逻辑可能异常如果某个 CPU 上软中断占比极高就要怀疑是否是网络收包路径触发了调度问题。sysrq是系统几乎无响应时的最后手段。内核在紧急情况下仍然会处理魔术键组合echo t /proc/sysrq-trigger可以打印所有任务栈echo w /proc/sysrq-trigger可以打印 D 状态任务echo m /proc/sysrq-trigger可以导出内存信息。注意echo c /proc/sysrq-trigger会故意触发崩溃生成 vmcore只能在测试环境操作别在生产环境手滑。2.3 追踪与事件工具ftrace、perf、eBPFftrace 是我最依赖的追踪工具没有之一。它内建在 Linux 内核中开销低不需要额外安装几乎每个发行版都支持。基础用法是查看tracefs下的available_filter_functions选择感兴趣的函数开启追踪然后读取trace文件。用function_graph模式可以看到函数级调用链定位问题比打日志精确一个量级。perf 的优势是硬件性能计数器和采样。perf top能实时看到内核函数级别的 CPU 热点perf record可以把采样数据保存下来再通过perf report或者perf script做深度分析。我遇到内核态 CPU 占用异常时会先perf record -g记录调用栈然后看是不是某个驱动函数被频繁调用或者某个自旋锁等待路径占了大量 CPU。eBPF 系列工具适合在目标环境内做动态观测。BCC/bpftrace 可以挂 kprobe/uprobe/tracepoint在不重启、不加载内核模块的前提下输出自定义信息。需要注意权限和安全边界生产环境要求内核支持 BPF 且开启相关配置还要注意部分内核函数被标记为不可追踪。2.4 故障转储与离线分析kdump、crashkdump 是内核崩溃时的“黑匣子”。系统 panic 后kdump 内核会接管并把内存镜像保存成 vmcore。crash 工具随后可以打开 vmcore查看当时的每个 CPU 状态、每个任务的栈、各种内核数据结构。这种“事后回放”的能力是线上定位的终极手段。很多人一开始会被 crash 的交互命令吓到觉得门槛高。其实核心命令就那几条bt查看栈ps列任务struct读取数据结构dis反汇编。配合带调试信息的 vmlinux基本能还原崩溃现场。需要注意crash 分析效果高度依赖编译内核时保留的调试信息与 System.map。线上内核如果被 strip 过或者跑的版本和你手头的 vmlinux 不一致很多命令会直接报错。所以我牵头维护的项目都会把每一版内核对应的 vmlinux、System.map、模块符号表归档保存这是用最小存储成本换最省心的排障体验。2.5 内核态/用户态联合调试KGDB、QEMU、JTAG当问题复杂到日志和追踪都无法推进时就需要交互式调试。KGDB 允许两台机器之间通过串口或网络建立 gdb 连接内核里打断点、单步、查看变量。QEMU 自带 gdb stub虚拟机和 gdb 之间直接连接非常适合复现驱动问题。我为什么强调 QEMU因为很多内核问题需要反复修改验证用真机一次一次重启效率太低。QEMU 里跑一个最小系统几秒钟就能复现一次还方便快照回滚。配合 nokaslr 参数断点地址稳定调试体验会好很多。嵌入式场景还会用到 JTAG直接在硬件上控制 CPU 执行流程能查 cache、MMU、寄存器这是纯软件调试替代不了的。交互式调试不是万能的。它最好在测试环境用生产环境不具备条件而且一旦面对死锁和中断上下文异常单步执行反而会因时序改变而无法复现。我的经验是交互调试解决“逻辑错误”追踪和转储分析解决“时序与状态问题”两者要配合着用。3. 按真实场景走一遍故障定位的标准动作3.1 系统 hang 死不响应如何拿到第一手现场系统完全卡死的时候第一反应不是重启而是想尽办法保留现场。如果机器有串口一定要把串口日志完整保留下来这是最可信的证据。没有串口可以配置 netconsole 把内核日志通过网络发送到另一台机器虽然丢日志的概率高一点但总比什么都没有强。拿到串口后先看最后打印的内核日志通常是死锁检测、软锁、硬锁或者 OOM。如果没有打印任何东西用 SysRq 生成任务栈。步骤如下# 开启 sysrq echo 1 /proc/sys/kernel/sysrq # 打印所有 CPU 上的任务栈 echo t /proc/sysrq-trigger # 打印 D 状态任务不可中断睡眠 echo w /proc/sysrq-trigger # 查看输出 dmesg -T | tail -200有一次线上设备卡死串口最后只看到一次中断风暴的暗示没有 panic。我通过echo t拿到所有栈发现多个进程阻塞在同一个 spinlock 上顺着持锁栈往下查定位到是某驱动在关闭中断的临界区里调用了可能睡眠的接口。这类问题的关键就是快、准、不重启。3.2 内存越界、UAF 和泄漏的排查路径内存问题最坑的地方是会延迟暴露。写入越界不一定会立刻崩溃往往等到那块内存被其他模块复用才爆发那时候栈早就不知道飘到哪里去了。所以能提前开防御机制是最好的。KASAN 是内核的动态内存检测器能实时捕获越界访问、use-after-free、double free。用它重新编译内核代价是运行速度明显变慢内存占用也高适合测试环境复现。对于已经稳定复现的泄漏kmemleak是专门找“丢失的内存块”的利器它会扫描内存并报告疑似未释放的分配点。我处理过一例网络驱动的 UAF驱动在卸载时不等待发送队列清空就释放了 skb结果中断回调还持有旧指针。现场用 crash 查看 sk_buff 头部发现类型标记已经变成其他 slab 对象。复现环境开 KASAN 之后报错信息直接把释放函数和再次访问函数都打印出来了代码走查半小时就锁定了。3.3 CPU 占用异常与调度延迟单核 CPU 跑满在top里看到的是 100% 的 us 或 sy。但这最多告诉我们问题出在用户态还是内核态具体是哪个路径还得靠采样工具。先看进程级的pidstat -d -p再看内核热点perf top -C 3 perf record -g -C 3 -o perf.data -- sleep 30 perf report -i perf.data如果热点集中在一个不太起眼的内核函数上比如某个__do_softirq或cpumask_next那就要小心了很可能是一个高优先级线程在反复唤醒、旋转。调度延迟问题我会配合 ftrace 看sched_switch事件确认哪个任务在抢占 CPU。之前还碰到过 eventfd 唤醒机制引起的惊群效应生产者线程每轮唤醒成百上千个等待者处理方式是检查等待队列和唤醒条件用 ftrace 事件把唤醒路径打出来才看清楚。3.4 自旋锁误用、睡眠与死锁在 ARM64 上spinlock 临界区里睡眠是死锁最快的捷径之一。自旋锁本身就是“等待者原地旋转”的锁持锁者睡眠不释放其他 CPU 上所有抢锁任务全都跟着空转整个系统转不动看门狗接着触发。lockdep 是解决这类问题的神器它会主动检测锁序错误、递归锁、spinlock 内睡眠等场景。前提是编译内核时开启CONFIG_LOCKDEPy启动后系统会在检测到异常时直接输出警告。# 查看锁统计信息 cat /proc/lock_stat # 查看当前所有锁依赖信息 cat /proc/lockdep我看到过一次典型的 ARM64 死锁场景驱动在中断上下文里调用了一个试图获取普通互斥锁的函数中断优先级高于持锁进程于是持锁进程永远得不到调度整个 CPU 卡死。打开 lockdep 后它立刻指出“possible circular locking dependency detected”这个提示能省半天甚至一天的排查时间。4. 内核调试环境搭建与我的踩坑记录4.1 用 QEMU 快速搭一个随时可以复现的实验内核QEMU 调试环境是我最常用的“工地”。它的价值不在于性能而在于可控。你可以随时给内核发 break、修改内存、重新编译几秒钟启动一版新内核这比每次用真机测试效率高太多了。搭建流程也不复杂# 编译内核开启调试选项 make ARCHarm64 defconfig make ARCHarm64 menuconfig # 关键开关 # CONFIG_DEBUG_INFOy # CONFIG_DEBUG_KERNELy # CONFIG_KGDBy # CONFIG_DEBUG_INFO_DWARF4y # 制作最小根文件系统busybox make ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- -j$(nproc) # 启动 QEMU关闭地址随机化以方便断点 qemu-system-aarch64 -machine virt -cpu cortex-a57 \ -kernel arch/arm64/boot/Image -initrd rootfs.cpio.gz \ -append consolettyAMA0 nokaslr panic-1 \ -nographic \ -s -S-s是让 QEMU 监听端口 1234 的 gdb stub-S是启动即暂停等待调试器连接。这时候 gdb 远程连接target remote :1234就可以对内核进行单步调试了。很多新手不知道nokaslr的重要性内核地址随机化开启时软件断点地址每次启动都变调试体验很差所以实验环境一定要关。4.2 串口/网络调试连接与日志收集真实板子调试串口是最可靠的伙伴。内核启动参数加consolettyS0,115200日志就会打到串口。串口不仅是日志出口还承担着 KGDB 的连接通道。启动参数里加kgdbocttyS0,115200主机侧用 gdb 连接串口就能进入内核调试模式。网络日志收集可以用 netconsole 替代物理串口# 配置 netconsole目标IP:端口 sudo modprobe netconsole netconsole/eth0,192.168.1.100:6666/嵌入式系统没有串口时netconsole 是把日志导出的救命通道。我踩过最大的坑是波特率不匹配板子上实际是 115200上位机终端却开成 9600看到一堆乱码还以为系统启动失败。另外串口日志建议直接落盘不要只留在屏幕缓冲区否则崩溃日志一多前面的关键信息容易被冲掉。4.3 编译选项与符号表没有调试信息的反汇编很要命调试内核的前提是符号齐全。建议至少开启CONFIG_DEBUG_INFOy生成的 vmlinux 里就会包含 DWARF 调试信息。crash 工具打开 vmcore 时如果没有匹配的 vmlinux函数栈只能显示地址你还得一个个对着 System.map 手动换算极其痛苦。模块的符号表同样重要。比如某个.ko是问题源头你需要用与线上完全一致的内核源码和编译器重新编译它才能用 crash 加载模块调试信息。有一次我们线上内核的 GCC 版本比仓库里标准镜像高了两个小版本导致模块调试信息对不上整整折腾了一个下午。后来我们把工具链版本写进构建脚本这个问题才算根治。保存产物时我习惯按这组文件归档vmlinux、System.map、所有*.ko文件、.config。磁盘占用不大但在排障时价值极高。很多人只在出事之后才后悔没存我建议从项目第一天就建立这个目录结构。4.4 老内核更换与多版本共存调试中经常遇到发行版自带内核版本太旧或太新。Ubuntu 上切换默认内核最直接的办法是修改/etc/default/grub里的GRUB_DEFAULT指向menuentry标签或者用GRUB_DEFAULTsaved配合grub-set-default 1指定序号。改完执行sudo update-grub sudo rebootDebian 升级内核后第三方驱动比如英伟达闭源驱动经常因为内核模块接口变化而无法加载。这种问题一般需要重新构建驱动对应的 DKMS 模块。升级前先确认旧内核可以正常引导再升级新内核不要手快把旧头文件全删了。至少保留一个已知能启动的内核万一新内核引导失败还有退路。还有很多人会问能不能直接卸载旧内核释放磁盘空间可以但建议删之前先跑一遍dpkg --list | grep linux-image认准正在运行的版本号别把自己当前的内核卸掉。稳妥操作是保留最近两个版本其他清理。5. 进阶组合玩法把多个工具串成一个闭环5.1 ftrace 打底perf 定位热点crash 收尾单独用 perf 看热点能发现“某个函数占用 CPU 高”但没有调用来源解释单独用 ftrace 追踪函数调用能看清路径但开销大脖子以下全是喘。我把它们串起来分三步走先用 perf 快速缩小范围到某类函数再用 ftrace 追踪关键路径的完整调用序列最后出现崩溃或需要核验数据结构时用 crash 打开现场。举一个实例某网卡吞吐量严重下滑perf top显示softirq路径占了 60% CPU。接着用 ftrace 开启function_graph追踪napi_poll和网卡驱动的poll函数发现驱动每轮只处理少量包就返回导致 NAPI 调度频繁。最后看代码原来是收包预算变量被错误地清零。整个定位过程不到两小时如果没有分层工具可能还在盲改驱动参数。5.2 eBPF 动态插桩的落地姿势eBPF 最适合在不改动业务代码的情况下临时插桩。我常用 bpftrace 写一行就能挂探针bpftrace -e kprobe:netif_receive_skb { [comm] count(); }这种动态统计能快速回答“哪些进程在内核收包路径上最活跃”。kprobe 可以在任意函数入口插桩但要注意内核中部分函数被标记为notrace或blacklist这类函数挂不上去。用bpftrace -l先确认函数是否存在再挂探针。生产环境使用 eBPF要评估内核版本、BTF 支持和权限模型。有些老内核没有 CONFIG_DEBUG_INFO_BTF现代 CO-RE 工具就可能加载失败。只要条件允许我倾向于用 eBPF 做长时观测因为它开销可控相比加载自定义内核模块风险低得多。5.3 lockdep 与 lock_stat把死锁变成可见输出很多人把 lockdep 当成“出了问题再开”的调试选项这不是好习惯。lockdep 应该从内核开发阶段就开启早发现问题比事后分析成本低一个数量级。它在每次获取锁时都会记录锁依赖关系形成一张依赖图一旦检测到环就立刻输出。/proc/lock_stat能给出每个锁的竞争统计等待次数、等待时长、持有者。我查锁竞争时常用它辅助判断是“某个锁真的被频繁争用”还是“某个误用导致持有时间过长”。在 ARM64 多核场景下锁竞争经常出现在共享 cache line 上perf c2c还能进一步揪出 cacheline 伪共享问题。lockdep 开启后的系统会明显变慢但它值得。我曾经在生产环境临时打开过一次 lockdep抓到了驱动注册路径里的一个递归锁问题日志直接指向两个函数五分钟就确认了改法。5.4 tracefs 和 debugfs 的“调试模块”自组织“内核调试模块”这个概念我理解成两层。一层是内核编译时内建的调试子系统tracefs、debugfs、lockdep、KASAN 等。另一层是你可以动态加载的、专门用于复现或观测的内核模块。很多时候我要自定义打印某个内部数据结构时并不需要改主线代码直接写一个小的观测模块挂到 debugfs 下加载、观测、卸载非常灵活。tracefs 下的events目录提供了大量现成事件点。可以用 echo 直接开启事件mount -t tracefs tracefs /sys/kernel/tracing echo 1 /sys/kernel/tracing/events/sched/sched_wakeup/enable echo function /sys/kernel/tracing/current_tracer echo sys_nanosleep /sys/kernel/tracing/set_ftrace_filter cat /sys/kernel/tracing/trace_pipe这种方式不需要写一行代码就能看到特定事件流。尤其是调度、唤醒、timer、irq 这些子系统事件点覆盖很全调试入门必会。等等debugfs和tracefs在生产环境默认不挂载或只读挂载你自己调试时也要注意权限和日志量别在业务高峰期制造额外负担。6. 专栏导航持续更新的机制与规划6.1 什么样的内容会进这个导航这个导航不是流水账。我要求每一篇笔记必须满足三个条件有可复现的命令序列、有明确的版本信息、有真实案例作为支撑。纯理论或者“我感觉”类的内容不收录因为内核调试里没有现场验证过的建议都可能误导别人。案例本身会做脱敏处理不涉及具体公司业务数据但保留技术细节。具体到哪一次 panic、哪一个函数调用路径、哪一个结构体字段我都会写清楚。这样读者可以直接对照自己的环境做验证而不是看一堆“某系统出现异常”的空话。更新节奏大概两周一更每次补充一到两篇新工具或新案例。遇到新内核版本发布会优先检查既有内容是否失效失效的内容会直接标注并补充新版本行为。导航的价值在于时效性和可信度如果内容停更半年再全也只能当历史资料读。6.2 工具与内核版本演进带来的维护策略内核调试工具随着内核版本变动很快。perf 的参数、tracefs 事件路径、kprobe event 写法、BPF 内核接口都在持续变化。我们的维护策略是每篇文章首行标注适用的内核版本和架构比如 “Linux 5.15x86_64 / arm64”。这样即使后续版本改了格式读者也能判断手上的内容是否还可直接照用。另一个策略是保留废弃内容但不删除。比如旧版 tracefs 路径/sys/kernel/debug/tracing在新内核中被迁移到/sys/kernel/tracing那旧路径的内容我不直接删而是加一段“新版本注意”说明迁移原因和影响。内核调试圈子里不少老博客失效就是因为只写当时能用的路径没预留版本演进的余量。对于社区新增的工具和库我会定期跟进特别是 BCC 替代工具链、基于 libbpf 的轻量工具、内核官方 perf 新子命令这些方向。保持导航与主流内核版本同步比文章数量多更重要。6.3 读者参与与路线图持续更新只有我一个人维护肯定不够。我建议读者提交问题或者补充资料时至少带上这些信息内核版本、CPU 架构、发行版/构建方式、复现步骤、原始日志。只有这些信息齐排查建议才可能准确。没有日志、没有版本、只有一句“系统崩了”任何人都没法给出可靠结论。接下来专栏会分三个方向走第二阶段重点完善 QEMU 复现环境系列把常见内存问题、死锁问题做成可直接启动的镜像案例第三阶段做性能专题聚焦调度延迟、锁竞争、DMA 和中断路径再往后会加入更多 ARM64 特有调试场景比如 cache 一致性、MMU 页表问题、固件交互等。我个人在实际操作中的体会是维护这份导航的过程本身就是最好的学习方式。每次为了写清楚一个问题去重新跑一遍实验往往能发现当年排查时忽略的细节。调试内核最贵的成本从来不是工具授权或者机器资源而是大脑里那张“症状—原因—工具”的映射表。工具再多不进现场跑一遍都是摆设。这份导航会一直留在我的工作流里随着每次真实排障继续生长。