Linux之ebpf(3)uprobe与ebpf

打印 上一主题 下一主题

主题 901|帖子 901|积分 2703

Linux之ebpf(3)uprobe扼要利用

  
   Author: Once Day Date: 2024年9月5日


  一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许尽头只是一场白日梦…


   漫漫长路,有人对你微笑过嘛…


  全系列文章可参考专栏: Linux底子知识_Once-Day的博客-CSDN博客。


  参考文章:
  

  • 击败eBPF Uprobe监控 (qq.com)
  • kernel.org/doc/Documentation/trace/uprobetracer.txt
  • kernel.org/doc/html/latest/trace/uprobetracer
  • uprobe的利用浅析 - yooooooo - 博客园 (cnblogs.com)
  • 深入ftrace uprobe原理和功能先容-CSDN博客
  

  
1. 概述

1.1 先容

Linux 内核从 3.5 版本开始引入了 uprobe 功能,它是一种用户态的动态追踪技术。Uprobe 允许在用户空间的应用步调中插入探测点,以便实时监控和跟踪步调的运行状态和行为,而无需修改或重新编译应用步调的源代码。
Uprobe 的工作原理如下:

  • 在目的应用步调的特定指令位置设置探测点。当步调实行到该指令时,会触发探测点。
  • 探测点被触发后,步调实行流程会被中断,并将控制权转移给预先注册的探测处理步调。
  • 探测处理步调可以访问寄存器、内存等步调运行时的上下文信息,以此来分析和记录步调的状态。
  • 处理完成后,控制权会返回给原始步调,步调继续实行。
Uprobe 的优势在于:


  • 动态性:可以在运行时动态地插入和删除探测点,无需重启应用步调。
  • 低开销:探测点的插入和删除对应用步调性能影响很小。
  • 灵活性:可以在应用步调的任意指令位置设置探测点,获取丰富的运行时信息。
  • 与其他工具的集成:可以与其他追踪和性能分析工具(如 ftrace、perf 等)结合利用。
Uprobe 在现实应用中有广泛的用途,例如:

  • 性能剖析和优化:通过收集关键函数的调用次数、实行时间等指标,发现性能瓶颈。
  • 故障诊断和调试:通过记录异常发生时的步调状态, 快速定位息争决 bug。
  • 安全监控和审计:通过监督特定函数的实行,发现可疑行为和潜在的安全威胁。
  • 业务逻辑分析:通过跟踪特定函数参数和返回值,洞察应用步调的业务流程和逻辑。
要利用uprobe功能,编译内核需要开启CONFIG_UPROBE_EVENTS=y宏。
1.2 kprobe和uprobe联系和区别

Kprobe和Uprobe都是Linux内核提供的动态追踪技术,它们允许在内核或用户空间的指定位置插入探测点,以便实时监控和跟踪步调的运行状态和行为。


  • 动态插装:Kprobe和Uprobe都支持在运行时动态地插入和删除探测点,无需修改或重新编译目的步调的源代码。
  • 探测机制:两者的工作原理类似,当步调实行到探测点时,会触发探测处理步调,处理步调可以访问寄存器、内存等步调运行时的上下文信息。
  • 与其他工具的集成:Kprobe和Uprobe都可以与其他追踪和性能分析工具(如ftrace、perf等)结合利用,以实现更强大的分析功能。
KprobeUprobe应用对象Kprobe专门用于内核空间的追踪,它的探测点设置在内核函数或指令上。Uprobe则针对用户空间的应用步调,探测点设置在用户步调的函数或指令上。可访问的数据Kprobe可以访问内核空间的所有数据,包括内核变量、数据结构等。Uprobe只能访问用户空间的数据,对内核空间的数据没有直接访问权限。利用复杂度Kprobe的利用相对复杂,需要对内核源代码有深入的理解,并警惕处理探测点对内核的影响。Uprobe的利用相对简朴,仅需了解目的应用步调的函数和指令即可,对内核知识的要求较低。安全风险由于Kprobe运行在内核空间,如果探测处理步调编写不当,可能会导致内核瓦解或安全弊端。Uprobe运行在用户空间,纵然探测处理步调有错误,也只会影响目的应用步调,对体系的稳定性影响较小。适用场景Kprobe适用于内核级别的性能分析、调试、安全监控等任务。Uprobe适用于应用步调级别的性能优化、故障诊断、业务逻辑分析等任务。 1.3 uprobe原理扼要

Uprobe 的原理可以概括为以下几个步骤:
(1) 注册探测点:


  • 通过 uprobe_register() 函数注册一个探测点,指定目的应用步调的二进制文件路径和偏移量(或符号名)。
  • 内核会在指定位置插入一个断点指令(通常是 int3)。
(2) 实行探测点:


  • 当应用步调实行到探测点位置时,会触发断点,产生一个异常。
  • 内核捕捉这个异常,并将控制权转交给 Uprobe 的异常处理步调。
(3) 保存上下文:


  • Uprobe 的异常处理步调会保存当前的寄存器状态和一些其他上下文信息。
  • 处理步调会将原始指令(被 int3 替换的指令)复制到一个安全的位置。
