Linux之ebpf(3)uprobe与ebpf

news2024/9/20 9:40:21

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博客

文章目录

  • Linux之ebpf(3)uprobe简要使用
        • 1. 概述
          • 1.1 介绍
          • 1.2 kprobe和uprobe联系和区别
          • 1.3 uprobe原理简要
          • 1.4 uprobe输出信息
        • 2. 命令行实践
          • 2.1 命令行参数
          • 2.2 命令行使用uprobe
          • 2.2 perf+uprobe使用
        • 3. 编码实践
          • 3.1 编译uprobe+ebpf模块
          • 3.2 ebpf源码
          • 3.3 用户空间源码
          • 3.4 ebpf编译
          • 3.5 运行和输出
        • 4. 总结

1. 概述
1.1 介绍

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

Uprobe 的工作原理如下:

  1. 在目标应用程序的特定指令位置设置探测点。当程序执行到该指令时,会触发探测点。

  2. 探测点被触发后,程序执行流程会被中断,并将控制权转移给预先注册的探测处理程序。

  3. 探测处理程序可以访问寄存器、内存等程序运行时的上下文信息,以此来分析和记录程序的状态。

  4. 处理完成后,控制权会返回给原始程序,程序继续执行。

Uprobe 的优势在于:

  • 动态性:可以在运行时动态地插入和删除探测点,无需重启应用程序。

  • 低开销:探测点的插入和删除对应用程序性能影响很小。

  • 灵活性:可以在应用程序的任意指令位置设置探测点,获取丰富的运行时信息。

  • 与其他工具的集成:可以与其他追踪和性能分析工具(如 ftrace、perf 等)结合使用。

Uprobe 在实际应用中有广泛的用途,例如:

  1. 性能剖析和优化:通过收集关键函数的调用次数、执行时间等指标,发现性能瓶颈。

  2. 故障诊断和调试:通过记录异常发生时的程序状态, 快速定位和解决 bug。

  3. 安全监控和审计:通过监视特定函数的执行,发现可疑行为和潜在的安全威胁。

  4. 业务逻辑分析:通过跟踪特定函数参数和返回值,洞察应用程序的业务流程和逻辑。

要使用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 是一个用于跟踪和调试的虚拟文件系统,它提供了一种访问内核跟踪信息的标准接口:

onceday->~:# mount
......
debugfs on /sys/kernel/debug type debugfs (rw,nosuid,nodev,noexec,relatime)
tracefs on /sys/kernel/tracing type tracefs (rw,nosuid,nodev,noexec,relatime)
tracefs on /sys/kernel/debug/tracing type tracefs (rw,nosuid,nodev,noexec,relatime)
......

通过下面命令可以查看trace事件的输出格式,很多内核事件记录消息都是经过该方式输出:

onceday->~:# cat /sys/kernel/debug/tracing/trace
# tracer: nop
#
# entries-in-buffer/entries-written: 0/0   #P:4
#
#                                _-----=> irqs-off
#                               / _----=> need-resched
#                              | / _---=> hardirq/softirq
#                              || / _--=> preempt-depth
#                              ||| / _-=> migrate-disable
#                              |||| /     delay
#           TASK-PID     CPU#  |||||  TIMESTAMP  		FUNCTION
#              | |         |   |||||     |         			|
            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。

p[:[GRP/][EVENT]] PATH:OFFSET [FETCHARGS] : Set a uprobe
r[:[GRP/][EVENT]] PATH:OFFSET [FETCHARGS] : Set a return uprobe (uretprobe)
p[:[GRP/][EVENT]] PATH:OFFSET%return [FETCHARGS] : Set a return uprobe (uretprobe)
-:[GRP/][EVENT]                           : Clear uprobe or uretprobe event

GRP           : Group name. If omitted, "uprobes" is the default value.
EVENT         : Event name. If omitted, the event name is generated based
                on PATH+OFFSET.
PATH          : Path to an executable or a library.
OFFSET        : Offset where the probe is inserted.
OFFSET%return : Offset where the return probe is inserted.

