Linux内核调试手段实战:ftrace、kprobe、kdump与kgdb全解析

发布时间:2026/10/8 10:17:38
Linux内核调试手段实战:ftrace、kprobe、kdump与kgdb全解析
刚写完调试手段第一篇的时候有个朋友私信我说printk和动态调试虽然好用但真遇到系统panic重启、驱动卡死这种硬问题还是不知道从哪下手。这很正常printk这类手段本质上是埋点观察适合你大概知道问题在哪条路径上再去验证。可内核问题很多时候是不知道在哪儿、不知道发生了啥这时候就需要另一批更重量级的工具上场了。这篇是第3章 Linux内核调试手段之二我打算把动态追踪、崩溃转储、交互式调试这三类手段一次讲透ftrace、kprobe、kdump/crash、kgdb。它们解决的是完全不同的场景——ftrace和kprobe用来观察代码执行路径kdump用来保存死机现场kgdb用来做源码级交互调试。这几样东西配合起来覆盖掉日常工作里九成以上的内核问题排查场景。适合正在啃内核源码、写驱动遇到死机重启没头绪、或者想系统补齐内核调试能力的朋友。1. 调试手段全景先看清地图再选路1.1 内核调试的四种基本流派内核调试手段五花八门但归根结底跑不出四个大类日志观察、动态追踪、崩溃转储、交互调试。把分类搞清楚很有必要因为很多人学调试技巧最大的问题不是不会用工具而是面对一个具体问题不知道挑哪把刀。日志观察是门槛最低的一类printk、dev_dbg、动态调试都在这个范畴。它们的思路是在代码里埋点运行后看输出适合你大概知道问题位置、想验证某个假设的场景。缺点是信息是静态的埋少了看不到、埋多了刷屏而且对于已经上线的问题基本上无能为力。动态追踪则是事后插桩ftrace、kprobe、uprobe、perf都算这一类。它们的好处是不用重新编译内核、不用改代码直接在运行中的系统上动态地贴探针观察函数调用、参数、返回值。上一章我们聊过dynamic debug是动态控制printk的开关但ftrace和kprobe是更彻底的动态方案它们能让你看到完整的内核函数调用链条。崩溃转储解决的是最棘手的一类问题系统已经panic了。kdump在系统崩溃瞬间把内存镜像保存下来配合crash工具做离线分析。这玩意儿平时不显山露水但遇到那种跑几天崩一次、崩了还没日志的问题它几乎是唯一手段。交互调试就是kgdb用串口连上gdb像调试普通程序一样打断点、单步、看变量。它是最重的方案对硬件和环境要求高但能解决其他手段解决不了的逻辑难题。四类手段各有各的适用场景后面我详细展开。1.2 贴合场景的手段选型逻辑这篇选ftrace、kprobe、kdump/crash、kgdb这四样作为之二是经过考量的它们正好对应动态追踪、崩溃转储、交互调试三类手段跟前一篇覆盖的日志观察形成互补。举个例子驱动注册失败、设备节点不生成这一类问题printk能帮你在probe函数里加打印但如果驱动直接卡死在某个等待队列里或者中断处理函数异常返回导致系统hang住printk的输出可能根本来不及刷到console上。这时候ftrace能展示内核到底执行到哪一步就停止推进了kprobe能精确测量某个参数在传递过程中是否被改写。再比如那种拔插设备后系统秒崩的问题你不可能在现场用gdb慢慢跟——crash已经发生了唯一能做的就是让系统在死的时候把内存现场完整吐出来事后慢慢分析。kdump的价值就在于它是事故黑匣子panic瞬间的寄存器、堆栈、全局变量状态全都在vmcore里。kgdb则适合开发阶段的疑难杂症。比如你怀疑某个锁的使用有问题但加printk会改变时序、不改变又看不到内部状态kgdb打断点看现场就非常有用了。当然kgdb对环境要求苛刻生产环境基本用不上后面我会细说它的局限。2. 动态追踪三件套ftrace、kprobe与tracefs2.1 ftrace零改造看清内核执行路径ftrace是内核自带的动态追踪框架从2.6.x内核开始就有了经过这么多年发展它已经从最初的函数跟踪器演变成一个完整的追踪平台。我最常用的是function_graph跟踪器它能以缩进树的格式展示函数调用关系阅读体验非常友好。实际操作起来并不复杂。先把tracefs挂载上mount -t tracefs tracefs /sys/kernel/tracing然后设置跟踪器为function_graph模式cd /sys/kernel/tracing echo function_graph current_tracer这时候直接读trace文件就能看到整个内核的调用流水。但注意全量跟踪的输出量非常恐怖你根本看不出所以然。正确做法是用set_ftrace_filter做过滤只跟踪你关心的函数。比如我想看设备驱动的probe流程echo xxx_probe set_ftrace_filter echo 1 tracing_on # 触发设备probe操作 echo 0 tracing_on cat trace | head -200输出会是一棵完整的调用树从xxx_probe开始往下展开所有子函数调用。性能开销方面function_graph对系统吞吐影响很大不适合长期开着做短时间采样没问题。我自己的习惯是先预估问题可能涉及的函数过滤到只剩一层调用关系然后通过反复触发操作抓取差异。2.2 kprobe动态插桩不重编译就能下断点kprobe解决了ftrace的一个痛点ftrace能看调用关系但看不到函数参数、返回值和局部变量。kprobe的核心机制是在指定指令地址上动态插入断点指令x86上是int3ARM64上是brk命中时回调注册的handler执行完再恢复原指令继续跑。这个机制的好处是你可以把探针安插在任何函数入口、出口甚至函数内部的任意指令位置。用tracefs接口操作kprobe是最方便的方式。假设我想观察open系统调用的参数echo p:my_open do_sys_open dfd%di filename%si flags%cx mode%dx kprobe_events echo 1 events/kprobes/my_open/enable cat trace这里%di、%si、%cx、%dx是x86_64架构的函数参数寄存器约定。ARM64上要用x0~x7不同架构写法不一样新手最容易在这里踩坑。kprobe还可以抓返回值用r前缀定义kretprobeecho r:my_ret do_sys_open ret$retval kprobe_events$retval是返回值寄存器kretprobe机制会在函数返回时捕获它这对于确认函数是否执行成功、返回值是否被篡改非常有价值。kprobe最大的坑是目标函数如果被编译器内联了符号表里根本找不到它探针注册会直接失败。碰到这种情况可以先查一下/proc/kallsyms确认符号存在不存在的话就退回到ftrace或者在内核源码对应位置加printk重新编译。2.3 实战定位一次设备卡死问题我处理过一个真实案例一个USB转串口设备驱动在拔出时经常导致系统hang住。用printk在disconnect路径里加了大量log复现时却发现最后一条log永远打不全——说明系统卡在某个中间环节但printk的输出顺序和实际执行顺序可能因为缓冲区而失真。换上ftrace把disconnect相关函数全部过滤出来跟踪一下就看到了问题disconnect函数在等待一个互斥锁而这个锁在另一个CPU核上的中断上下文里正被持有着形成了典型的中断上下文和进程上下文锁竞争死锁。kprobe在锁的acquire函数上挂了探针确认了持锁路径确实没有释放。整个定位过程不到一个小时比起反复拔插设备碰运气加printk高效太多。这类问题用ftracekprobe组合拳最有效ftrace看调用路径确定卡在哪kprobe看参数和返回值确定为什么卡。没有它们嵌入式环境里很多死锁问题基本只能靠代码走读和猜。3. 崩溃现场保存kdump与vmcore分析3.1 kdump原理与配置给系统装一个黑匣子kdump的机制说起来很巧妙系统正常运行时内核会保留一小块内存专门给备用内核用。一旦主内核panickexec会快速启动备用内核备用内核做的事很简单——把崩溃瞬间的主内核内存镜像完整保存到磁盘上。这个镜像叫vmcore之后再启动一个正常的系统用crash工具离线分析它。配置kdump需要做三件事内核配置、启动参数、保存路径。内核需要打开CONFIG_KEXEC和CONFIG_CRASH_DUMP然后给主内核预留内存。在grub配置里加参数crashkernel256M这个值取决于你的系统内存总量和崩溃抓取需求一般建议至少128M内存紧张时可以加到256M。设置完重启后用cat /proc/iomem | grep Crash确认预留内存是否生效。然后配置保存行为。不同发行版配置方法略有差异以常见的/kdump.conf为例path /var/crash core_collector makedumpfile -c --message-level 1 -d 31makedumpfile的-d参数是压缩级别31表示过滤掉大部分page cache只保留内核必须的数据结构——因为分析内核崩溃用不到用户态页缓存过滤掉能大幅减小vmcore体积。生产环境建议开启压缩否则一个满内存的vmcore可能达几十GB。配置完成后可以用echo c /proc/sysrq-trigger主动触发一次panic来验证kdump是否正常生成vmcore。3.2 用crash工具解剖vmcorevmcore收好了重头戏在分析。crash工具是分析vmcore的标准武器使用前确保有与崩溃内核完全匹配的vmlinux并且内核编译时开启了CONFIG_DEBUG_INFO。crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/20250xxx/vmcore进入crash交互环境后最常用的几把刀crash bt # 查看崩溃时的函数调用栈 crash log # 查看内核环形缓冲区日志 crash ps # 查看进程状态 crash vm # 查看虚拟内存区域 crash dis -l xxx_func # 反汇编指定函数-l附带行号 crash mod -S # 加载模块符号分析驱动模块bug实际处理panic问题时我的标准流程是先log看最后几条内核日志确认panic的直接原因——是空指针、死锁、还是page fault然后bt看当前进程的调用栈找到panic确切发生在哪一行最后根据调用栈里的函数去代码里定位逻辑问题。3.3 实战一次内存越界引发的panic有次客户现场的机器频繁重启没有任何日志输出应用层完全无感知。远程排查时我先配置好kdump等崩了一次后拿到了vmcore。crash日志显示panic发生在copy_page_to_iter函数里报的是page fault。bt一看调用栈发现是某个网卡驱动的接收路径调用到这里。继续追发现驱动在构建skb时没有正确设置data长度导致后续拷贝越界访问了非法地址。整个问题用kdump一次定位没有浪费第二台机器去复现。没有kdump时这种问题几乎无法排查——系统重启后崩溃现场彻底消失printk日志最多只能帮你看个表象底层的内存破坏原因很难定位。kdump是我的核武器级调试手段任何出现不可解释重启的系统我都会第一时间配上再说。4. 用kgdb做真正的源码级调试4.1 kgdb的配置与启动方式kgdbKernel GNU Debugger让内核调试体验向用户态程序调试看齐打断点、单步执行、查看变量、修改寄存器。它通过串口与宿主机上的gdb通信需要目标机有可用的串口。内核配置需要打开这些选项CONFIG_KGDBy CONFIG_KGDB_SERIAL_CONSOLEy CONFIG_KGDB_KDBy CONFIG_DEBUG_INFOy启动参数里告诉内核用哪个串口做kgdb调试kgdbocttyS0,115200 kgdbwaitkgdbwait表示内核启动时就进入kgdb等待状态适合调试早期启动阶段的问题。如果只想调试运行中的问题就不加kgdbwait系统正常启动随时用echo g /proc/sysrq-trigger主动进入调试模式。宿主机上连接时gdb vmlinux (gdb) set serial baud 115200 (gdb) target remote /dev/ttyUSB0连上之后就是熟悉的gdb操作break设置断点、continue继续执行、next单步、print变量。中断下来后可以查看当前进程上下文、调用栈、局部变量值。4.2 kgdb的使用要点与局限kgdb虽然强大但限制也很明显。首先它需要目标机预留串口很多嵌入式板卡的串口已经被console占用需要复用或者引入额外的串口硬件。其次kgdb会让整个内核停下来这意味着所有CPU都会停顿对实时系统和外部交互场景影响很大。另外kgdb不能调试中断上下文——你在ISR里打断点是没意义的中断可能反复触发导致系统直接崩溃。它最适合的是调试驱动的正常流程逻辑比如初始化顺序、状态机跳转、异步事件的竞态问题。实际使用中我发现kgdb最有价值的场景是配合ftrace找到问题函数后用kgdb在那个函数上打断点看每一步执行时关键变量怎么变化。比加printk改变时序要好得多因为打断点不会改变原有执行流程——虽然内核会停但没有printk那种加了log就正常去掉log就复现的灵异现象。我个人经验是kgdb在纯软件调试场景下其实用得不多大量问题靠前面的动态追踪和崩溃分析能更快解决。但如果你在开发一个全新的驱动需要反复跟踪设备状态注册、硬件寄存器读写、中断请求处理的完整流程kgdb无可替代。尤其适合某些需要精确理解每一步做了什么的场景比如DMA描述符链表构建、MMIO映射顺序。4.3 没有串口时的替代交互手段串口不是随时都有的尤其是笔记本电脑调试嵌入式板卡、或者云服务器上排查问题。这种情况下kdump是你的扎实保障还有几个轻量方案可以顶替kgdb的功能KDBkgdb在串口不可用时可以切到KDB模式直接在本地键盘上操作。KDB能看寄存器、看堆栈内容、查看和修改内存但语法和gdb不同能力也弱一些。ftrace tracefs事件其实很多kgdb想看的当前值都可以通过tracefs的enable_events打开相关跟踪事件来获得。事件比kprobe提供更结构化的输出也不需要符号精确匹配。故障注入fault injection如果想验证某个错误处理路径是否完善不必通过kgdb硬改变量内核自带fail_make_request、fail_page_alloc等故障注入框架模拟内存分配失败、IO错误等场景。对我来说kgdb解决的是深度逻辑调试而平时更多问题在路径确认和现场还原层面不必每次都上kgdb这么重的工具。但很多从应用开发转过来的人潜意识里想找的就是内核版的gdb。如果想要的是这个kgdb确实是唯一的标准答案。5. 定位内核问题的排查路线先易后难由表及里5.1 一份可复现的临场排查流程结合多年的调试经验我总结出一条比较管用的临场处理流程按优先级排列先看dmesg日志确认有没有明显的oops、panic、WARN_ON输出。如果有直接看panic堆栈定位到函数。如果日志里只有部分痕迹或者说系统hang住——用ftrace跟踪相关路径确定代码停在哪一步。如果问题是偶发死机、重启马上配置kdump等着保存崩溃现场。拿到vmcore后用crash分析确认崩溃点、调用栈、寄存器状态。如果还是搞不定且硬件环境允许上kgdb做交互调试。这套流程的核心思路是由表及里从代价最小的手段开始。printk和日志最轻量但能力有限ftrace和kprobe能提供路径级信息适用面最广kdump是兜底方案任何时候崩了都有现场kgdb作为终极手段只在前面都失灵时动用。5.2 手段对比不同场景的选型推荐问题类型推荐手段原因驱动初始化失败ftrace kprobe看probe流程走到哪参数是否合法系统panic重启kdump crash保存崩溃现场离线分析内核hang住无日志ftrace function_graph找出卡死的函数路径偶发内存越界kdump crash 代码走读分析内存破坏来源驱动逻辑疑难kgdb源码级断点调试性能瓶颈ftrace perf找出热点函数个人经验就是第一优先用ftrace和kprobe把问题范围缩小到具体函数再用其他手段深入。别一开始就指望kgdb把问题直接看明白——内核态代码的复杂度决定了问题往往是一系列调用路径组合导致的没有基本路径分析直接下断点很容易被表象误导。5.3 从之一到之二调试能力的体系化前一篇我们讲了printk、动态调试这类基础手段它们解决的是有没有执行到这个分支的问题。这篇的ftrace、kprobe、kdump、kgdb则往上走了一个台阶解决的是执行序列是什么样的崩溃瞬间发生了什么为什么走到这一步的更深层问题。真正在内核上调试解决问题靠的是组合运用而不是单点工具。比如ftrace定位路径kprobe判断参数kdump保存现场kgdb做细节确认。每一种手段都有自己的边界跨过边界就要换工具。这也是为什么我一直强调先把手段分类搞清楚的重要性——知道问题属于哪一类自然知道该用哪一把刀。6. 常见问题与排查技巧实录6.1 高频问题与解决思路速查表现象可能原因排查思路与操作ftrace 无输出或输出为空tracefs未挂载/权限不足/跟踪器未开启mount -t tracefs tracefs /sys/kernel/tracing检查tracing_on是否为1kprobe 写入kprobe_events报错符号不存在或被内联优化cat /proc/kallsyms确认符号到源码里找非内联的实现函数kdump 触发后无vmcore生成crashkernel参数未生效/保存路径错误cat /proc/iomem查看Crash保留区域检查kdump服务状态和配置路径crash 无法加载vmlinux版本不匹配/缺少调试符号用带CONFIG_DEBUG_INFO编译的vmlinux确保vmlinux与内核完全同版本kgdb 无法通过串口连接串口参数不一致/引导参数未生效核对两者波特率确认启动日志里kgdboc被正确解析内核频发但日志看不见panicpanic立即重启且日志丢失调高/proc/sys/kernel/panic或设成-1配置kdump作为兜底ftrace 输出量过大set_ftrace_filter未设置过滤范围尽量精确过滤目标函数配合grep和timeout提取有效信息这张表是我自己在工作中翻来覆去比对总结出来的每条都是真实踩过的坑。尤其kdump那个不生成vmcore的问题十有八九是crashkernel预留内存没生效查/proc/iomem一眼就能看出来。6.2 实战心得避坑经验与新工具趋势写了不少代码和调试经验最后分享几个真正帮过我的中小技巧第一个是善用 tracefs 里的 per-event enable。很多人追踪kprobe事件时写入kprobe_events后直接看trace文件忘了给对应的kprobe事件开enable。结果trace文件要么显示探针未激活要么什么都没有。正确顺序是写kprobe_events之后echo 1 events/kprobes/xxx/enable然后再触发你的测试操作最后cat trace。第二个是动态调试和ftrace配合决策。模块加载之后经常发现某个函数的行为和代码走读不一致。我会先在模块编译时开启dynamic debug看函数入口有没有被调用如果入口没进用ftrace看是不是内联掉或走错分支如果入口进了但行为不对上kprobe看参数值。这套诊断逻辑帮助我在很多模块级bug里迅速定位。第三个是多核环境下的kdump必要配置。有些崩溃和CPU间交互有关crash在分析时默认只dump崩溃的CPU现场。但如果想看其他CPU在干什么用crash的foreach bt命令可以列出所有CPU的调用栈。这个在分析软锁死soft lockup问题时尤其有用——往往能看到另一个CPU持锁不放。最后聊聊新趋势。内核调试这几年也在不断进化比如内核自带的大量tracepoint事件、BPF工具链对内核可观测性的提升、以及Rust内核模块带来的安全调试思路变化。但基本功里的ftrace、kprobe、kdump、kgdb依旧是一切上层工具的地基搞懂它们的原理遇到任何新工具都能快速上手。调试内核这事儿说到底是个系统性工程。每次困惑于系统为什么跑着跑着就死了其实就是还没找到合适的观察角度。上面这些手段和方法我用了很多年也都实打实帮我定位过各种疑难杂症。如果你在调试中遇到了其他棘手的场景不妨顺着这个思路先给问题分个类再看看手里有哪些工具大概率能少走很多弯路。