(4) 实行处理步调:


  • Uprobe 会调用用户预先注册的处理步调函数。
  • 处理步调可以访问寄存器、内存等步调运行时的上下文信息,实行所需的分析和跟踪操作。
(5) 恢复实行:


  • 处理步调实行完毕后,Uprobe 会恢复之前保存的寄存器状态。
  • Uprobe 将控制权交还给原始的应用步调,并从复制的原始指令处继续实行。
(6) 单步实行并恢复探测点:


  • 应用步调会实行复制的原始指令,然后再次触发断点。
  • Uprobe 的异常处理步调再次捕捉异常,将 int3 指令重新写回探测点位置,然后恢复步调实行。
(7) 卸载探测点:


  • 当不再需要跟踪时,可以通过 uprobe_unregister() 函数卸载探测点。
  • 内核会将探测点位置的指令恢复为原始指令。
下面是触发断点时的实行流表现图(下图源自:深入ftrace uprobe原理和功能先容-齐小葩-CSDN博客):

1.4 uprobe输出信息

Uprobe 的输出信息通常通过 tracefs 文件体系举行查看。tracefs 是一个用于跟踪和调试的虚拟文件体系,它提供了一种访问内核跟踪信息的尺度接口:
  1. onceday->~:# mount
  2. ......
  3. debugfs on /sys/kernel/debug type debugfs (rw,nosuid,nodev,noexec,relatime)
  4. tracefs on /sys/kernel/tracing type tracefs (rw,nosuid,nodev,noexec,relatime)
  5. tracefs on /sys/kernel/debug/tracing type tracefs (rw,nosuid,nodev,noexec,relatime)
  6. ......
复制代码
通过下面命令可以查看trace事件的输出格式,很多内核事件记录消息都是经过该方式输出:
  1. onceday->~:# cat /sys/kernel/debug/tracing/trace
  2. # tracer: nop
  3. #
  4. # entries-in-buffer/entries-written: 0/0   #P:4
  5. #
  6. #                                _-----=> irqs-off
  7. #                               / _----=> need-resched
  8. #                              | / _---=> hardirq/softirq
  9. #                              || / _--=> preempt-depth
  10. #                              ||| / _-=> migrate-disable
  11. #                              |||| /     delay
  12. #           TASK-PID     CPU#  |||||  TIMESTAMP                  FUNCTION
  13. #              | |         |   |||||     |                                 |
  14.             bash-1168242 [002] d...1 20343808.647931: bpf_trace_printk: Command from root: ls
复制代码
Uprobe 事件的输出格式通常包含以下字段:
(1) TASK-PID: 触发事件的进程名称和进程 ID (PID),TASK: 进程的名称,PID: 进程的 ID。
(2) CPU#: 事件发生在的 CPU 编号,表现事件是在哪个 CPU 上触发的。
(3) 标志位: 事件发生时的一些标志位,通常包括以下几个字符:


  • irqs-off: 表现中断是否关闭,d: 中断关闭(disabled),.: 中断启用(enabled)。
  • need-resched: 表现是否需要重新调理,N: 需要重新调理,.: 不需要重新调理。
  • hardirq/softirq: 表现是否在硬中断或软中断上下文中,H: 在硬中断上下文中,S: 在软中断上下文中,.: 不在硬中断或软中断上下文中。
  • preempt-depth: 表现抢占深度,数字: 当前的抢占深度。
  • migrate-disable: 表现是否禁用了进程迁移,D: 进程迁移被禁用,.: 进程迁移未被禁用。
(3) TIMESTAMP: 事件的时间戳,以秒为单位,精确到纳秒级别,表现事件发生的时间间隔体系启动的秒数。
(4) FUNCTION: 事件的名称,通常与注册 Uprobe 时指定的名称相同。
(5) 附加信息或参数: 事件的附加信息或参数,这部门内容取决于详细的 Uprobe 注册方式和通报的参数。
2. 命令行实践

2.1 命令行参数