FETCHARGS     : Arguments. Each probe can have up to 128 args.
 %REG         : Fetch register REG
 @ADDR        : Fetch memory at ADDR (ADDR should be in userspace)
 @+OFFSET     : Fetch memory at OFFSET (OFFSET from same file as PATH)
 $stackN      : Fetch Nth entry of stack (N >= 0)
 $stack       : Fetch stack address.
 $retval      : Fetch return value.(\*1)
 $comm        : Fetch current task comm.
 +|-[u]OFFS(FETCHARG) : Fetch memory at FETCHARG +|- OFFS address.(\*2)(\*3)
 \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" and bitfield are supported.

(\*1) only for return probe.
(\*2) this is useful for fetching a field of data structures.
(\*3) Unlike kprobe event, "u" prefix will just be ignored, because uprobe
      events can access only user-space memory.

uprobe 的命令行参数形式如下:

(1) 设置 uprobe 事件:

p[:[GRP/][EVENT]] PATH:OFFSET [FETCHARGS]
  • GRP: 事件组名,可选。如果省略,默认值为 “uprobes”。
  • EVENT: 事件名,可选。如果省略,事件名将根据 PATH 和 OFFSET 自动生成。
  • PATH: 可执行文件或库的路径。
  • OFFSET: 插入探针的偏移量。
  • FETCHARGS: 探针的参数,每个探针最多可以有 128 个参数。

(2) 设置 return uprobe 事件(uretprobe):

r[:[GRP/][EVENT]] PATH:OFFSET [FETCHARGS]
  • GRPEVENTPATHOFFSETFETCHARGS 的含义与设置 uprobe 事件相同。
  • %return 表示在函数返回处插入探针。

(3) 清除 uprobe 或 uretprobe 事件:

-:[GRP/][EVENT]
  • GRPEVENT 的含义与设置事件相同。

(4) FETCHARGS 可以包含以下类型的参数:

  • %REG: 获取寄存器 REG 的值。
  • @ADDR: 获取用户空间地址 ADDR 处的内存值。
  • @+OFFSET: 获取与 PATH 相同文件的 OFFSET 处的内存值。
  • $stackN: 获取栈上第 N 个条目的值(N >= 0)。
  • $stack: 获取栈的地址。
  • $retval: 获取函数的返回值(仅适用于 return probe)。
  • $comm: 获取当前任务的 comm。
  • +|-[u]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)。

b<bit-width>@<bit-offset>/<container-size>
  • bit-width: 位宽,表示要获取的位的数量。
  • bit-offset: 位偏移,表示要获取的位的起始位置。
  • container-size: 容器大小,通常为 32,表示位域所在的整型变量的大小。

字符串类型 “string” 用于获取以空字符结尾的字符串,对于 $comm,默认类型为 “string”,任何其他类型都是无效的

2.2 命令行使用uprobe

添加一个新的uprobe事件,例如读取bash的readline函数返回值,可以如下操作:

# 1. 获取bash函数里的readline函数偏移地址, 使用nm
onceday->tracing:# nm -D /usr/bin/bash |grep -w readline
00000000000d5690 T readline
# 2. 获取bash函数里的readline函数偏移地址, 使用objdump
onceday->tracing:# objdump -T /bin/bash | grep -w readline
00000000000d5690 g    DF .text  00000000000000c9  Base        readline
# 3. 添加一个新的uretprobe事件
onceday->tracing:# echo 'r:BashReadline /bin/bash:0xd5690 cmd=$retval' > /sys/kernel/tracing/uprobe_events
# 4. 查看当前的uprobe事件
onceday->tracing:# cat /sys/kernel/tracing/uprobe_events
r:uprobes/BashReadline /bin/bash:0x00000000000d5690 cmd=$retval
# 5. 使能uprobe追踪
onceday->tracing:# echo 1 > events/uprobes/enable

然后可以通过pipe查看trace输出信息,并且通过其他shell进行触发操作(操作bash shell):

onceday->~:# cat /sys/kernel/tracing/trace_pipe
           <...>-1238366 [001] ..... 20355397.380178: BashReadline: (0x55fb5bc51015 <- 0x55fb5bcf1690) cmd=0x55fb5c15bd30

由于这里的参数是指针,所有输出是字符串指针地址,需要转换为string类型,才会打印输出,下面删除后重新创建:

# 清除所有的uprobe事件
echo > /sys/kernel/tracing/uprobe_events
# 清除指定的uprobe事件
echo '-:<uprobe事件名字>' >> /sys/kernel/tracing/uprobe_events

下面是操作流程,先关闭uprobe使能,然后再清除BashReadline事件:

onceday->tracing:# echo 0 > events/uprobes/enable
onceday->tracing:# echo '-:BashReadline' >> /sys/kernel/tracing/uprobe_events
onceday->tracing:# cat /sys/kernel/tracing/uprobe_events

然后重新添加新的uprobe事件,支持打印字符串:

onceday->tracing:# echo 'r:BashReadline /bin/bash:0xd5690 cmd=+0($retval):string' > /sys/kernel/tracing/uprobe_events
onceday->tracing:# cat /sys/kernel/tracing/uprobe_events
r:uprobes/BashReadline /bin/bash:0x00000000000d5690 cmd=+0($retval):string
onceday->~:# cat /sys/kernel/tracing/trace_pipe
            bash-1168242 [002] ..... 20356173.202169: BashReadline: (0x55c737e9c015 <- 0x55c737f3c690) cmd="cat /sys/kernel/tracing/trace_pipe"
            bash-1238366 [001] ..... 20356186.907223: BashReadline: (0x55fb5bc51015 <- 0x55fb5bcf1690) cmd=""
            bash-1238366 [001] ..... 20356187.116360: BashReadline: (0x55fb5bc51015 <- 0x55fb5bcf1690) cmd=""
            bash-1238366 [001] ..... 20356187.288427: BashReadline: (0x55fb5bc51015 <- 0x55fb5bcf1690) cmd=""
            bash-1238366 [001] ..... 20356188.615086: BashReadline: (0x55fb5bc51015 <- 0x55fb5bcf1690) cmd="ls"

可以查看对应事件的输出内容格式,包括用户自定义和系统默认两部分:

onceday->tracing:# cat events/uprobes/BashReadline/format 
name: BashReadline
ID: 1962
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_func;       offset:8;       size:8; signed:0;
        field:unsigned long __probe_ret_ip;     offset:16;      size:8; signed:0;
        field:__data_loc char[] cmd;    offset:24;      size:4; signed:1;

print fmt: "(%lx <- %lx) cmd=\"%s\"", REC->__probe_func, REC->__probe_ret_ip, __get_str(cmd)
2.2 perf+uprobe使用

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

  1. 在函数的入口和返回点插入探测点。
  2. 在指定的代码行插入探测点。
  3. 在变量读写处插入探测点。

使用perf probe可以在不修改源代码和重新编译的情况下,对程序进行细粒度的性能分析。

  1. 通过perf probe -x /path/to/binary --add='probe_name line_num'在目标程序的指定行插入一个探测点,探测点名称可自定义
  2. 通过perf record -e probe_name ./binary运行程序并记录探测点信息。
  3. 通过perf report分析perf.data文件,可以看到探测点被命中的次数、耗时等统计信息。
  4. 事后用perf probe --del=probe_name移除探测点,无需重启程序。

下面是一个实例展示:

(1) 使用 perf probe 命令来定义一个 uprobe 事件:

perf probe -x /bin/bash readline

这个命令会在 /bin/bash 可执行文件中的 readline 函数处创建一个 uprobe 事件。

(2) 使用 perf record 命令来记录 uprobe 事件:

perf record -e probe_bash:readline -aR

这个命令会启动 perf 记录,并捕获 probe_bash:readline 事件的信息。-a 选项表示记录所有 CPU 上的事件,-R 选项表示记录函数的返回值。

(3) 在另一个终端中运行 bash,并执行一些命令来触发 readline 函数:

bash
ls
cd /tmp

(4) 然后停止 perf 记录,使用 Ctrl+C 终止 perf record 命令

(5) 使用 perf script 命令来查看记录的事件信息,这个命令会显示记录的事件信息,包括触发事件的进程、时间戳、函数名称以及返回值。

onceday->~:# perf script
            bash 1452910 [001] 20389814.622780: probe_bash:readline: (5650f6928690)
            bash 1452910 [001] 20389815.240841: probe_bash:readline: (5650f6928690)
            bash 1452910 [001] 20389815.450196: probe_bash:readline: (5650f6928690)
            bash 1452910 [001] 20389815.621115: probe_bash:readline: (5650f6928690)
            bash 1452910 [001] 20389817.092868: probe_bash:readline: (5650f6928690)
            bash 1452910 [001] 20389822.188101: probe_bash:readline: (5650f6928690)

