Linux kernel调试方法及工具

3313 2025-07-08 11:43:21
"调试难度本来就是写代码的两倍. 因此, 如果你写代码的时候聪明用尽, 根据定义, 你就没有能耐去调试它了." --Brian Kernighan1 内核调试以及工具总

"调试难度本来就是写代码的两倍. 因此, 如果你写代码的时候聪明用尽, 根据定义, 你就没有能耐去调试它了."

--Brian Kernighan1 内核调试以及工具总结内核总是那么捉摸不透, 内核也会犯错, 但是调试却不能像用户空间程序那样, 为此内核开发者为我们提供了一系列的工具和系统来支持内核的调试.

内核的调试, 其本质是内核空间与用户空间的数据交换, 内核开发者们提供了多样的形式来完成这一功能.

工具 描述debugfs等文件系统 提供了 procfs, sysfs, debugfs以及 relayfs 来与用户空间进行数据交互, 尤其是 debugfs, 这是内核开发者们实现的专门用来调试的文件系统接口. 其他的工具或者接口, 多数都依赖于 debugfs.printk 强大的输出系统, 没有什么逻辑上的bug是用PRINT解决不了的ftrace以及其前端工具trace-cmd等, 内核提供了 ftrace 工具来实现检查点, 事件等的检测, 这一框架依赖于 debugfs, 他在 debugfs 中的 tracing 子系统中为用户提供了丰富的操作接口, 我们可以通过该系统对内核实现检测和分析. 功能虽然强大, 但是其操作并不是很简单, 因此使用者们为实现了 trace-cmd 等前端工具, 简化了 ftrace 的使用.kprobe以及更强大的systemtap 内核中实现的 krpobe 通过类似与代码劫持一样的技巧, 在内核的代码或者函数执行前后, 强制加上某些调试信息, 可以很巧妙的完成调试工作, 这是一项先进的调试技术, 但是仍然有觉得它不够好, 劫持代码需要用驱动的方式编译并加载, 能不能通过脚本的方式自动生成劫持代码并自动加载和收集数据, 于是systemtap 出现了. 通过 systemtap 用户只需要编写脚本, 就可以完成调试并动态分析内核KGDB 是大名鼎鼎的内核调试工具, KGTP则通过驱动的方式强化了 gdb的功能, 诸如tracepoint, 打印内核变量等.Perf是一款随 inux内核代码一同发布和维护的性能诊断工具, 核社区维护和发展. 不仅可以用于应用程序的性能统计分析, 也可以应用于内核代码的性能统计和分析. 得益于其优秀的体系结构设计, 越来越多的新功能被加入 Perf, 使其已经成为一个多功能的性能统计工具集。LTTng 是一个 Linux 平台开源的跟踪工具, 是一套软件组件, 可允许跟踪 Linux 内核和用户程序, 并控制跟踪会话(开始/停止跟踪、启动/停止事件 等等).

2 用户空间与内核空间数据交换的文件系统内核中有三个常用的伪文件系统: procfs, debugfs和sysfs.

文件系统 描述procfs, The proc filesystem is a pseudo-filesystem which provides an interface to kernel data structures.sysfs, The filesystem for exporting kernel objects.debugfs, Debugfs exists as a simple way for kernel developers to make information available to user space.relayfs, A significantly streamlined version of relayfs was recently accepted into the -mm kernel tree.它们都用于Linux内核和用户空间的数据交换, 但是适用的场景有所差异:

procfs 历史最早, 最初就是用来跟内核交互的唯一方式, 用来获取处理器、内存、设备驱动、进程等各种信息.

sysfs 跟 kobject 框架紧密联系, 而 kobject 是为设备驱动模型而存在的, 所以 sysfs 是为设备驱动服务的.

debugfs 从名字来看就是为 debug 而生, 所以更加灵活.

relayfs 是一个快速的转发 (relay) 数据的文件系统, 它以其功能而得名. 它为那些需要从内核空间转发大量数据到用户空间的工具和应用提供了快速有效的转发机制.

2.1 procfs文件系统ProcFs 介绍procfs 是比较老的一种用户态与内核态的数据交换方式, 内核的很多数据都是通过这种方式出口给用户的, 内核的很多参数也是通过这种方式来让用户方便设置的. 除了 sysctl 出口到 /proc 下的参数, procfs 提供的大部分内核参数是只读的. 实际上, 很多应用严重地依赖于procfs, 因此它几乎是必不可少的组件.

2.2 sysfs文件系统内核子系统或设备驱动可以直接编译到内核, 也可以编译成模块, 编译到内核, 使用前一节介绍的方法通过内核启动参数来向它们传递参数, 如果编译成模块, 则可以通过命令行在插入模块时传递参数, 或者在运行时, 通过 sysfs 来设置或读取模块数据.