Linux内核文档先容了这部门,可以参阅:kernel.org/doc/html/latest/trace/uprobetracer。
  1. p[:[GRP/][EVENT]] PATH:OFFSET [FETCHARGS]
  2. : Set a uprobe
  3. r[:[GRP/][EVENT]] PATH:OFFSET [FETCHARGS]
  4. : Set a return uprobe (uretprobe)
  5. p[:[GRP/][EVENT]] PATH:OFFSET%return [FETCHARGS] : Set a return uprobe (uretprobe)
  6. -:[GRP/][EVENT]
  7.                            : Clear uprobe or uretprobe event
  8. GRP           : Group name. If omitted, "uprobes" is the default value.
  9. EVENT         : Event name. If omitted, the event name is generated based
  10.                 on PATH+OFFSET.
  11. PATH          : Path to an executable or a library.
  12. OFFSET        : Offset where the probe is inserted.
  13. OFFSET%return : Offset where the return probe is inserted.
  14. FETCHARGS     : Arguments. Each probe can have up to 128 args.
  15. %REG         : Fetch register REG
  16. @ADDR        : Fetch memory at ADDR (ADDR should be in userspace)
  17. @+OFFSET     : Fetch memory at OFFSET (OFFSET from same file as PATH)
  18. $stackN      : Fetch Nth entry of stack (N >= 0)
  19. $stack       : Fetch stack address.
  20. $retval      : Fetch return value.(\*1)
  21. $comm        : Fetch current task comm.
  22. +|-[u]OFFS(FETCHARG) : Fetch memory at FETCHARG +|- OFFS address.(\*2)(\*3)
  23. \IMM         : Store an immediate value to the argument.
  24. NAME=FETCHARG     : Set NAME as the argument name of FETCHARG.
  25. FETCHARG:TYPE     : Set TYPE as the type of FETCHARG. Currently, basic types
  26.                      (u8/u16/u32/u64/s8/s16/s32/s64), hexadecimal types
  27.                      (x8/x16/x32/x64), "string" and bitfield are supported.
  28. (\*1) only for return probe.
  29. (\*2) this is useful for fetching a field of data structures.
  30. (\*3) Unlike kprobe event, "u" prefix will just be ignored, because uprobe
  31.       events can access only user-space memory.
复制代码
uprobe 的命令行参数形式如下:
(1) 设置 uprobe 事件:
  1. p[:[GRP/][EVENT]] PATH:OFFSET [FETCHARGS]
复制代码


  • GRP: 事件组名,可选。如果省略,默认值为 “uprobes”。
  • EVENT: 事件名,可选。如果省略,事件名将根据 PATH 和 OFFSET 自动生成。
  • PATH: 可实行文件或库的路径。
  • OFFSET: 插入探针的偏移量。
  • FETCHARGS: 探针的参数,每个探针最多可以有 128 个参数。
(2) 设置 return uprobe 事件(uretprobe):
  1. r[:[GRP/][EVENT]] PATH:OFFSET [FETCHARGS]
复制代码


  • GRP,EVENT,PATH,OFFSET 和 FETCHARGS 的含义与设置 uprobe 事件相同。
  • %return 表现在函数返回处插入探针。
(3) 扫除 uprobe 或 uretprobe 事件:
  1. -:[GRP/][EVENT]
复制代码


  • GRP 和 EVENT 的含义与设置事件相同。
(4) FETCHARGS 可以包含以下范例的参数:


  • %REG: 获取寄存器 REG 的值。
  • @ADDR: 获取用户空间地址 ADDR 处的内存值。
  • @+OFFSET: 获取与 PATH 相同文件的 OFFSET 处的内存值。
  • $stackN: 获取栈上第 N 个条目的值(N >= 0)。
  • $stack: 获取栈的地址。
  • $retval: 获取函数的返回值(仅适用于 return probe)。
  • $comm: 获取当前任务的 comm。
  • +|-OFFS(FETCHARG): 获取 FETCHARG 地址 +|- OFFS 处的内存值。
  • \IMM: 将立即数值存储到参数中。
  • NAME=FETCHARG: 将 FETCHARG 的参数名设置为 NAME。
  • FETCHARG:TYPE: 将 FETCHARG 的范例设置为 TYPE。支持的范例包括基本范例(u8/u16/u32/u64/s8/s16/s32/s64)、十六进制范例(x8/x16/x32/x64)、“string” 和位域。
Uprobe 跟踪器将根据给定的范例访问内存。前缀 ‘s’ 和 ‘u’ 表现这些范例分别是有符号和无符号的。‘x’ 前缀意味着它是无符号的。跟踪的参数以十进制(‘s’ 和 ‘u’)或十六进制(‘x’)显示。
如果没有范例转换,将根据架构利用 ‘x32’ 或 ‘x64’(例如,x86-32 利用 x32,x86-64 利用 x64)。
位域是另一种特殊范例,它接受 3 个参数:位宽、位偏移和容器大小(通常为 32)。
  1. b<bit-width>@<bit-offset>/<container-size>
复制代码


  • bit-width: 位宽,表现要获取的位的数量。
  • bit-offset: 位偏移,表现要获取的位的起始位置。
  • container-size: 容器大小,通常为 32,表现位域所在的整型变量的大小。
字符串范例 “string” 用于获取以空字符末了的字符串,对于 $comm,默认范例为 “string”,任何其他范例都是无效的
2.2 命令行利用uprobe

添加一个新的uprobe事件,例如读取bash的readline函数返回值,可以如下操作:
  1. # 1. 获取bash函数里的readline函数偏移地址, 使用nm
  2. onceday->tracing:# nm -D /usr/bin/bash |grep -w readline
  3. 00000000000d5690 T readline
  4. # 2. 获取bash函数里的readline函数偏移地址, 使用objdump
  5. onceday->tracing:# objdump -T /bin/bash | grep -w readline
  6. 00000000000d5690 g    DF .text  00000000000000c9  Base        readline
  7. # 3. 添加一个新的uretprobe事件
  8. onceday->tracing:# echo 'r:BashReadline /bin/bash:0xd5690 cmd=$retval' > /sys/kernel/tracing/uprobe_events
  9. # 4. 查看当前的uprobe事件
  10. onceday->tracing:# cat /sys/kernel/tracing/uprobe_events
  11. r:uprobes/BashReadline /bin/bash:0x00000000000d5690 cmd=$retval
  12. # 5. 使能uprobe追踪
  13. onceday->tracing:# echo 1 > events/uprobes/enable
