乐趣区

关于bpf:经典-libbpf-范例-uprobe-分析-eBPF基础知识-Part4

经典 libbpf 范例: uprobe 剖析 – eBPF 基础知识 Part4

《eBPF 基础知识》系列简介:

《eBPF 基础知识》系列指标是整顿一下 BPF 相干的基础知识。次要聚焦程序与内核互动接口局部。文章应用了 libbpf,但如果你不间接应用 libbpf,看本系列还是有肯定意义的,因为它聚焦于程序与内核互动接口局部,而非 libbpf 封装自身。而所有 bpf 开发框架,都要以类似的形式跟内核互动。甚至框架自身就是基于 libbpf。哪怕是 golang/rust/python/BCC/bpftrace。

  1. 《ELF 格局简述 – eBPF 基础知识 Part1》
  2. 《BPF 零碎接口 与 libbpf 示例剖析 – eBPF 基础知识 Part2》
  3. 《经典 libbpf 范例: bootstrap 剖析 – eBPF 基础知识 Part3》

国内习惯:尽量多图少文字。以下假如读者曾经对 BPF 有肯定的理解,或者浏览过之前的《eBPF 基础知识》系列文章。

libbpf 提供了一个应用 libbpf 的示例:https://github.com/libbpf/libbpf-bootstrap。其中的 uprobe 程序示范了一个最简略的 BPF uprobe 程序加载、绑定到 user space ELF 函数、与内核互动的过程。上面将图解剖析这个程序与内核的互动过程。

动机:为何我想学习 BPF uprobe

开始剖析前,我想说几句废话:为何我想学习 BPF uprobe?

  1. 大部分利用的行为,都以函数为单元来划分,跟踪利用的函数是跟踪利用很好的切入点

    例如我之前写的:

    • 逆向工程与云原生现场剖析 Part1 —— eBPF 跟踪 Istio/Envoy 之学步
    • 逆向工程与云原生现场剖析 Part2 —— eBPF 跟踪 Istio/Envoy 之启动、监听与线程负载平衡
    • 逆向工程与云原生现场剖析 Part3 —— eBPF 跟踪 Istio/Envoy 事件驱动模型、连贯建设、TLS 握手与 filter_chain 抉择
    • 逆向工程与云原生现场剖析 Part4 —— eBPF 跟踪 Istio/Envoy 之 upstream/downstream 事件驱动合作下的 HTTP 反向代理流程
  2. stack 性能剖析

    火焰图数据之源

  3. Troubleshooting

    生产上遇到问题,不太可能用 gdb 对剖析,但有的状况下能够用 BPF uprobe 去拦挡函数调用和获取入参出参。

uprobe 示例程序性能

uprobe 程序是一个用户空间(user-space)函数进入(entry)和退出(exit)探针示例,在 libbpf 术语中称为 uprobeuretprobe。它将 uprobeuretprobe BPF 程序绑定到它本人的函数(uprobed_add()uprobed_sub()),并应用 bpf_printk() 宏记录输出参数和返回值。用户空间函数每秒触发一次:

$ sudo ./uprobe
libbpf: loading object 'uprobe_bpf' from buffer
...
Successfully started!
...........

你能够这样监督程序的输入:

$ sudo cat /sys/kernel/debug/tracing/trace_pipe
          uprobe-1809291 [007] .... 4017233.106596: 0: uprobed_add ENTRY: a = 0, b = 1
          uprobe-1809291 [007] .... 4017233.106605: 0: uprobed_add EXIT: return = 1
          uprobe-1809291 [007] .... 4017233.106606: 0: uprobed_sub ENTRY: a = 0, b = 0
          uprobe-1809291 [007] .... 4017233.106607: 0: uprobed_sub EXIT: return = 0
          uprobe-1809291 [007] .... 4017234.106694: 0: uprobed_add ENTRY: a = 1, b = 2
          uprobe-1809291 [007] .... 4017234.106697: 0: uprobed_add EXIT: return = 3
          uprobe-1809291 [007] .... 4017234.106700: 0: uprobed_sub ENTRY: a = 1, b = 1
          uprobe-1809291 [007] .... 4017234.106701: 0: uprobed_sub EXIT: return = 0

程序主流程

<mark> 我始终致力防止在文章间接上代码。起因是,我本人的体验是,在文章中读代码太难了…… </mark> 不过有时还是要贴。指标不是让读者齐全一次看懂代码,而是对次要逻辑和命名符号有个理性的理解。我尽量精简一下吧。不要被这纸老虎吓跑。前面有图解的。

内核态 BPF 字节码程序

先看 BPF 内核字节码程序局部:

uprobe.bpf.c

SEC("uprobe")
int BPF_KPROBE(uprobe_add, int a, int b)
{bpf_printk("uprobed_add ENTRY: a = %d, b = %d", a, b);
    return 0;
}

SEC("uretprobe")
int BPF_KRETPROBE(uretprobe_add, int ret)
{bpf_printk("uprobed_add EXIT: return = %d", ret);
    return 0;
}

