使用 kprobe debug 内核

内核 DEBUG 有很多种方式。其中 kprobe 的好处是:

  • 可以在线开关

  • 不需要修改或者重新编译内核

  • 支持几乎所有的(除了少数之外)内核代码

  • 对性能影响较小

使用 kprobe 的两种方式

使用 kprobe 的最直接了当的方式,自然是根据它的 文档 ,编写一个内核模块,并且加载到内核。在内核源码的 samples/kprobes/ 下面有示例。

另外一种方法,是使用内核 trace 接口提供的 Kprobe-based Event Tracing 。使用这种方法,可以做到不用编写内核模块,在线增加 kprobe 跟踪,更方便一些,但是灵活性相对就差一些。

kprobe-based event tracing 的写法

官方文档挺好懂的,这里就不重复翻译了。唯一容易引发误解的是 FETCHARGS 的命名。文档是

 FETCHARGS	: Arguments. Each probe can have up to 128 args.
  %REG		: Fetch register REG
  \@ADDR		: Fetch memory at ADDR (ADDR should be in kernel)
  \@SYM[+|-offs]	: Fetch memory at SYM +|- offs (SYM should be a data symbol)
  $stackN	: Fetch Nth entry of stack (N >= 0)
  $stack	: Fetch stack address.
  $argN		: Fetch the Nth function argument. (N >= 1) (\*1)
  $retval	: Fetch return value.(\*2)
  $comm		: Fetch current task comm.
  +|-[u]OFFS(FETCHARG) : Fetch memory at FETCHARG +|- OFFS address.(\*3)(\*4)
  \IMM		: Store an immediate value to the argument.
  NAME=FETCHARG : Set NAME as the argument name of FETCHARG.
  FETCHARG:TYPE : Set TYPE as the type of FETCHARG. Currently, basic types
          (u8/u16/u32/u64/s8/s16/s32/s64), hexadecimal types
          (x8/x16/x32/x64), "string", "ustring" and bitfield
          are supported.

这块写得蛮不清楚的。这个语法有点像汇编,但是又不完全一样,在这里整理一下(以 64 位体系结构为例):

  1. %REG 就是寄存器地址,但是寄存器不使用名称来区分大小。例如栈指针寄存器,只能写成 %sp ,不能写成 %rsp ,通用正整数寄存器 r9,只能写成 %r9 ,不能写成 %r9d 。变量的实际大小,使用 TYPE 字段来标识

  2. 内存引用则是加上括号,形成行为 +|-[u]OFFS(FETCHARG) 的格式。其中最前面的符号和 OFFS 是不可省略的。例如当前栈的栈顶,需要写成 +0(%sp) ,如果栈顶是个指针,希望进一步引用指针地址,就继续嵌套,写成 +0(+0(%sp))

一个实际的例子

我们以内核回复 syn 包(也就是发送 synack 包时),计算滑动窗口的函数为例。假如我们需要 DEBUG 这个函数。

这里给一个简单的例子,仅仅打印这个函数传递的所有参数。实际上,通过计算内存偏移量,你可以打印出这个函数的所有局部变量,以及各种数据结构的值。这里仅仅以打印参数为例子。

这个函数的定义为:

1
2
3
4
void tcp_select_initial_window(const struct sock *sk, int __space, __u32 mss,
                               __u32 *rcv_wnd, __u32 *window_clamp,
                               int wscale_ok, __u8 *rcv_wscale,
                               __u32 init_rcv_wnd)

我们可以看到它有八个参数。我们知道 64 位体系架构下,会把前六个参数分别保存在 %di %si %dx %cx %r8 %r9 这六个整数寄存器中,剩余的参数则放在栈顶。

在 64 位体系结构下,一个 int 为 4 个字节,一个指针则为 8 个字节。这样我们可以算出所有八个变量的位置。其中所有指针需要加多一个内存引用。栈中的变量如果是指针,则需要两层内存应用,第一层是栈寄存器到内存的栈顶的指针,第二层是内存栈顶指针的解引用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 注意这里用追加,防止覆盖现有的规则
echo 'p:myprobe tcp_select_initial_window sk=+0(%di):x64 space=%si:s32 mss=%dx:u32 rcv_wnd=+0(%cx):u32 window_clamp=+0(%r8):u32 wscale_ok=%r9:s32 rsv_wscale=+0(+0(%sp)):u8 init_rcv_wnd=+4(%sp):u32' >> /sys/kernel/debug/tracing/kprobe_events
# 确认规则是否正确
cat /sys/kernel/debug/tracing/events/kprobes/myprobe/format
# 启用规则
echo 1 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
# 查看跟踪的结果
cat /sys/kernel/debug/tracing/trace
# 禁用规则
echo 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
# 删除规则
echo '-:myprobe tcp_select_initial_window' >> /sys/kernel/debug/tracing/kprobe_events
 KCupsConnection-1485  [005] ...1 56022.465539: myprobe: (tcp_select_initial_window+0x0/0xf0) sk=0x100007f0100007f space=43690 mss=65495 rcv_wnd=0 window_clamp=0 wscale_ok=1 rsv_wscale=15 init_rcv_wnd=4294967295