复制代码
然后可以通过pipe查看trace输出信息,并且通过其他shell举行触发操作(操作bash shell):
  1. onceday->~:# cat /sys/kernel/tracing/trace_pipe
  2.            <...>-1238366 [001] ..... 20355397.380178: BashReadline: (0x55fb5bc51015 <- 0x55fb5bcf1690) cmd=0x55fb5c15bd30
复制代码
由于这里的参数是指针,所有输出是字符串指针地址,需要转换为string范例,才会打印输出,下面删除后重新创建:
  1. # 清除所有的uprobe事件
  2. echo > /sys/kernel/tracing/uprobe_events
  3. # 清除指定的uprobe事件
  4. echo '-:<uprobe事件名字>' >> /sys/kernel/tracing/uprobe_events
复制代码
下面是操作流程,先关闭uprobe使能,然后再扫除BashReadline事件:
  1. onceday->tracing:# echo 0 > events/uprobes/enable
  2. onceday->tracing:# echo '-:BashReadline' >> /sys/kernel/tracing/uprobe_events
  3. onceday->tracing:# cat /sys/kernel/tracing/uprobe_events
复制代码
然后重新添加新的uprobe事件,支持打印字符串:
  1. onceday->tracing:# echo 'r:BashReadline /bin/bash:0xd5690 cmd=+0($retval):string' > /sys/kernel/tracing/uprobe_events
  2. onceday->tracing:# cat /sys/kernel/tracing/uprobe_events
  3. r:uprobes/BashReadline /bin/bash:0x00000000000d5690 cmd=+0($retval):string
  4. onceday->~:# cat /sys/kernel/tracing/trace_pipe
  5.             bash-1168242 [002] ..... 20356173.202169: BashReadline: (0x55c737e9c015 <- 0x55c737f3c690) cmd="cat /sys/kernel/tracing/trace_pipe"
  6.             bash-1238366 [001] ..... 20356186.907223: BashReadline: (0x55fb5bc51015 <- 0x55fb5bcf1690) cmd=""
  7.             bash-1238366 [001] ..... 20356187.116360: BashReadline: (0x55fb5bc51015 <- 0x55fb5bcf1690) cmd=""
  8.             bash-1238366 [001] ..... 20356187.288427: BashReadline: (0x55fb5bc51015 <- 0x55fb5bcf1690) cmd=""
  9.             bash-1238366 [001] ..... 20356188.615086: BashReadline: (0x55fb5bc51015 <- 0x55fb5bcf1690) cmd="ls"
复制代码
可以查看对应事件的输出内容格式,包括用户自界说和体系默认两部门:
  1. onceday->tracing:# cat events/uprobes/BashReadline/format
  2. name: BashReadline
  3. ID: 1962
  4. format:
  5.         field:unsigned short common_type;       offset:0;       size:2; signed:0;
  6.         field:unsigned char common_flags;       offset:2;       size:1; signed:0;
  7.         field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
  8.         field:int common_pid;   offset:4;       size:4; signed:1;
  9.         field:unsigned long __probe_func;       offset:8;       size:8; signed:0;
  10.         field:unsigned long __probe_ret_ip;     offset:16;      size:8; signed:0;
  11.         field:__data_loc char[] cmd;    offset:24;      size:4; signed:1;
  12. print fmt: "(%lx <- %lx) cmd="%s"", REC->__probe_func, REC->__probe_ret_ip, __get_str(cmd)
复制代码
2.2 perf+uprobe利用

perf probe是Linux性能分析工具perf的一个子命令,用于动态地在用户步调或内核中插入探测点,以便举行性能分析,如下:

  • 在函数的入口和返回点插入探测点。
  • 在指定的代码行插入探测点。
  • 在变量读写处插入探测点。
利用perf probe可以在不修改源代码和重新编译的环境下,对步调举行细粒度的性能分析。

  • 通过perf probe -x /path/to/binary --add='probe_name line_num'在目的步调的指定行插入一个探测点,探测点名称可自界说
  • 通过perf record -e probe_name ./binary运行步调并记录探测点信息。
  • 通过perf report分析perf.data文件,可以看到探测点被掷中的次数、耗时等统计信息。
  • 过后用perf probe --del=probe_name移除探测点,无需重启步调。
下面是一个实例展示:
(1) 利用 perf probe 命令来界说一个 uprobe 事件:
  1. perf probe -x /bin/bash readline
复制代码
这个命令会在 /bin/bash 可实行文件中的 readline 函数处创建一个 uprobe 事件。
(2) 利用 perf record 命令来记录 uprobe 事件:
  1. perf record -e probe_bash:readline -aR