SEC("uprobe//proc/self/exe:uprobed_sub")
int BPF_KPROBE(uprobe_sub, int a, int b)
{bpf_printk("uprobed_sub ENTRY: a = %d, b = %d", a, b);
    return 0;
}

SEC("uretprobe//proc/self/exe:uprobed_sub")
int BPF_KRETPROBE(uretprobe_sub, int ret)
{bpf_printk("uprobed_sub EXIT: return = %d", ret);
    return 0;
}

可见,这里包含 uprobe_adduprobe_sub 两个 user space 函数的入口与进口探针。这个示例是用户态过程本人探测本人(这个样的示例其实不太好,不事实)。过程本人探测本人,所以能够用 /proc/self/exe。熟识 Linux proc 目录的同学都晓得,这个文件是指向拜访这个文件的过程自身的 symbol link:

$ ls -l /proc/self/exe 
lrwxrwxrwx 1 labile labile 0 Apr  2 22:40 /proc/self/exe -> /usr/bin/ls

同是 uprobe 实现函数 的 section 定义,下面代码有两种表达方法:

  • SEC(“uprobe”)

    这种没指定指标函数。由用户态 bpf 程序加载和动静绑定到探测指标函数。下层用户态 bpf 程序须要自行计算函数在 ELF 文件中的 offset。

  • SEC(“uprobe//proc/self/exe:uprobed_sub”)

    这种指定了指标 elf 门路和函数。可由用户态 libbpf 主动加载和绑定到探测指标函数。下层用户态 bpf 程序不须要计算函数在 ELF 文件中的 offset。由 libbpf 主动计算。

在 make 的过程中,实际上是执行了:

clang -g -O2 -target bpf -D__TARGET_ARCH_x86 -I.output -I../../libbpf/include/uapi -I../../vmlinux/x86/ 
-idirafter /usr/lib/llvm-14/lib/clang/14.0.0/include -idirafter /usr/local/include -idirafter 
/usr/include/x86_64-linux-gnu -idirafter /usr/include -c uprobe.bpf.c -o .output/uprobe.bpf.o

llvm-strip -g .output/uprobe.bpf.o

最初一行就是重点。输出是 uprobe.bpf.c。输入是 uprobe.bpf.o。这是一个 ELF 格局的文件。这个文件将会嵌入到利用中。uprobe.bpf.o section 如下:

$ readelf -aW examples/c/.output/uprobe.bpf.o

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [1] .strtab           STRTAB          0000000000000000 000c41 00014f 00      0   0  1
  [2] .text             PROGBITS        0000000000000000 000040 000000 00  AX  0   0  4
  [3] uprobe            PROGBITS        0000000000000000 000040 000040 00  AX  0   0  8
  [4] .reluprobe        REL             0000000000000000 000aa8 000010 10   I 18   3  8
  [5] uretprobe         PROGBITS        0000000000000000 000080 000038 00  AX  0   0  8
  [6] .reluretprobe     REL             0000000000000000 000ab8 000010 10   I 18   5  8
  [7]  PROGBITS        0000000000000000 0000b8 000040 00  AX  0   0  8
  [8] .reluprobe//proc/self/exe:uprobed_sub REL             0000000000000000 000ac8 000010 10   I 18   7  8
  [9] uretprobe//proc/self/exe:uprobed_sub PROGBITS        0000000000000000 0000f8 000038 00  AX  0   0  8
  [10] .reluretprobe//proc/self/exe:uprobed_sub REL             0000000000000000 000ad8 000010 10   I 18   9  8
  [11] license           PROGBITS        0000000000000000 000130 00000d 00  WA  0   0  1
  [12] .rodata           PROGBITS        0000000000000000 00013d 000080 00   A  0   0  1
  [13] .BTF              PROGBITS        0000000000000000 0001c0 000635 00      0   0  4
  [14] .rel.BTF          REL             0000000000000000 000ae8 000050 10   I 18  13  8
  [15] .BTF.ext          PROGBITS        0000000000000000 0007f8 000148 00      0   0  4
  [16] .rel.BTF.ext      REL             0000000000000000 000b38 000100 10   I 18  15  8
  [17] .llvm_addrsig     LOOS+0xfff4c03  0000000000000000 000c38 000009 00   E  0   0  1
  [18] .symtab           SYMTAB          0000000000000000 000940 000168 18      1  10  8

如果你不太理解 ELF 格局,倡议先看看,因为了解这个格局很重要。能够参考我的《ELF 格局简述 – eBPF 基础知识》

下面可见,uprobe//proc/self/exe:uprobed_suburetprobe//proc/self/exe:uprobed_sub section 的名字指明了相应 BPF program 要绑定的指标用户态 ELF 与函数名。

用户态 bpf 程序

uprobe.c

// 计算本过程的一个函数在 ELF 文件的 offset
ssize_t get_uprobe_offset(const void *addr)
{
    size_t start, end, base;
    char buf[256];
    bool found = false;
    FILE *f;

    f = fopen("/proc/self/maps", "r");
...
    while (fscanf(f, "%zx-%zx %s %zx %*[^\n]\n", &start, &end, buf, &base) == 4) {if (buf[2] == 'x' && (uintptr_t)addr >= start && (uintptr_t)addr < end) {
            found = true;
            break;
        }
    }

    fclose(f);
...
    return (uintptr_t)addr - start + base;
}

// 探测指标函数
int uprobed_add(int a, int b)
{return a + b;}

// 探测指标函数
int uprobed_sub(int a, int b)
{return a - b;}

int main(int argc, char **argv)
{
    struct uprobe_bpf *skel;
    long uprobe_offset;
    int err, i;
...

    /* 1. 加载内核态 BPF 程序 */
    skel = uprobe_bpf__open_and_load();
    if (!skel) {fprintf(stderr, "Failed to open and load BPF skeleton\n");
        return 1;
    }

    /* uprobe/uretprobe expects relative offset of the function to attach
     * to. This offset is relateve to the process's base load address. So
     * easy way to do this is to take an absolute address of the desired
     * function and substract base load address from it.  If we were to
     * parse ELF to calculate this function, we'd need to add .text
     * section offset and function's offset within .text ELF section.
     * 计算 uprobed_add 函数在本过程的 ELF 中的 offset
     */
    uprobe_offset = get_uprobe_offset(&uprobed_add);

    /* 2. 绑定 BPF 程序 BPF_KPROBE(uprobe_add, int a, int b) 到函数 uprobed_add  */
    skel->links.uprobe_add = bpf_program__attach_uprobe(skel->progs.uprobe_add,
                                false /* not uretprobe */,
                                0 /* self pid */,
                                "/proc/self/exe",
                                uprobe_offset);
...
    
    /* Let libbpf perform auto-attach for uprobe_sub/uretprobe_sub
     * NOTICE: we provide path and symbol info in SEC for BPF programs
     * 3. 让 libbpf 主动依据 uprobe.bpf.c 的 section 定义(如:SEC("uprobe//proc/self/exe:uprobed_sub")),去加载和绑定 bpf 程序到过程用户态函数
     */
    err = uprobe_bpf__attach(skel);
...

    printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe`"
           "to see output of the BPF programs.\n");

    for (i = 0; ; i++) {
        /* trigger our BPF programs */
        fprintf(stderr, ".");
        uprobed_add(i, i + 1);
        uprobed_sub(i * i, i);
        sleep(1);
    }
...
}

看看输入的 ELF 内容:

$ readelf -aW uprobe

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  PHDR           0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R   0x8
  INTERP         0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R   0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x000000 0x0000000000000000 0x0000000000000000 0x004828 0x004828 R   0x1000
  LOAD           0x005000 0x0000000000005000 0x0000000000005000 0x02ba29 0x02ba29 R E 0x1000
# 关注下面的 Offset: 0x005000

# 只关注 .symtab 局部 的 uprobed_sub
Symbol table '.symtab' contains 720 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
   470: 000000000000646b    24 FUNC    GLOBAL DEFAULT   16 uprobed_add
   523: 0000000000006483    22 FUNC    GLOBAL DEFAULT   16 uprobed_sub   

程序架构

uprobe 与内核互动概述

如上图排版有问题,请点这里用 Draw.io 关上。局部带互动链接和 hover tips

图中是我跟踪的后果。用 Draw.io 关上后,每一步均有 link,点击可看到代码。鼠标放到连接线上,会 hover 出 stack(调用堆栈)。

图中的阐明曾经比拟具体。其中包含重要的数据结构和步骤。

上图 file descriptor 之间的连线,反映了它们之间的关联。这里简略列一下上图的流程:

  • 1 ~ 5 建设 libbpf 用到的数据结构。
  • 6 ~ 12 加载 BPF program 与 BPF Map
  • 13 ~ 16 开启动静 uprobe

      1. 计算动静 int uprobed_add(int a, int b) 的 offset
      2. 创立 函数 perf event probe
      3. 函数 perf event probe 绑定到 BPF program
      4. 启动 函数 perf event probe
    1. 开启动态 uprobe
    • 利用 libbpf 依据 uprobe.bpf.o ELF 文件的 section 信息,主动计算 int uprobed_sub(int a, int b) offset。之后实现相似下面 开启动静 uprobe 的过程。最终 函数 perf event probe 绑定到 BPF program BPF_KPROBE(uprobe_sub, int a, int b)
    1. 用户态利用定时调用函数 uprobed_add(int a, int b)uprobed_sub(int a, int b) 触发了 BPF progam

我 fork 了我的项目到这里:

https://github.com/labilezhu/libbpf-bootstrap/tree/20230226

后记

这个后记和本文没什么相干了,不喜可跳过。最近看了一部 1994 年的电影。

退出移动版