这个输出表示在 bash 进程(PID 为 1452910)中触发了 readline 函数,返回值为 0x5650f6928690。

3. 编码实践
3.1 编译uprobe+ebpf模块

uprobe和eBPF结合使用,可以实现对用户态程序的动态跟踪和性能分析,而无需修改程序源代码或重启进程。

它们的组合使用流程如下:

  1. uprobe在用户态程序的指定位置插入探测点,当程序执行到该处时会触发uprobe事件。

  2. 触发的uprobe事件将执行eBPF程序,该程序是事先编写并加载到内核中的。

  3. eBPF程序可以访问uprobe传递的上下文信息,如函数参数、局部变量等,也可以调用辅助函数统计数据。

  4. eBPF程序处理完成后,将统计数据写入eBPF map或perf buffer,用户态程序可以读取并分析这些数据。

一些使用uprobe+eBPF的开源工具:

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

下面是一个记录用户堆栈信息的ebpf的代码:

#include <unistd.h>
#include <linux/sched.h>
#include <linux/ptrace.h>
#include <linux/bpf.h>
#include <linux/perf_event.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

// Define stack data map.
struct bpf_map_def SEC("maps") stack_map = {
    .type        = BPF_MAP_TYPE_STACK_TRACE,
    .key_size    = sizeof(__u32),
    .value_size  = PERF_MAX_STACK_DEPTH * sizeof(__u64),
    .max_entries = 10000,
};

SEC("uprobe/StackPrint")

int printForRoot(struct pt_regs *ctx)
{
    int ret;

    // Get the user stack and print it to the kernel log.
    ret = bpf_get_stackid(ctx, &stack_map, BPF_F_USER_STACK);
    if (ret < 0) {
        bpf_printk("Stack error: %d", ret);
        return 0;
    }

    // Print the stack to the kernel log.
    bpf_printk("Stack id: %d", ret);

    return 0;
}

/*  定义 LICENSE */
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库实现,如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <bfd.h>

static void print_stack(uint64_t *ips)
{
    static bool warned;
    int         i;

    for (i = 126; i >= 0; i--) {
        if (!ips[i]) {
            continue;
        }
        printf("0x%lx;", ips[i] - 0x555555554000ul);
        /* 解析符号, 使用bfd */

    }

    printf("\n");
}

int main(int argc, char **argv)
{
    struct bpf_link    *link;
    struct bpf_program *prog;
    struct bpf_object  *obj;
    int                 map_fd;
    int                 count = 0;
    uint32_t            key = 0, next_key = 0;
    uint64_t            value[127] = {0};

    link = NULL;
    prog = NULL;
    obj  = NULL;
    // 读取 BPF 程序
    obj = bpf_object__open_file("./output/ebpf/ebpf_print.o", NULL);
    if (libbpf_get_error(obj)) {
        fprintf(stderr, "Error opening BPF object file.\n");
        return 1;
    }

    // 加载 BPF 对象到内核
    if (bpf_object__load(obj)) {
        fprintf(stderr, "Error loading BPF object file.\n");
        bpf_object__close(obj);
        return 1;
    }

    // 加载 uprobe 处理函数
    prog = bpf_object__find_program_by_title(obj, "uprobe/StackPrint");
    if (!prog) {
        fprintf(stderr, "Error finding uprobe program.\n");
        goto cleanup;
    }

    // dump BPF 程序
    printf("BPF program try to attach uprobe:\n");

    // 将 BPF 程序附加到 uprobe 点, 获取readline的返回值
    link = bpf_program__attach_uprobe(prog, true, -1, "./output/bin/anmk_ebpf_test", 0xa286);
    if (libbpf_get_error(link)) {
        fprintf(stderr, "Error attaching uprobe.\n");
        goto cleanup;
    }

    // 读取和处理 uprobe 事件
    map_fd = bpf_object__find_map_fd_by_name(obj, "stack_map");
    while (count < 100) {
        // 读取 bpf map数据
        sleep(1);
        printf("Read stack map data:\n");
        while (bpf_map_get_next_key(map_fd, &key, &next_key) == 0) {
            bpf_map_lookup_elem(map_fd, &next_key, &value);
            print_stack(value);
            bpf_map_delete_elem(map_fd, &next_key);
            key = next_key;
        }
        count++;
    }

cleanup:
    if (link) {
        bpf_link__destroy(link);
    }

    if (obj) {
        bpf_object__unload(obj);
        bpf_object__close(obj);
    }

    return 0;
}