复制代码
这个命令会启动 perf 记录,并捕捉 probe_bash:readline 事件的信息。-a 选项表现记录所有 CPU 上的事件,-R 选项表现记录函数的返回值。
(3) 在另一个终端中运行 bash,并实行一些命令来触发 readline 函数:
  1. bash
  2. ls
  3. cd /tmp
复制代码
(4) 然后停止 perf 记录,利用 Ctrl+C 停止 perf record 命令
(5) 利用 perf script 命令来查看记录的事件信息,这个命令会显示记录的事件信息,包括触发事件的进程、时间戳、函数名称以及返回值。
  1. onceday->~:# perf script
  2.             bash 1452910 [001] 20389814.622780: probe_bash:readline: (5650f6928690)
  3.             bash 1452910 [001] 20389815.240841: probe_bash:readline: (5650f6928690)
  4.             bash 1452910 [001] 20389815.450196: probe_bash:readline: (5650f6928690)
  5.             bash 1452910 [001] 20389815.621115: probe_bash:readline: (5650f6928690)
  6.             bash 1452910 [001] 20389817.092868: probe_bash:readline: (5650f6928690)
  7.             bash 1452910 [001] 20389822.188101: probe_bash:readline: (5650f6928690)
复制代码
这个输出表现在 bash 进程(PID 为 1452910)中触发了 readline 函数,返回值为 0x5650f6928690。
3. 编码实践

3.1 编译uprobe+ebpf模块

uprobe和eBPF结合利用,可以实现对用户态步调的动态跟踪和性能分析,而无需修改步调源代码或重启进程。
它们的组合利用流程如下:

  • uprobe在用户态步调的指定位置插入探测点,当步调实行到该处时会触发uprobe事件。
  • 触发的uprobe事件将实行eBPF步调,该步调是事先编写并加载到内核中的。
  • eBPF步调可以访问uprobe通报的上下文信息,如函数参数、局部变量等,也可以调用辅助函数统计数据。
  • eBPF步调处理完成后,将统计数据写入eBPF map或perf buffer,用户态步调可以读取并分析这些数据。
一些利用uprobe+eBPF的开源工具:


  • bcc: BPF Compiler Collection,提供了大量uprobe+eBPF的案例和工具集
  • bpftrace: 专为eBPF打造的高级跟踪语言和工具
  • libbpf: eBPF步调加载运行库,结合uprobe API可定制开发跟踪工具
3.2 ebpf源码

下面是一个记任命户堆栈信息的ebpf的代码:
  1. #include <unistd.h>
  2. #include <linux/sched.h>
  3. #include <linux/ptrace.h>
  4. #include <linux/bpf.h>
  5. #include <linux/perf_event.h>
  6. #include <bpf/bpf_helpers.h>
  7. #include <bpf/bpf_tracing.h>
  8. // Define stack data map.
  9. struct bpf_map_def SEC("maps") stack_map = {
  10.     .type        = BPF_MAP_TYPE_STACK_TRACE,
  11.     .key_size    = sizeof(__u32),
  12.     .value_size  = PERF_MAX_STACK_DEPTH * sizeof(__u64),
  13.     .max_entries = 10000,
  14. };
  15. SEC("uprobe/StackPrint")
  16. int printForRoot(struct pt_regs *ctx)
  17. {
  18.     int ret;
  19.     // Get the user stack and print it to the kernel log.
  20.     ret = bpf_get_stackid(ctx, &stack_map, BPF_F_USER_STACK);
  21.     if (ret < 0) {
  22.         bpf_printk("Stack error: %d", ret);
  23.         return 0;
  24.     }
  25.     // Print the stack to the kernel log.
  26.     bpf_printk("Stack id: %d", ret);
  27.     return 0;
  28. }
  29. /*  定义 LICENSE */
  30. char LICENSE[] SEC("license") = "GPL";
复制代码
这是一段 eBPF (extended Berkeley Packet Filter) 步调的代码,用于在 Linux 内核中跟踪和打印用户空间步调的调用栈信息。


  • 头文件引入了须要的 Linux 内核头文件和 eBPF 辅助函数库。
  • 界说了一个名为 stack_map 的 eBPF map,范例为 BPF_MAP_TYPE_STACK_TRACE,用于存储调用栈信息。
  • 利用 SEC("uprobe/StackPrint") 宏界说了一个 uprobe 范例的 eBPF 步调 printForRoot,当被追踪的用户步调实行到特定位置时会触发该步调。
  • 在 printForRoot 函数中:

    • 通过 bpf_get_stackid 函数获取当前用户空间步调的调用栈,并将栈 ID 存储在 stack_map 中。
    • 利用 bpf_printk 函数将栈 ID 打印到内核日志中。

  • 最后利用 char LICENSE[] SEC("license") = "GPL"; 界说了该 eBPF 步调利用的允许证范例为 GPL。
3.3 用户空间源码