Sysfs 是一个基于内存的文件系统, 实际上它基于ramfs, sysfs 提供了一种把内核数据结构, 它们的属性以及属性与数据结构的联系开放给用户态的方式, 它与 kobject 子系统紧密地结合在一起, 因此内核开发者不需要直接使用它, 而是内核的各个子系统使用它. 用户要想使用 sysfs 读取和设置内核参数, 仅需装载 sysfs 就可以通过文件操作应用来读取和设置内核通过 sysfs 开放给用户的各个参数:

mkdir -p /sysfsmount -t sysfs sysfs /sysfs注意, 不要把 sysfs 和 sysctl 混淆, sysctl 是内核的一些控制参数, 其目的是方便用户对内核的行为进行控制, 而 sysfs 仅仅是把内核的 kobject 对象的层次关系与属性开放给用户查看, 因此 sysfs 的绝大部分是只读的, 模块作为一个 kobject 也被出口到 sysfs, 模块参数则是作为模块属性出口的, 内核实现者为模块的使用提供了更灵活的方式, 允许用户设置模块参数在 sysfs 的可见性,并允许用户在编写模块时设置这些参数在 sysfs 下的访问权限, 然后用户就可以通过 sysfs 来查看和设置模块参数, 从而使得用户能在模块运行时控制模块行为.

2.3 debugfs文件系统内核开发者经常需要向用户空间应用输出一些调试信息, 在稳定的系统中可能根本不需要这些调试信息, 但是在开发过程中, 为了搞清楚内核的行为, 调试信息非常必要, printk可能是用的最多的, 但它并不是最好的, 调试信息只是在开发中用于调试, 而 printk 将一直输出, 因此开发完毕后需要清除不必要的 printk 语句, 另外如果开发者希望用户空间应用能够改变内核行为时, printk 就无法实现.

因此, 需要一种新的机制, 那只有在需要的时候使用, 它在需要时通过在一个虚拟文件系统中创建一个或多个文件来向用户空间应用提供调试信息.

有几种方式可以实现上述要求:

使用 procfs, 在 /proc 创建文件输出调试信息, 但是 procfs 对于大于一个内存页(对于 x86 是 4K)的输出比较麻烦, 而且速度慢, 有时会出现一些意想不到的问题.

使用 sysfs( 2.6 内核引入的新的虚拟文件系统), 在很多情况下, 调试信息可以存放在那里, 但是sysfs主要用于系统管理,它希望每一个文件对应内核的一个变量,如果使用它输出复杂的数据结构或调试信息是非常困难的.

使用 libfs 创建一个新的文件系统, 该方法极其灵活, 开发者可以为新文件系统设置一些规则, 使用 libfs 使得创建新文件系统更加简单, 但是仍然超出了一个开发者的想象.

为了使得开发者更加容易使用这样的机制, Greg Kroah-Hartman 开发了 debugfs(在 2.6.11 中第一次引入), 它是一个虚拟文件系统, 专门用于输出调试信息, 该文件系统非常小, 很容易使用, 可以在配置内核时选择是否构件到内核中, 在不选择它的情况下, 使用它提供的API的内核部分不需要做任何改动.

2.4 relayfs文件系统relayfs 是一个快速的转发(relay)数据的文件系统, 它以其功能而得名. 它为那些需要从内核空间转发大量数据到用户空间的工具和应用提供了快速有效的转发机制.

Channel 是 relayfs 文件系统定义的一个主要概念, 每一个 channel 由一组内核缓存组成, 每一个 CPU 有一个对应于该 channel 的内核缓存, 每一个内核缓存用一个在 relayfs 文件系统中的文件表示, 内核使用 relayfs 提供的写函数把需要转发给用户空间的数据快速地写入当前 CPU 上的 channel 内核缓存, 用户空间应用通过标准的文件 I/ O函数在对应的 channel 文件中可以快速地取得这些被转发出的数据 mmap 来. 写入到 channel 中的数据的格式完全取决于内核中创建channel 的模块或子系统.

relayfs 的用户空间API :

relayfs 实现了四个标准的文件 I/O 函数, open、mmap、poll和close

