Linux内核调试工具之Kprobes相关概念

背景及概念

Kprobes: Kernel Probes

背景

开发人员在内核或者模块的调试过程中,往往会需要要知道其中的一些函数有无被调用、何时被调用、执行是否正确以及函数的入参和返回值是什么等等。

比较简单的做法是在内核代码对应的函数中添加日志打印信息,但这种方式往往需要重新编译内核或模块,重新启动设备之类的,操作较为复杂甚至可能会破坏原有的代码执行过程。

而利用kprobes技术,用户可以定义自己的回调函数,然后在内核或者模块中几乎所有的函数中动态的插入探测点,当内核执行流程执行到指定的探测函数时,会调用该回调函数,用户即可收集所需的信息了,同时内核最后还会回到原本的正常执行流程。

如果用户已经收集足够的信息,不再需要继续探测,则同样可以动态的移除探测点。因此kprobes技术具有对内核执行流程影响小和操作方便的优点。

概念

目前有两种类型的探针:kprobes和 kretprobes(也称为return probes)。

kprobe是最基本的探测方式,几乎可以插入到内核中的任何指令上。它提供了探测点的调用前、调用后和内存访问出错3种回调:

  • pre_handler: 在被探测指令被执行前回调
  • post_handler: 在被探测指令执行完毕后回调
  • fault_handler: 在内存访问出错时被调用

kretprobe基于kprobe实现,用于获取被探测函数的返回值。

学习最好且最权威的资料还是官方文档!

工作原理

kprobe 工作原理

当一个kprobe被注册时,Kprobes会复制一个被探测的指令的副本,并将被探测指令的第一个字节替换为替换为断点指令(例如,i386和x86_64上的int3,ARM64的BRK指令)。

当CPU碰到断点指令时,就会发生一个trap,CPU的寄存器被保存起来,控制权通过 “通知器调用链 “(notifier_call_chain )传递给Kprobes。Kprobes会执行 “pre_handler”,将处理程序的地址传递给 kprobe结构和保存的寄存器的地址。

接下来,kprobes会对刚刚复制的指令进行单步操作。在执行单步指令后,会调用post_handler,然后继续执行探测点之后的指令。

kretprobes 工作原理

kretprobes的工作原理类似,这里就略过了,详情可见参考中的文档

kprobes跳转优化

如果编译选项CONFIG_OPTPROBES=ydebug.kprobes_optimization 内核参数设置为1,Kprobes会试图通过在每个探测点使用跳转指令而不是断点指令来减少探测命中的开销。

具体的原理及优化过程可查看参考文档。

kprobes的Blacklist

Kprobes可以探测除自己以外的大部分内核,但有一些函数是Kprobes不能探测的。探测这些函数可能会导致递归陷阱或嵌套的探测处理函数可能永远不会被调用。Kprobes将这类函数作为黑名单Blacklist来管理。
如果你想把一个函数加入黑名单,只需要包含linux/kprobes.h,然后使用NOKPROBE_SYMBOL()宏来指定该函数。例如:

#include <linux/kprobes.h>
...
bool nmi_cpu_backtrace(struct pt_regs *regs)
{
    ...
}

NOKPROBE_SYMBOL(nmi_cpu_backtrace);

Kprobes会根据黑名单来检查给定的探测地址,如果给定的地址在黑名单中,则拒绝注册。

支持架构

kprobes和kretprobes支持以下架构:

  • i386 (Supports jump optimization)
  • x86_64 (AMD-64, EM64T) (Supports jump optimization)
  • ppc64
  • sparc64 (Return probes not yet implemented.)
  • arm
  • ppc
  • mips
  • s390
  • parisc
  • loongarch

参考