09月02, 2022

bpftrace 入门(3)probe 的参数

ebpf 支持

  • hardware
  • iter
  • kfunc
  • kprobe
  • software
  • tracepoint

以及 uprobe 等几大类探针,我们从最常用的应用场景:探测内核和应用函数调用入手。 image.png

dynamic tracing

首先是动态 trace,ebpf 提供了

  • 面向内核的 kprobe/kretprobe,k = kernel
  • 面向应用的 uprobe/uretprobe,u = user land

分别用于探测函数入口处和函数返回 (ret) 处的信息。

kprobe/kretprobe 可以探测内核大部分函数,出于安全考虑,有部分内核函数不允许安装探针,另外也可以配合 offset 探测函数中任意位置的信息。

你可以这样查看 kprobe 的黑名单:

 cat /sys/kernel/debug/kprobes/blacklist

uprobe/uretprobe 则可以为应用的任意函数安装探针。

动态 trace 技术依赖内核和应用的符号表,对于那些 inline 或者 static 函数则无法直接安装探针,需要自行通过 offset 实现。可以借助 nm 或者 strings 指令查看应用的符号表。

这两种动态 trace 技术的原理与 GDB 类似,当对某段代码安装探针,内核会将目标位置指令复制一份,并替换为 int3 中断, 执行流跳转到用户指定的探针 handler,再执行备份的指令,如果此时也指定了 ret 探针,也会被执行,最后再跳转回原来的指令序列。

kprobe 和 uprobe 可以通过 arg0arg1... ... 访问所有参数;kretprobe 和 uretprobe 通过 retval 访问函数的返回值。除了基本类型:char、int 等,字符串需通过 str() 函数才能访问。

# bpftrace -e 'uprobe:/lib/x86_64-linux-gnu/libc-2.23.so:fopen { printf("fopen: %s\n", str(arg0)); }'
Attaching 1 probe...
fopen: /proc/filesystems
fopen: /usr/share/locale/locale.alias
fopen: /proc/self/mountinfo

复合参数

动态 trace 除了可以访问基本类型参数,当然也可以访问有高级结构的参数,前提是 bpftrace 代码中可以获得该结构的定义。

有 2 种方式描述这些参数的结构:

  1. 对于结构简单的参数,可以直接在 bpftrace 代码中按 C 语言风格定义复杂参数的结构。必须确保 bpftrace 可以获取需要访问成员的准确 offset 和实际大小,结构体前半部分其他成员可以准确定义,也定义可以替换为更简单的 char[N],只要 offset 与实际一致即可,关心的成员之后其他部分,可以不定义:

    /* 比如实际参数定义如下 */
    struct arg_s{
      struct arg_s1 mem1;
      struct arg_s1 mem2;
      int32_t  x;
      int32_t  y;
      char    *name;
      ....  // other members
    };
    
    /* 当我们只关心 name,且知道结构体前 4 个成员总大小为 64,那么在 bpftrace 中可以这样写*/
    # someprobe.bt
    struct arg_s{
      /* name 前其他成员替换为 padding,只要 offset 一致*/
      char  padding[64];
      char    *name;
      /* name 之后成员无需定义 */
    };
    
    someprobe:funcx {
      printf("%s\n", str(((struct arg_s *)arg0)->name); 
    }
    
  2. 某些情况下参数结构复杂,也可按照 C 风格 #include 头文件,让 bpftrace 知道参数的定义。

    # cat path.bt
    #include <linux/path.h>
    #include <linux/dcache.h>
    
    kprobe:vfs_open
    {
        printf("open path: %s\n", str(((struct path *)arg0)->dentry->d_name.name));
    }
    

需要特别注意结构体可能发生的内存对齐,否则可能获得错误的成员 offset。

static tracing

另一种常用的探针是静态 trace,所谓 “静态” 是指探针的位置、名称都是在代码中硬编码的,编译时就确定了。静态 trace 的实现原理类似 callback,当被激活时执行,关闭时不执行,性能比动态 trace 高一些。

  • 其中 tracepoint 是内核中的,
  • usdt = Userland Statically Defined Tracing,是应用中的。

静态 trace 已经在内核和应用中饱含了探针参数信息,可以直接通过 args->参数名 访问函数参数。tracepoint 的 参数 format 信息可以通过 bpftrace -v probe 查看:

# bpftrace -lv tracepoint:raw_syscalls:sys_exit
tracepoint:raw_syscalls:sys_exit
    long id;
    long ret;

或者访问 debugfs:

# cat /sys/kernel/debug/tracing/events/raw_syscalls/sys_exit/format 
name: sys_exit
ID: 20
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:long id;  offset:8;       size:8; signed:1;
        field:long ret; offset:16;      size:8; signed:1;

print fmt: "NR %ld = %ld", REC->id, REC->ret

下面是一个官方示例,通过 args->filename 访问 sys_enter_openatfilename 参数:

# bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'
Attaching 1 probe...
irqbalance /proc/interrupts
irqbalance /proc/stat
snmpd /proc/diskstats
snmpd /proc/stat
snmpd /proc/vmstat
snmpd /proc/net/dev
[...]

本文链接:http://www.thinkinpython.com/post/bpftrace_tutorial_3.html

-- EOF --