用户空间需要负载加载ebpf步调到内核中,并且读取bpf map数据,然后打印,借助libbpf库实现,如下:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <bpf/bpf.h>
  5. #include <bpf/libbpf.h>
  6. #include <bfd.h>
  7. static void print_stack(uint64_t *ips)
  8. {
  9.     static bool warned;
  10.     int         i;
  11.     for (i = 126; i >= 0; i--) {
  12.         if (!ips[i]) {
  13.             continue;
  14.         }
  15.         printf("0x%lx;", ips[i] - 0x555555554000ul);
  16.         /* 解析符号, 使用bfd */
  17.     }
  18.     printf("\n");
  19. }
  20. int main(int argc, char **argv)
  21. {
  22.     struct bpf_link    *link;
  23.     struct bpf_program *prog;
  24.     struct bpf_object  *obj;
  25.     int                 map_fd;
  26.     int                 count = 0;
  27.     uint32_t            key = 0, next_key = 0;
  28.     uint64_t            value[127] = {0};
  29.     link = NULL;
  30.     prog = NULL;
  31.     obj  = NULL;
  32.     // 读取 BPF 程序
  33.     obj = bpf_object__open_file("./output/ebpf/ebpf_print.o", NULL);
  34.     if (libbpf_get_error(obj)) {
  35.         fprintf(stderr, "Error opening BPF object file.\n");
  36.         return 1;
  37.     }
  38.     // 加载 BPF 对象到内核
  39.     if (bpf_object__load(obj)) {
  40.         fprintf(stderr, "Error loading BPF object file.\n");
  41.         bpf_object__close(obj);
  42.         return 1;
  43.     }
  44.     // 加载 uprobe 处理函数
  45.     prog = bpf_object__find_program_by_title(obj, "uprobe/StackPrint");
  46.     if (!prog) {
  47.         fprintf(stderr, "Error finding uprobe program.\n");
  48.         goto cleanup;
  49.     }
  50.     // dump BPF 程序
  51.     printf("BPF program try to attach uprobe:\n");
  52.     // 将 BPF 程序附加到 uprobe 点, 获取readline的返回值
  53.     link = bpf_program__attach_uprobe(prog, true, -1, "./output/bin/anmk_ebpf_test", 0xa286);
  54.     if (libbpf_get_error(link)) {
  55.         fprintf(stderr, "Error attaching uprobe.\n");
  56.         goto cleanup;
  57.     }
  58.     // 读取和处理 uprobe 事件
  59.     map_fd = bpf_object__find_map_fd_by_name(obj, "stack_map");
  60.     while (count < 100) {
  61.         // 读取 bpf map数据
  62.         sleep(1);
  63.         printf("Read stack map data:\n");
  64.         while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
  65.             bpf_map_lookup_elem(map_fd, &next_key, &value);
  66.             print_stack(value);
  67.             bpf_map_delete_elem(map_fd, &next_key);
  68.             key = next_key;
  69.         }
  70.         count++;
  71.     }
  72. cleanup:
  73.     if (link) {
  74.         bpf_link__destroy(link);
  75.     }
  76.     if (obj) {
  77.         bpf_object__unload(obj);
  78.         bpf_object__close(obj);
  79.     }
  80.     return 0;
  81. }
复制代码
这部门代码是一个用户空间步调,用于加载和运行前面提到的 eBPF 步调,并读取和处理 eBPF 步调生成的调用栈信息。


  • 引入了须要的头文件,包括尺度 C 库、libbpf 库和 bfd 库(用于解析符号信息)。
  • 界说了 print_stack 函数,用于打印 eBPF 步调生成的调用栈信息。目前只打印了指令地址,符号解析部门还未实现。
  • 在 main 函数中:

    • 利用 bpf_object__open_file 函数打开编译好的 eBPF 目的文件。
    • 利用 bpf_object__load 函数将 eBPF 对象加载到内核中。
    • 利用 bpf_object__find_program_by_title 函数查找名为 “uprobe/StackPrint” 的 eBPF 步调。
    • 利用 bpf_program__attach_uprobe 函数将 eBPF 步调附加到指定的用户空间步调 (“./output/bin/anmk_ebpf_test”) 的指定位置 (0xa286)。
    • 进入一个循环,每隔 1 秒读取 eBPF map 中的调用栈数据,并利用 print_stack 函数打印调用栈信息。
    • 循环 100 次后退出循环,清理资源并退出步调。

  • 在 cleanup 标签处,烧毁 eBPF 链接,卸载 eBPF 对象,并关闭 eBPF 对象文件。
3.4 ebpf编译