这部分代码是一个用户空间程序,用于加载和运行前面提到的 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编译脚本如下所示:

# 查找Clang编译器和llvm-link工具, 用于eBPF编译
find_program(CLANG_EBPF_COMPILER clang)
if(NOT CLANG_EBPF_COMPILER)
    message(FATAL_ERROR "Clang compiler or llvm-link tool not found for eBPF compilation")
endif()

# 查找LLVM工具, link, opt, llc, objcopy
find_program(LLVM_LINK_TOOL llvm-link)
if (NOT LLVM_LINK_TOOL)
    message(FATAL_ERROR "LLVM link tool not found")
endif()
find_program(LLVM_OPT opt)
if (NOT LLVM_OPT)
    message(STATUS "LLVM opt tool not found")
endif()
find_program(LLVM_LLC llc)
if (NOT LLVM_LLC)
    message(STATUS "LLVM llc tool not found")
endif()
find_program(LLVM_OBJCOPY llvm-objcopy)
if (NOT LLVM_OBJCOPY)
    message(FATAL_ERROR "LLVM objcopy tool not found")
endif()

# 设置eBPF C文件
set(EBPF_SOURCES
    print.c
)

# 设置编译选项
set(EBPF_C_FLAGS
    -O2                     # 优化级别
    -m64                    # 64位
    -U __GNUC__             # 不包含__GNUC__宏定义
    -D__TARGET_ARCH_x86     # 定义__TARGET_ARCH_x86宏
    -D__x86_64__            # 定义__TARGET_ARCH_x86_64宏
    # -mstack-alignment=16  # 栈对齐16字节
    # -isystem /usr/include/x86_64-linux-gnu # 包含系统头文件目录
    -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
    -target bpf             # 目标平台
    -march=bpf              # 指定BPF指令集
    -Wall                   # 显示所有警告
    -Werror                 # 警告转为错误
    -Wno-unused-value       # 不显示未使用的值警告
    -fno-asynchronous-unwind-tables # 不生成异步取消表
    -fno-jump-tables        # 不生成跳转表
    -fno-stack-protector    # 不生成栈保护
    #-fno-builtin            # 不使用内建函数
    #-nostdinc               # 不包含标准头文件
)

# 设置eBPF IR文件
set(EBPF_BC_FILES)

# 编译eBPF IR文件
foreach(ebpf_file ${EBPF_SOURCES})
    # 成功编译的eBPF IR文件加入列表
    get_filename_component(ebpf_file_we ${ebpf_file} NAME_WE)
    execute_process(
        COMMAND ${CLANG_EBPF_COMPILER} ${EBPF_C_FLAGS} -emit-llvm -c ${ebpf_file} -o ${ebpf_file_we}.bc
        WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
        RESULT_VARIABLE CMD_RESULT
        COMMAND_ECHO STDOUT
    )
    if (NOT CMD_RESULT EQUAL 0)
        message(FATAL_ERROR "Failed to compile eBPF IR file ${ebpf_file}: ${CMD_RESULT}")
    endif()
    list(APPEND EBPF_BC_FILES ${ebpf_file_we}.bc)
endforeach()

# 如果没有eBPF IR文件, 则退出
if(NOT EBPF_BC_FILES)
    message(FATAL_ERROR "No eBPF IR files generated")
endif()

# 链接eBPF IR文件到一个目标文件
execute_process(
    COMMAND ${LLVM_LINK_TOOL} -o ebpf_combined.bc ${EBPF_BC_FILES}
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    COMMAND_ECHO STDOUT
)

# 优化eBPF IR文件
execute_process(
    COMMAND ${LLVM_OPT} -O2 -o ebpf_combined_opt.bc ebpf_combined.bc
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    COMMAND_ECHO STDOUT
)