函数 描述open 打开一个 channel 在某一个 CPU 上的缓存对应的文件.mmap 把打开的 channel 缓存映射到调用者进程的内存空间.read 读取 channel 缓存, 随后的读操作将看不到被该函数消耗的字节, 如果 channel 的操作模式为非覆盖写, 那么用户空间应用在有内核模块写时仍可以读取, 但是如 channel 的操作模式为覆盖式, 那么在读操作期间如果有内核模块进行写,结果将无法预知, 因此对于覆盖式写的 channel, 用户应当在确认在 channel 的写完全结束后再进行读.poll 用于通知用户空间应用转发数据跨越了子缓存的边界, 支持的轮询标志有 POLLIN、POLLRDNORM 和 POLLERRclose 关闭 open 函数返回的文件描述符, 如果没有进程或内核模块打开该 channel 缓存, close 函数将释放该channel 缓存注意 : 用户态应用在使用上述 API 时必须保证已经挂载了 relayfs 文件系统, 但内核在创建和使用 channel时不需要relayfs 已经挂载. 下面命令将把 relayfs 文件系统挂载到 /mnt/relay.

3 printk在内核调试技术之中, 最简单的就是 printk 的使用了, 它的用法和C语言应用程序中的 printf 使用类似, 在应用程序中依靠的是 stdio.h 中的库, 而在 linux 内核中没有这个库, 所以在 linux 内核中, 实现了自己的一套库函数, printk 就是标准的输出函数

4 ftrace && trace-cmd4.1 trace && ftraceLinux当前版本中, 功能最强大的调试、跟踪手段. 其最基本的功能是提供了动态和静态探测点, 用于探测内核中指定位置上的相关信息.

静态探测点, 是在内核代码中调用 ftrace 提供的相应接口实现, 称之为静态是因为, 是在内核代码中写死的, 静态编译到内核代码中的, 在内核编译后, 就不能再动态修改. 在开启 ftrace 相关的内核配置选项后, 内核中已经在一些关键的地方设置了静态探测点, 需要使用时, 即可查看到相应的信息.

动态探测点, 基本原理为 : 利用 mcount 机制, 在内核编译时, 在每个函数入口保留数个字节, 然后在使用 ftrace时, 将保留的字节替换为需要的指令, 比如跳转到需要的执行探测操作的代码。

ftrace 的作用是帮助开发人员了解 Linux 内核的运行时行为, 以便进行故障调试或性能分析.

最早 ftrace 是一个 function tracer, 仅能够记录内核的函数调用流程. 如今 ftrace 已经成为一个 framework, 采用 plugin 的方式支持开发人员添加更多种类的 trace 功能.

4.2 ftrace前端工具trace-cmdtrace-cmd 介绍trace-cmd 和 开源的 kernelshark 均是内核Ftrace 的前端工具, 用于分析内核性能.

他们相当于是一个 /sys/kernel/debug/tracing 中文件系统接口的封装, 为用户提供了更加直接和方便的操作.

# 收集信息sudo trace-cmd reord subsystem:tracing

# 解析结果#sudo trace-cmd report其本质就是对/sys/kernel/debug/tracing/events 下各个模块进行操作, 收集数据并解析。

5 Kprobe && systemtap5.1 内核kprobe机制kprobe 是 linux 内核的一个重要特性, 是一个轻量级的内核调试工具, 同时它又是其他一些更高级的内核调试工具(比如 perf 和 systemtap)的 “基础设施”, 4.0版本的内核中, 强大的 eBPF 特性也寄生于 kprobe 之上, 所以 kprobe 在内核中的地位就可见一斑了.

Kprobes 提供了一个强行进入任何内核例程并从中断处理器无干扰地收集信息的接口. 使用 Kprobes 可以收集处理器寄存器和全局数据结构等调试信息。开发者甚至可以使用 Kprobes 来修改寄存器值和全局数据结构的值.

如何高效地调试内核?

printk 是一种方法, 但是 printk 终归是毫无选择地全量输出, 某些场景下不实用, 于是你可以试一下tracepoint, 我使能 tracepoint 机制的时候才输出. 对于傻傻地放置 printk 来输出信息的方式, tracepoint 是个进步, 但是 tracepoint 只是内核在某些特定行为(比如进程切换)上部署的一些静态锚点, 这些锚点并不一定是你需要的, 所以你仍然需要自己部署tracepoint, 重新编译内核. 那么 kprobe 的出现就很有必要了, 它可以在运行的内核中动态插入探测点, 执行你预定义的操作.

它的基本工作机制是 : 用户指定一个探测点, 并把一个用户定义的处理函数关联到该探测点, 当内核执行到该探测点时, 相应的关联函数被执行,然后继续执行正常的代码路径.

kprobe 实现了三种类型的探测点 : kprobes, jprobes和 kretprobes(也叫返回探测点). kprobes 是可以被插入到内核的任何指令位置的探测点, jprobes 则只能被插入到一个内核函数的入口, 而 kretprobes 则是在指定的内核函数返回时才被执行.

农产品生鲜为什么烧钱?痛点在哪?2022通过这些方式破局!|大话西游手游怎么转生 1转流程攻略