ebpf步调编译需要用到clang编译器,cmake编译脚本如下所示:
  1. # 查找Clang编译器和llvm-link工具, 用于eBPF编译
  2. find_program(CLANG_EBPF_COMPILER clang)
  3. if(NOT CLANG_EBPF_COMPILER)
  4.     message(FATAL_ERROR "Clang compiler or llvm-link tool not found for eBPF compilation")
  5. endif()
  6. # 查找LLVM工具, link, opt, llc, objcopy
  7. find_program(LLVM_LINK_TOOL llvm-link)
  8. if (NOT LLVM_LINK_TOOL)
  9.     message(FATAL_ERROR "LLVM link tool not found")
  10. endif()
  11. find_program(LLVM_OPT opt)
  12. if (NOT LLVM_OPT)
  13.     message(STATUS "LLVM opt tool not found")
  14. endif()
  15. find_program(LLVM_LLC llc)
  16. if (NOT LLVM_LLC)
  17.     message(STATUS "LLVM llc tool not found")
  18. endif()
  19. find_program(LLVM_OBJCOPY llvm-objcopy)
  20. if (NOT LLVM_OBJCOPY)
  21.     message(FATAL_ERROR "LLVM objcopy tool not found")
  22. endif()
  23. # 设置eBPF C文件
  24. set(EBPF_SOURCES
  25.     print.c
  26. )
  27. # 设置编译选项
  28. set(EBPF_C_FLAGS
  29.     -O2                     # 优化级别
  30.     -m64                    # 64位
  31.     -U __GNUC__             # 不包含__GNUC__宏定义
  32.     -D__TARGET_ARCH_x86     # 定义__TARGET_ARCH_x86宏
  33.     -D__x86_64__            # 定义__TARGET_ARCH_x86_64宏
  34.     # -mstack-alignment=16  # 栈对齐16字节
  35.     # -isystem /usr/include/x86_64-linux-gnu # 包含系统头文件目录
  36.     -idirafter /usr/lib/llvm-14/lib/clang/14.0.0/include
  37.     -idirafter /usr/local/include
  38.     -idirafter /usr/include/x86_64-linux-gnu
  39.     -idirafter /usr/include
  40.     -target bpf             # 目标平台
  41.     -march=bpf              # 指定BPF指令集
  42.     -Wall                   # 显示所有警告
  43.     -Werror                 # 警告转为错误
  44.     -Wno-unused-value       # 不显示未使用的值警告
  45.     -fno-asynchronous-unwind-tables # 不生成异步取消表
  46.     -fno-jump-tables        # 不生成跳转表
  47.     -fno-stack-protector    # 不生成栈保护
  48.     #-fno-builtin            # 不使用内建函数
  49.     #-nostdinc               # 不包含标准头文件
  50. )
  51. # 设置eBPF IR文件
  52. set(EBPF_BC_FILES)
  53. # 编译eBPF IR文件
  54. foreach(ebpf_file ${EBPF_SOURCES})
  55.     # 成功编译的eBPF IR文件加入列表
  56.     get_filename_component(ebpf_file_we ${ebpf_file} NAME_WE)
  57.     execute_process(
  58.         COMMAND ${CLANG_EBPF_COMPILER} ${EBPF_C_FLAGS} -emit-llvm -c ${ebpf_file} -o ${ebpf_file_we}.bc
  59.         WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  60.         RESULT_VARIABLE CMD_RESULT
  61.         COMMAND_ECHO STDOUT
  62.     )
  63.     if (NOT CMD_RESULT EQUAL 0)
  64.         message(FATAL_ERROR "Failed to compile eBPF IR file ${ebpf_file}: ${CMD_RESULT}")
  65.     endif()
  66.     list(APPEND EBPF_BC_FILES ${ebpf_file_we}.bc)
  67. endforeach()
  68. # 如果没有eBPF IR文件, 则退出
  69. if(NOT EBPF_BC_FILES)
  70.     message(FATAL_ERROR "No eBPF IR files generated")
  71. endif()
  72. # 链接eBPF IR文件到一个目标文件
  73. execute_process(
  74.     COMMAND ${LLVM_LINK_TOOL} -o ebpf_combined.bc ${EBPF_BC_FILES}
  75.     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  76.     COMMAND_ECHO STDOUT
  77. )
  78. # 优化eBPF IR文件
  79. execute_process(
  80.     COMMAND ${LLVM_OPT} -O2 -o ebpf_combined_opt.bc ebpf_combined.bc
  81.     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  82.     COMMAND_ECHO STDOUT
  83. )
  84. # 生成eBPF字节码
  85. execute_process(
  86.     COMMAND ${LLVM_LLC} -march=bpf -filetype=obj ebpf_combined_opt.bc -o ebpf_program.o
  87.     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  88.     COMMAND_ECHO STDOUT
  89. )
  90. # 安装eBPF字节码到指定目录
  91. execute_process(
  92.     COMMAND ${CMAKE_COMMAND} -E copy ebpf_program.o ${TOPDIR}/output/ebpf/ebpf_print.o
  93.     WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
  94.     COMMAND_ECHO STDOUT
  95. )