# 生成eBPF字节码
execute_process(
    COMMAND ${LLVM_LLC} -march=bpf -filetype=obj ebpf_combined_opt.bc -o ebpf_program.o
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    COMMAND_ECHO STDOUT
)

# 安装eBPF字节码到指定目录
execute_process(
    COMMAND ${CMAKE_COMMAND} -E copy ebpf_program.o ${TOPDIR}/output/ebpf/ebpf_print.o
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    COMMAND_ECHO STDOUT
)

eBPF 程序的编译流程如下:

  1. 查找 Clang 编译器和 LLVM 工具链,Clang 编译器用于编译 eBPF 程序。LLVM 工具链中的 llvm-link、opt、llc 和 llvm-objcopy 工具,用于链接、优化和生成 eBPF 字节码。

  2. 设置 eBPF C 源文件和编译选项,指定 eBPF 程序的 C 源文件列表 (EBPF_SOURCES),以及设置 eBPF 程序的编译选项 (EBPF_C_FLAGS),包括优化级别、目标平台、包含路径等。

  3. 编译 eBPF C 源文件为 LLVM IR(中间表示),遍历 eBPF C 源文件列表,对每个文件执行以下步骤:

    • 使用 Clang 编译器将 C 源文件编译为 LLVM IR 文件 (.bc)。

    • 如果编译成功,将生成的 .bc 文件添加到 EBPF_BC_FILES 列表中。

    • 如果没有成功编译任何 eBPF C 源文件,则报错并退出。

  4. 链接 LLVM IR 文件,使用 llvm-link 工具将所有生成的 .bc 文件链接到一个名为 ebpf_combined.bc 的文件中。

  5. 优化 LLVM IR,使用 opt 工具对 ebpf_combined.bc 进行优化,生成优化后的 LLVM IR 文件 ebpf_combined_opt.bc。

  6. 生成 eBPF 字节码,使用 llc 工具将优化后的 LLVM IR 文件编译为 eBPF 字节码,生成目标文件 ebpf_program.o。

  7. 安装 eBPF 字节码,将生成的 eBPF 字节码文件 ebpf_program.o 复制到指定的输出目录 (${TOPDIR}/output/ebpf/ebpf_print.o)。

编译成功之后,会生成一个ebpf elf文件,如下所示:

ubuntu->ANMK_netdog:$ readelf -h output/ebpf/ebpf_print.o 
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Linux BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          688 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         9
  Section header string table index: 1
3.5 运行和输出

用户空间的程序使用Gcc正常编译即可,然后运行进行测试和验证:

ubuntu->ANMK_netdog:$ sudo ./output/bin/anmk_uprobe_print
libbpf: elf: skipping unrecognized data section(6) .rodata.str1.1
BPF program try to attach uprobe:
Read stack map data:
0x2a3cced16d90;0xb934d0717f;0x2a3ccef1e302;0x2a3ccef1dd54;0x2a3ccef1da7e;0xb934d0704a;0xb934d06d59;0xb934d06b68;
0x2a3cced16d90;0xb934d0717f;0x2a3ccef1e302;0x2a3ccef1dd54;0x2a3ccef1da7e;0xb934d068dc;0xb934d06751;
0x2a3cced16d90;0xb934d0717f;0x2a3ccef1e302;0x2a3ccef1dd54;0x2a3ccef1da7e;0xb934d0704a;0xb934d06d0f;0xb934d06b68;
......

可以看到,用户空间的程序正常将bpf map中的数据读取出来,但是没有转换为符号名称,因为这些地址都是虚拟地址,需要二次转换才能通过addr2line转换为符号地址。

/sys/kernel/debug/tracing/trace_pipe中,也可以读取到如下输出:

ubuntu->~:$ sudo cat /sys/kernel/debug/tracing/trace_pipe
  anmk_ebpf_test-1853091 [002] d...1 20451764.633240: bpf_trace_printk: Stack id: 15475
  anmk_ebpf_test-1853091 [002] d...1 20451764.633287: bpf_trace_printk: Stack id: 15475
  anmk_ebpf_test-1853091 [002] d...1 20451764.633320: bpf_trace_printk: Stack id: 15475
  anmk_ebpf_test-1853091 [002] d...1 20451764.633355: bpf_trace_printk: Stack id: 15475
  anmk_ebpf_test-1853091 [002] d...1 20451764.633394: bpf_trace_printk: Stack id: 15475
  .....
