基于 Kprobe 的事件追踪

作者:

Masami Hiramatsu

概述

这些事件与基于 tracepoint 的事件类似。不同之处在于,它基于 kprobes (kprobe 和 kretprobe)。因此,它可以在 kprobes 可以探测的任何地方进行探测(这意味着,除了带有 __kprobes/nokprobe_inline 注释和标记为 NOKPROBE_SYMBOL 的函数之外的所有函数)。与基于 tracepoint 的事件不同,它可以动态地、即时地添加和删除。

要启用此功能,请使用 CONFIG_KPROBE_EVENTS=y 构建您的内核。

与事件追踪器类似,此功能无需通过 current_tracer 激活。相反,通过 /sys/kernel/tracing/kprobe_events 添加探针点,并通过 /sys/kernel/tracing/events/kprobes/<EVENT>/enable 启用它。

您也可以使用 /sys/kernel/tracing/dynamic_events 代替 kprobe_events。该接口还将提供对其他动态事件的统一访问。

kprobe_events 概要

 p[:[GRP/][EVENT]] [MOD:]SYM[+offs]|MEMADDR [FETCHARGS]        : Set a probe
 r[MAXACTIVE][:[GRP/][EVENT]] [MOD:]SYM[+0] [FETCHARGS]        : Set a return probe
 p[:[GRP/][EVENT]] [MOD:]SYM[+0]%return [FETCHARGS]    : Set a return probe
 -:[GRP/][EVENT]                                               : Clear a probe

GRP            : Group name. If omitted, use "kprobes" for it.
EVENT          : Event name. If omitted, the event name is generated
                 based on SYM+offs or MEMADDR.
MOD            : Module name which has given SYM.
SYM[+offs]     : Symbol+offset where the probe is inserted.
SYM%return     : Return address of the symbol
MEMADDR        : Address where the probe is inserted.
MAXACTIVE      : Maximum number of instances of the specified function that
                 can be probed simultaneously, or 0 for the default value
                 as defined in Documentation/trace/kprobes.rst section 1.3.1.

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), VFS layer common type(%pd/%pD), "char",
                 "string", "ustring", "symbol", "symstr" and bitfield are
                 supported.

 (\*1) only for the probe on function entry (offs == 0). Note, this argument access
       is best effort, because depending on the argument type, it may be passed on
       the stack. But this only support the arguments via registers.
 (\*2) only for return probe. Note that this is also best effort. Depending on the
       return value type, it might be passed via a pair of registers. But this only
       accesses one register.
 (\*3) this is useful for fetching a field of data structures.
 (\*4) "u" means user-space dereference. See :ref:`user_mem_access`.

kretprobe 处的函数参数

可以使用 $arg<N> fetcharg 在 kretprobe 处访问函数参数。这对于一次记录函数参数和返回值,以及跟踪结构字段的差异(用于调试函数是否正确更新给定的数据结构)很有用。请参阅 fprobe 事件中的 示例,了解其工作原理。

类型

fetchargs 支持多种类型。 Kprobe 追踪器将按给定的类型访问内存。前缀 ‘s’ 和 ‘u’ 分别表示这些类型是有符号和无符号的。 ‘x’ 前缀表示它是无符号的。追踪的参数以十进制(‘s’ 和 ‘u’)或十六进制(‘x’)显示。如果没有类型转换,则根据架构使用 ‘x32’ 或 ‘x64’(例如,x86-32 使用 x32,而 x86-64 使用 x64)。

这些值类型可以是数组。要记录数组数据,您可以将 ‘[N]’(其中 N 是一个固定数字,小于 64)添加到基本类型。例如,‘x16[4]’ 表示一个包含 4 个元素的 x16(2 字节十六进制)数组。请注意,该数组可以应用于内存类型 fetchargs,您不能将其应用于寄存器/堆栈条目等。(例如,‘$stack1:x8[8]’ 是错误的,但 ‘+8($stack):x8[8]’ 是可以的。)

Char 类型可用于显示追踪参数的字符值。