复制代码
eBPF 步调的编译流程如下:

  • 查找 Clang 编译器和 LLVM 工具链,Clang 编译器用于编译 eBPF 步调。LLVM 工具链中的 llvm-link、opt、llc 和 llvm-objcopy 工具,用于链接、优化和生成 eBPF 字节码。
  • 设置 eBPF C 源文件和编译选项,指定 eBPF 步调的 C 源文件列表 (EBPF_SOURCES),以及设置 eBPF 步调的编译选项 (EBPF_C_FLAGS),包括优化级别、目的平台、包含路径等。
  • 编译 eBPF C 源文件为 LLVM IR(中间表现),遍历 eBPF C 源文件列表,对每个文件实行以下步骤:

    • 利用 Clang 编译器将 C 源文件编译为 LLVM IR 文件 (.bc)。
    • 如果编译成功,将生成的 .bc 文件添加到 EBPF_BC_FILES 列表中。
    • 如果没有成功编译任何 eBPF C 源文件,则报错并退出。

  • 链接 LLVM IR 文件,利用 llvm-link 工具将所有生成的 .bc 文件链接到一个名为 ebpf_combined.bc 的文件中。
  • 优化 LLVM IR,利用 opt 工具对 ebpf_combined.bc 举行优化,生成优化后的 LLVM IR 文件 ebpf_combined_opt.bc。
  • 生成 eBPF 字节码,利用 llc 工具将优化后的 LLVM IR 文件编译为 eBPF 字节码,生成目的文件 ebpf_program.o。
  • 安装 eBPF 字节码,将生成的 eBPF 字节码文件 ebpf_program.o 复制到指定的输出目录 (${TOPDIR}/output/ebpf/ebpf_print.o)。
编译成功之后,会生成一个ebpf elf文件,如下所示:
  1. ubuntu->ANMK_netdog:$ readelf -h output/ebpf/ebpf_print.o
  2. ELF Header:
  3.   Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  4.   Class:                             ELF64
  5.   Data:                              2's complement, little endian
  6.   Version:                           1 (current)
  7.   OS/ABI:                            UNIX - System V
  8.   ABI Version:                       0
  9.   Type:                              REL (Relocatable file)
  10.   Machine:                           Linux BPF
  11.   Version:                           0x1
  12.   Entry point address:               0x0
  13.   Start of program headers:          0 (bytes into file)
  14.   Start of section headers:          688 (bytes into file)
  15.   Flags:                             0x0
  16.   Size of this header:               64 (bytes)
  17.   Size of program headers:           0 (bytes)
  18.   Number of program headers:         0
  19.   Size of section headers:           64 (bytes)
  20.   Number of section headers:         9
  21.   Section header string table index: 1
复制代码
3.5 运行和输出

用户空间的步调利用Gcc正常编译即可,然后运行举行测试和验证:
  1. ubuntu->ANMK_netdog:$ sudo ./output/bin/anmk_uprobe_print
  2. libbpf: elf: skipping unrecognized data section(6) .rodata.str1.1
  3. BPF program try to attach uprobe:
  4. Read stack map data:
  5. 0x2a3cced16d90;0xb934d0717f;0x2a3ccef1e302;0x2a3ccef1dd54;0x2a3ccef1da7e;0xb934d0704a;0xb934d06d59;0xb934d06b68;
  6. 0x2a3cced16d90;0xb934d0717f;0x2a3ccef1e302;0x2a3ccef1dd54;0x2a3ccef1da7e;0xb934d068dc;0xb934d06751;
  7. 0x2a3cced16d90;0xb934d0717f;0x2a3ccef1e302;0x2a3ccef1dd54;0x2a3ccef1da7e;0xb934d0704a;0xb934d06d0f;0xb934d06b68;
  8. ......
复制代码
可以看到,用户空间的步调正常将bpf map中的数据读取出来,但是没有转换为符号名称,由于这些地址都是虚拟地址,需要二次转换才能通过addr2line转换为符号地址。
在/sys/kernel/debug/tracing/trace_pipe中,也可以读取到如下输出:
  1. ubuntu->~:$ sudo cat /sys/kernel/debug/tracing/trace_pipe
  2.   anmk_ebpf_test-1853091 [002] d...1 20451764.633240: bpf_trace_printk: Stack id: 15475
  3.   anmk_ebpf_test-1853091 [002] d...1 20451764.633287: bpf_trace_printk: Stack id: 15475
  4.   anmk_ebpf_test-1853091 [002] d...1 20451764.633320: bpf_trace_printk: Stack id: 15475
  5.   anmk_ebpf_test-1853091 [002] d...1 20451764.633355: bpf_trace_printk: Stack id: 15475
  6.   anmk_ebpf_test-1853091 [002] d...1 20451764.633394: bpf_trace_printk: Stack id: 15475
  7.   .....
复制代码
4. 总结

本文简朴的根据uprobe文档现实操作了一波,见识到了uprobe的作用,但是离现实应用另有一段较大的间隔,uprobe和ebpf这些工具利用,最大的阻碍在于内核的熟悉度,由于无法利用常见的尺度库功能,好比堆栈打印和数据获取,这就必须从虚拟内存映射出发,在内核内里解析出对应的现实偏移量。
想要纯熟的利用这些工具,必须深入学习Linux源码和相关的例子,门槛照旧很高,道阻且长!







   Once Day

  

    也信美人终作土,不堪幽梦太匆匆......
    如果这篇文章为您带来了帮助或启发,不妨点个赞

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

商道如狼道

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表