4. 总结

本文简单的根据uprobe文档实际操作了一波,见识到了uprobe的作用,但是离实际应用还有一段较大的距离,uprobe和ebpf这些工具使用,最大的阻碍在于内核的熟悉度,因为无法使用常见的标准库功能,比如堆栈打印和数据获取,这就必须从虚拟内存映射出发,在内核里面解析出对应的实际偏移量。

想要熟练的使用这些工具,必须深入学习Linux源码和相关的例子,门槛还是很高,道阻且长!







Alt

Once Day

也信美人终作土,不堪幽梦太匆匆......

如果这篇文章为您带来了帮助或启发,不妨点个赞👍和关注,再加上一个小小的收藏⭐!

(。◕‿◕。)感谢您的阅读与支持~~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2113238.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

心脑血管科张景龙医生:冠状动脉狭窄的症状与检查方法

冠状动脉狭窄作为一种常见的心血管疾病&#xff0c;其症状的出现往往与心肌供血不足密切相关。了解这些症状以及如何进行准确的检查&#xff0c;对于及早发现、诊断和治疗冠状动脉狭窄至关重要。本文将详细介绍冠状动脉狭窄的常见症状及检查方法。 冠状动脉狭窄的常见症状 1、…

电路笔记 控制(PID):Proportional–integral–derivative controller 比例-积分-微分控制器与仿真

PID控制&#xff08;Proportional-Integral-Derivative Control&#xff09;是一种常用的反馈控制算法&#xff0c;广泛应用于自动控制系统中。PID控制器通过对比例、积分和微分三项的计算&#xff0c;生成控制输出来调节系统的行为&#xff0c;以使其达到期望的目标值。 PID控…

利用人类反馈优化文本摘要质量

人工智能咨询培训老师叶梓 转载标明出处 精准评估和提升模型生成文本的质量&#xff0c;尤其是自动文摘的质量&#xff0c;成为了一个日益突出的挑战。传统的评估方法&#xff0c;如ROUGE指标&#xff0c;虽然在一定程度上能够衡量摘要的相关性&#xff0c;但往往无法全面反映…

Java算法:最大间距

前言 在处理数据密集型应用时&#xff0c;提高查询性能显得尤为关键。 解决最大间隔问题——即确定一组数值中最宽的相邻元素距离——是此类任务中的一大挑战。 该问题不仅在算法竞赛中常见&#xff0c;也是软件工程师面试的一个焦点&#xff0c;解决方法多样&#xff0c;包…

【B题第三套完整论文已出】2024数模国赛B题第三套完整论文+可运行代码参考(无偿分享)

基于多阶段优化的电子产品质量控制与成本管理研究 摘要 随着现代制造业和智能化生产的发展&#xff0c;质量控制和生产优化问题成为工业管理中的重要研究课题。本文针对电子产品生产过程中质量控制和成本优化中的问题&#xff0c;基于系统优化和决策分析思想&#xff0c;通过…

【C++ Primer Plus习题】12.1

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream> #include "Cow.h" u…

空气能热泵热水器

空气能热泵热水器压缩机把低温低压气态冷媒转换成高压高温气态&#xff0c;压缩机压缩功能转化的热量为q1&#xff0c;高温高压的气态冷媒与水进行热交换&#xff0c;高压的冷媒在常温下被冷却、冷凝为液态。这过程中&#xff0c;冷媒放出热量用来加热水&#xff0c;使水升温变…

机器学习数学公式推导之降维

文章目录 降维线性降维-主成分分析 PCA损失函数SVD 与 PCoASVD 的基本形式SVD 的计算p-PCA 小结 P22 (系列五) 降维1-背景 本文参考 B站UP: shuhuai008 &#x1f339;&#x1f339; 降维 我们知道&#xff0c;解决过拟合的问题除了正则化和添加数据之外&#xff0c;降维就是最…

数据链路层与ARP协议