String 类型是一种特殊类型,它从内核空间获取一个“以 null 结尾”的字符串。这意味着如果字符串容器已分页出去,它将失败并存储 NULL。 “ustring” 类型是用户空间的字符串替代方案。有关更多信息,请参见 用户内存访问

字符串数组类型与其他类型略有不同。对于其他基本类型,<base-type>[1] 等于 <base-type>(例如,+0(%di):x32[1] 与 +0(%di):x32 相同。)但 string[1] 不等于 string。 string 类型本身表示“char 数组”,但字符串数组类型表示“char * 数组”。因此,例如,+0(%di):string[1] 等于 +0(+0(%di)):string。位域是另一种特殊类型,它接受 3 个参数,位宽、位偏移量和容器大小(通常为 32)。语法是

b<bit-width>@<bit-offset>/<container-size>

Symbol 类型(‘symbol’)是 u32 或 u64 类型的别名(取决于 BITS_PER_LONG),它以“symbol+offset”样式显示给定的指针。另一方面,symbol-string 类型 (‘symstr’) 将给定的地址转换为“symbol+offset/symbolsize”样式,并将其存储为以 null 结尾的字符串。使用 ‘symstr’ 类型,您可以使用符号的通配符模式来过滤事件,并且您无需自己解析符号名称。对于 $comm,默认类型为“string”;任何其他类型均无效。

VFS 层通用类型 (%pd/%pD) 是一种特殊类型,它从 struct dentry 的地址或 struct file 的地址获取 dentry 或文件的名称。

用户内存访问

Kprobe 事件支持用户空间内存访问。为此,您可以使用用户空间解引用语法或 ‘ustring’ 类型。

用户空间解引用语法允许您访问用户空间中数据结构的字段。这是通过将“u”前缀添加到解引用语法来完成的。例如,+u4(%si) 表示它将从寄存器 %si 中偏移 4 的地址读取内存,并且该内存应位于用户空间中。您也可以将其用于字符串,例如 +u0(%si):string 将从寄存器 %si 中的地址读取一个字符串,该地址应位于用户空间中。 ‘ustring’ 是执行相同任务的快捷方式。也就是说,+0(%si):ustring 等同于 +u0(%si):string。

请注意,kprobe-event 提供了用户内存访问语法,但它不会透明地使用它。这意味着如果您对用户内存使用正常的解引用或字符串类型,它可能会失败,并且可能在某些架构上总是失败。用户必须仔细检查目标数据是在内核空间还是用户空间中。

按探针事件过滤

按探针事件过滤功能允许您在每个探针上设置不同的过滤器,并为您提供将在追踪缓冲区中显示哪些参数。如果在 kprobe_events 中 ‘p:’ 或 ‘r:’ 之后立即指定事件名称,它会在 tracing/events/kprobes/<EVENT> 下添加一个事件,您可以在该目录中看到 ‘id’、‘enable’、‘format’、‘filter’ 和 ‘trigger’。

enable

您可以通过在其上写入 1 或 0 来启用/禁用探针。

format

这显示了此探针事件的格式。

filter

您可以写入此事件的过滤规则。

id

这显示了此探针事件的 id。

trigger

这允许安装在命中事件时执行的触发命令(有关详细信息,请参见 事件追踪,第 6 节)。

事件分析

您可以通过 /sys/kernel/tracing/kprobe_profile 检查探针命中总数和探针未命中数。第一列是事件名称,第二列是探针命中数,第三列是探针未命中数。

内核启动参数

您可以通过 “kprobe_event=” 参数在启动内核时添加和启用新的 kprobe 事件。该参数接受以分号分隔的 kprobe 事件,其格式类似于 kprobe_events。不同之处在于,探针定义参数是以逗号分隔,而不是空格。例如,在 do_sys_open 上添加 myprobe 事件,如下所示

p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack)

内核启动参数应如下所示(只需将空格替换为逗号)

p:myprobe,do_sys_open,dfd=%ax,filename=%dx,flags=%cx,mode=+4($stack)

使用示例

要将探针添加为新事件,请将新定义写入 kprobe_events,如下所示