一.认识识以太网 "以太网" 不是一种具体的网络, 而是一种技术标准; 既包含了数据链路层的内 容, 也包含了一些物理层的内容. 例如: 规定了网络拓扑结构, 访问控制方式, 传输速率等; 以太网中的网线必须使用双绞线; 传输速率有 10M, 100M, 1000M 等; 以太网是当前应用…

【最新华为OD机试E卷-支持在线评测】机器人活动区域(100分)多语言题解-(Python/C/JavaScript/Java/Cpp)

🍭 大家好这里是春秋招笔试突围 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-E/D卷的三语言AC题解 💻 ACM金牌🏅️团队| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,…

2024年【金属非金属矿山(露天矿山)安全管理人员】考试题及金属非金属矿山(露天矿山)安全管理人员最新解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 金属非金属矿山&#xff08;露天矿山&#xff09;安全管理人员考试题参考答案及金属非金属矿山&#xff08;露天矿山&#xff09;安全管理人员考试试题解析是安全生产模拟考试一点通题库老师及金属非金属矿山&#xf…

Java魔板游戏软件(含代码)

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

【第0007页 · 数组】数组中重复的数据(如何实现数组的原地修改)

【前言】本文以及之后的一些题解都会陆续整理到目录中&#xff0c;若想了解全部题解整理&#xff0c;请看这里&#xff1a; 第0007页 数组中重复的数据 今天&#xff0c;我们来看一个在实际工作中运用不多&#xff0c;但是对于一些算法题还是有必要的奇技淫巧——数组的原地修…

基于开源链动 2 + 1 模式、AI 智能名片与 S2B2C 商城小程序的用户忠诚度计划

摘要&#xff1a;本文深入探讨了在商业环境中执行用户忠诚度计划的创新途径。通过整合开源链动 2 1 模式、AI 智能名片以及 S2B2C 商城小程序等先进元素&#xff0c;从提供福利、解决问题和创造赚钱机会三个核心方面展开详细阐述。研究表明&#xff0c;这些新技术和新模式的有…

LLM大模型:将爬虫与大语言模型结合

摘要 Web自动化是一种重要技术&#xff0c;通过自动化常见的Web操作来完成复杂的Web任务&#xff0c;可以提高运营效率并减少手动操作的需要。 传统的实现方式&#xff0c;比如包装器&#xff0c;当面对新的网站时&#xff0c;面临着适应性和可扩展性的限制。 另一方面&…

国内短剧系统怎么搭建以及都需要那些资质?

聊到国内短剧&#xff0c;相信大家都不陌生&#xff0c;在各大短视频平台可谓是火的一批&#xff0c;您或许有想加入进来的想法&#xff0c;或是已经有规划还未实现的&#xff0c;请停下脚步&#xff0c;耐心看完该文章&#xff0c;相信一定会对你有所帮助的。本文介绍短剧平台…

C语言中结构体struct和联合体union的区别

C语言 文章目录 C语言前言一、什么是结构体二、什么是联合体三、结构体和联合体的区别 前言 一、什么是结构体 在C语言中&#xff0c;结构体指的是一种数据结构&#xff0c;是C语言中聚合数据类型的一类。结构体可以被声明为变量、指针或数组等&#xff0c;用以实现较复杂的数…

单调栈的实现

这是C算法基础-数据结构专栏的第二十四篇文章&#xff0c;专栏详情请见此处。 引入 单调栈就是满足单调性的栈结构&#xff0c;它最经典的应用就是给定一个序列&#xff0c;找出每个数左边离它最近的比它大/小的数。 下面我们就来讲单调栈的实现。 定义 单调栈就是满足单调性…

SnapGene 5.3.1下载安装教程百度网盘分享链接地址

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 SnapGene介绍 SnapGene 5.3.1下载安装教程百度网盘分享链接地址&#xff0c;SnapGene 是一款由美国公司开发&#xff08;后被收购&#xff09;的分子生物学软件&#xff0c;…

Deepspeed框架学习笔记

DeepSpeed 是由 Microsoft 开发的深度学习优化库,与PyTorch/TensorFlow等这种通用的深度学习框架不同的是,它是一个专门用于优化和加速大规模深度学习训练的工具,尤其是在处理大模型和分布式训练时表现出色。它不是一个独立的深度学习框架,而是依赖 PyTorch 等框架,扩展了…