echo 'p:myprobe do_sys_open dfd=%ax filename=%dx flags=%cx mode=+4($stack)' > /sys/kernel/tracing/kprobe_events

这会在 do_sys_open() 函数的顶部设置一个 kprobe,并将第 1 到第 4 个参数记录为 “myprobe” 事件。请注意,哪个寄存器/堆栈条目分配给每个函数参数取决于特定于架构的 ABI。如果您不确定 ABI,请尝试使用 perf-tools 的 probe 子命令(您可以在 tools/perf/ 下找到它)。如此示例所示,用户可以选择更熟悉的名称用于每个参数。

echo 'r:myretprobe do_sys_open $retval' >> /sys/kernel/tracing/kprobe_events

这会在 do_sys_open() 函数的返回点上设置一个 kretprobe,并将返回值记录为 “myretprobe” 事件。您可以通过 /sys/kernel/tracing/events/kprobes/<EVENT>/format 查看这些事件的格式。

cat /sys/kernel/tracing/events/kprobes/myprobe/format
name: myprobe
ID: 780
format:
        field:unsigned short common_type;       offset:0;       size:2; signed:0;
        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
        field:unsigned char common_preempt_count;       offset:3; size:1;signed:0;
        field:int common_pid;   offset:4;       size:4; signed:1;

        field:unsigned long __probe_ip; offset:12;      size:4; signed:0;
        field:int __probe_nargs;        offset:16;      size:4; signed:1;
        field:unsigned long dfd;        offset:20;      size:4; signed:0;
        field:unsigned long filename;   offset:24;      size:4; signed:0;
        field:unsigned long flags;      offset:28;      size:4; signed:0;
        field:unsigned long mode;       offset:32;      size:4; signed:0;


print fmt: "(%lx) dfd=%lx filename=%lx flags=%lx mode=%lx", REC->__probe_ip,
REC->dfd, REC->filename, REC->flags, REC->mode

您可以看到该事件具有 4 个参数,如您指定的表达式中所示。

echo > /sys/kernel/tracing/kprobe_events

这会清除所有探针点。

或者,

echo -:myprobe >> kprobe_events

这会选择性地清除探针点。

在定义之后,默认情况下每个事件都是禁用的。对于追踪这些事件,您需要启用它。

echo 1 > /sys/kernel/tracing/events/kprobes/myprobe/enable
echo 1 > /sys/kernel/tracing/events/kprobes/myretprobe/enable

使用以下命令在间隔内开始追踪。

# echo 1 > tracing_on
Open something...
# echo 0 > tracing_on

您可以通过 /sys/kernel/tracing/trace 查看追踪的信息。

cat /sys/kernel/tracing/trace
# tracer: nop
#
#           TASK-PID    CPU#    TIMESTAMP  FUNCTION
#              | |       |          |         |
           <...>-1447  [001] 1038282.286875: myprobe: (do_sys_open+0x0/0xd6) dfd=3 filename=7fffd1ec4440 flags=8000 mode=0
           <...>-1447  [001] 1038282.286878: myretprobe: (sys_openat+0xc/0xe <- do_sys_open) $retval=fffffffffffffffe
           <...>-1447  [001] 1038282.286885: myprobe: (do_sys_open+0x0/0xd6) dfd=ffffff9c filename=40413c flags=8000 mode=1b6
           <...>-1447  [001] 1038282.286915: myretprobe: (sys_open+0x1b/0x1d <- do_sys_open) $retval=3
           <...>-1447  [001] 1038282.286969: myprobe: (do_sys_open+0x0/0xd6) dfd=ffffff9c filename=4041c6 flags=98800 mode=10
           <...>-1447  [001] 1038282.286976: myretprobe: (sys_open+0x1b/0x1d <- do_sys_open) $retval=3

每行显示内核何时命中事件,并且 <- SYMBOL 表示内核从 SYMBOL 返回(例如,“sys_open+0x1b/0x1d <- do_sys_open” 表示内核从 do_sys_open 返回到 sys_open+0x1b)。