最新资讯

  • Linux之ebpf(3)uprobe与ebpf

Linux之ebpf(3)uprobe与ebpf

2025-05-01 13:00:54 0 阅读

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: 容器大小,通常为 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 '-:' >> /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 
#include 
#include 
#include 
#include 
#include 
#include 

// 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 
#include 
#include 
#include 
#include 
#include 

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("
");
}

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.
");
        return 1;
    }

    // 加载 BPF 对象到内核
    if (bpf_object__load(obj)) {
        fprintf(stderr, "Error loading BPF object file.
");
        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.
");
        goto cleanup;
    }

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

    // 将 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.
");
        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:
");
        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源码和相关的例子,门槛还是很高,道阻且长!







Once Day

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

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

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

本文地址:https://www.vps345.com/6764.html

搜索文章

Tags

PV计算 带宽计算 流量带宽 服务器带宽 上行带宽 上行速率 什么是上行带宽? CC攻击 攻击怎么办 流量攻击 DDOS攻击 服务器被攻击怎么办 源IP 服务器 linux 运维 游戏 云计算 javascript 前端 chrome edge llama 算法 opencv 自然语言处理 神经网络 语言模型 ssh 进程 操作系统 进程控制 Ubuntu 阿里云 网络 网络安全 网络协议 ubuntu deepseek Ollama 模型联网 API CherryStudio RTSP xop RTP RTSPServer 推流 视频 科技 ai java 人工智能 个人开发 python MCP android harmonyos typescript 鸿蒙 numpy 物联网 tcp/ip mcu iot 信息与通信 分布式 面试 性能优化 jdk intellij-idea 架构 运维开发 云原生 Flask FastAPI Waitress Gunicorn uWSGI Uvicorn 华为 开发语言 计算机网络 rust http 数据库 centos oracle 关系型 安全 宝塔面板访问不了 宝塔面板网站访问不了 宝塔面板怎么配置网站能访问 宝塔面板配置ip访问 宝塔面板配置域名访问教程 宝塔面板配置教程 vue.js audio vue音乐播放器 vue播放音频文件 Audio音频播放器自定义样式 播放暂停进度条音量调节快进快退 自定义audio覆盖默认样式 ssl fastapi mcp mcp-proxy mcp-inspector fastapi-mcp agent sse filezilla 无法连接服务器 连接被服务器拒绝 vsftpd 331/530 YOLO efficientVIT YOLOv8替换主干网络 TOLOv8 Windsurf pycharm 深度学习 conda pillow node.js json html5 firefox sqlserver rust腐蚀 AI 爬虫 数据集 protobuf 序列化和反序列化 安装 WSL win11 无法解析服务器的名称或地址 macos word图片自动上传 word一键转存 复制word图片 复制word图文 复制word公式 粘贴word图文 粘贴word公式 docker kubernetes 容器 学习方法 经验分享 程序人生 vSphere vCenter 软件定义数据中心 sddc ESP32 camera Arduino 电子信息 golang 后端 安装教程 GPU环境配置 Ubuntu22 CUDA PyTorch Anaconda安装 vim EtherCAT转Modbus ECT转Modbus协议 EtherCAT转485网关 ECT转Modbus串口网关 EtherCAT转485协议 ECT转Modbus网关 DeepSeek-R1 API接口 VMware安装mocOS VMware macOS系统安装 adb mysql 目标检测 live555 rtsp rtp websocket Hyper-V WinRM TrustedHosts MacOS录屏软件 电脑 大数据 政务 分布式系统 监控运维 Prometheus Grafana windows 微服务 springcloud vue3 HTML audio 控件组件 vue3 audio音乐播放器 Audio标签自定义样式默认 vue3播放音频文件音效音乐 自定义audio播放器样式 播放暂停调整声音大小下载文件 嵌入式 linux驱动开发 arm开发 嵌入式硬件 数据挖掘 网络用户购物行为分析可视化平台 大数据毕业设计 1024程序员节 spring boot tomcat github 创意 社区 源码剖析 rtsp实现步骤 流媒体开发 vscode c# ffmpeg 音视频 c++ 单片机 温湿度数据上传到服务器 Arduino HTTP cpu 内存 实时 使用 编辑器 鸿蒙系统 前端框架 ipython flutter 计算机外设 mac 软件需求 机器学习 react.js 前端面试题 持续部署 udp 客户端 java-ee 向日葵 YOLOv8 NPU Atlas800 A300I pro asi_bench 统信UOS 麒麟 bonding 链路聚合 mount挂载磁盘 wrong fs type LVM挂载磁盘 Centos7.9 ddos c语言 qt stm32项目 stm32 Agent zotero WebDAV 同步失败 代理模式 微信 微信分享 Image wxopensdk 计算机视觉 YOLOv12 flask AI编程 AIGC debian PVE 智能路由器 dell服务器 go ESXi cuda cudnn anaconda 华为云 unity .netcore ollama 大模型 学习 ide Dify oceanbase rc.local 开机自启 systemd HarmonyOS Next 智能手机 NAS Termux Samba Linux WSL2 命名管道 客户端与服务端通信 chatgpt llama3 Chatglm 开源大模型 php pip 开源 自动化 Dell R750XS postman mock mock server 模拟服务器 mock服务器 Postman内置变量 Postman随机数据 pytorch jmeter 软件测试 ai小智 语音助手 ai小智配网 ai小智教程 智能硬件 esp32语音助手 diy语音助手 gpu算力 笔记 C 环境变量 进程地址空间 sql KingBase uni-app .net 豆瓣 追剧助手 迅雷 nas 银河麒麟 kylin v10 麒麟 v10 aws googlecloud 僵尸进程 LDAP rpc asm HCIE 数通 maven intellij idea 外网访问 内网穿透 端口映射 GaN HEMT 氮化镓 单粒子烧毁 辐射损伤 辐照效应 nginx dubbo gateway Clion Nova ResharperC++引擎 Centos7 远程开发 命令 大数据平台 echarts 信息可视化 数据分析 网页设计 VMware安装Ubuntu Ubuntu安装k8s k8s etcd 数据安全 RBAC 媒体 腾讯云 硬件架构 金融 https AISphereButler spring cloud kafka hibernate 课程设计 shell kamailio sip VoIP 国产操作系统 ukui 麒麟kylinos openeuler k8s集群资源管理 云原生开发 统信 虚拟机安装 pyautogui 飞牛NAS 飞牛OS MacBook Pro 框架搭建 职场和发展 jar spring 串口服务器 DeepSeek Cline 自动化编程 Java Applet URL操作 服务器建立 Socket编程 网络文件读取 AI大模型 大模型入门 Deepseek 大模型教程 Ubuntu Server Ubuntu 22.04.5 remote-ssh 企业微信 Linux24.04 deepin gradle Reactor 设计模式 C++ 其他 远程工作 C# MQTTS 双向认证 emqx 微信小程序 小程序 自动驾驶 RustDesk自建服务器 rustdesk服务器 docker rustdesk AI写作 agi prompt django web3.py 缓存 强制清理 强制删除 mac废纸篓 云服务器 VPS microsoft 数据结构 KylinV10 麒麟操作系统 虚拟机 Vmware apache 孤岛惊魂4 WebRTC gpt selenium 测试工具 恒源云 LLM 大模型面经 大模型学习 okhttp 系统架构 RTMP 应用层 jupyter pygame gitlab 雨云 NPS Ark-TS语言 飞书 iftop 网络流量监控 iBMC UltraISO MQTT 消息队列 进程信号 adobe 传统数据库升级 银行 大语言模型 LLMs cmos 硬件 Python 网络编程 聊天服务器 套接字 TCP Socket redis webrtc IPMITOOL BMC 硬件管理 jenkins ci/cd devops opcua opcda KEPServer安装 oneapi 安卓 pdf web安全 中间件 iis 多线程服务器 Linux网络编程 云服务 可信计算技术 密码学 springsecurity6 oauth2 授权服务器 token sas FTP 服务器 Open WebUI visualstudio prometheus kylin 银河麒麟操作系统 国产化 RoboVLM 通用机器人策略 VLA设计哲学 vlm fot robot 视觉语言动作模型 具身智能 rsyslog elasticsearch rabbitmq 服务器数据恢复 数据恢复 存储数据恢复 raid5数据恢复 磁盘阵列数据恢复 Cookie ue4 着色器 ue5 虚幻 mamba Vmamba Trae IDE AI 原生集成开发环境 Trae AI 状态管理的 UDP 服务器 Arduino RTOS 驱动开发 硬件工程 嵌入式实习 开机自启动 产品经理 Portainer搭建 Portainer使用 Portainer使用详解 Portainer详解 Portainer portainer virtualenv DigitalOcean GPU服务器购买 GPU服务器哪里有 GPU服务器 微信小程序域名配置 微信小程序服务器域名 微信小程序合法域名 小程序配置业务域名 微信小程序需要域名吗 微信小程序添加域名 Kylin-Server 服务器安装 AP配网 AK配网 小程序AP配网和AK配网教程 WIFI设备配网小程序UDP开 EasyConnect Kali Linux 黑客 渗透测试 信息收集 zabbix 监控 ecmascript nextjs react reactjs 黑客技术 mariadb 流式接口 llm 本地部署 api pyqt ssrf 失效的访问控制 gpt-3 文心一言 openwrt 医疗APP开发 app开发 mybatis hadoop 压测 ECS yum xrdp 远程桌面 远程连接 string模拟实现 深拷贝 浅拷贝 经典的string类问题 三个swap 游戏服务器 TrinityCore 魔兽世界 开发环境 SSL证书 r语言 数据可视化 TRAE tcpdump 无人机 机器人 华为认证 网络工程师 拓扑图 Java NFS redhat 蓝桥杯 系统安全 kali 共享文件夹 群晖 文件分享 雨云服务器 能力提升 面试宝典 技术 IT信息化 环境迁移 asp.net大文件上传 asp.net大文件上传源码 ASP.NET断点续传 asp.net上传文件夹 asp.net上传大文件 .net core断点续传 .net mvc断点续传 远程控制 远程看看 远程协助 直播推流 docker搭建nacos详解 docker部署nacos docker安装nacos 腾讯云搭建nacos centos7搭建nacos springboot springboot远程调试 java项目远程debug docker远程debug java项目远程调试 springboot远程 毕设 ansible playbook 剧本 git gitea DevEco Studio 微信公众平台 fpga开发 npm C语言 灵办AI kvm svn wsl Redis Desktop 矩阵 三级等保 服务器审计日志备份 服务器管理 宝塔面板 配置教程 网站管理 RAID RAID技术 磁盘 存储 实时音视频 实时互动 Invalid Host allowedHosts vue 多进程 k8s资源监控 annotations自动化 自动化监控 监控service 监控jvm matlab log4j bootstrap html 博客 css excel 工业4.0 vr linux安装配置 微信开放平台 微信公众号配置 压力测试 express 并查集 leetcode matplotlib KVM wsl2 wireshark 多层架构 解耦 eureka safari Mac 系统 transformer nvidia 分析解读 thingsboard postgresql rag ragflow ragflow 源码启动 部署 ssh漏洞 ssh9.9p2 CVE-2025-23419 CPU 主板 电源 网卡 bash vmware 卡死 虚拟化 半虚拟化 硬件虚拟化 Hypervisor IIS .net core Hosting Bundle .NET Framework vs2022 AI代码编辑器 测试用例 功能测试 micropython esp32 mqtt 磁盘监控 服务器配置 openEuler list 模拟实现 小艺 Pura X 显示过滤器 ICMP Wireshark安装 visual studio code 生物信息学 Qwen2.5-coder 离线部署 rdp 实验 王者荣耀 Wi-Fi threejs 3D DNS SenseVoice 腾讯云大模型知识引擎 outlook 报错 云电竞 云电脑 todesk 负载均衡 firewalld 交换机 设备 GPU PCI-Express W5500 OLED u8g2 TCP服务器 Linux无人智慧超市 LInux多线程服务器 QT项目 LInux项目 单片机项目 UOS 统信操作系统 深度优先 图论 并集查找 换根法 树上倍增 JAVA SSH 网络结构图 linux 命令 sed 命令 ping++ 权限 SWAT 配置文件 服务管理 网络共享 华为od OD机试真题 华为OD机试真题 服务器能耗统计 gaussdb ruoyi 远程 执行 sshpass 操作 DeepSeek行业应用 Heroku 网站部署 低代码 dify unix nuxt3 ArcTS 登录 ArcUI GridItem arkUI yum源切换 更换国内yum源 QQ bot Docker 程序员 IIS服务器 IIS性能 日志监控 next.js 部署next.js ros2 moveit 机器人运动 ocr 银河麒麟服务器操作系统 系统激活 telnet 远程登录 n8n 工作流 workflow dba 算力 安全架构 网络攻击模型 windwos防火墙 defender防火墙 win防火墙白名单 防火墙白名单效果 防火墙只允许指定应用上网 防火墙允许指定上网其它禁止 实战案例 高效远程协作 TrustViewer体验 跨设备操作便利 智能远程控制 图像处理 IPMI mongodb 服务器繁忙 小游戏 五子棋 漏洞 kind 同步 备份 建站 安全威胁分析 openvpn server openvpn配置教程 centos安装openvpn grafana 蓝耘科技 元生代平台工作流 ComfyUI bug 弹性计算 裸金属服务器 弹性裸金属服务器 p2p unity3d 软件工程 Docker Hub docker pull 镜像源 daemon.json 网络穿透 Xterminal dns mysql离线安装 ubuntu22.04 mysql8.0 源码 毕业设计 Google pay Apple pay 混合开发 环境安装 JDK 服务器主板 AI芯片 致远OA OA服务器 服务器磁盘扩容 android studio 交互 CORS 跨域 游戏机 hive Hive环境搭建 hive3环境 Hive远程模式 gitee centos-root /dev/mapper yum clean all df -h / du -sh 京东云 arm 大模型微调 代码调试 ipdb 命令行 基础入门 编程 ollama下载加速 code-server mosquitto chrome 浏览器下载 chrome 下载安装 谷歌浏览器下载 视觉检测 VMware创建虚拟机 sqlite MS Materials openssl 自动化运维 信号处理 MySql 银河麒麟高级服务器 外接硬盘 Kylin 边缘计算 华为机试 信创 信创终端 中科方德 数据库系统 docker命令大全 Ubuntu DeepSeek DeepSeek Ubuntu DeepSeek 本地部署 DeepSeek 知识库 DeepSeek 私有化知识库 本地部署 DeepSeek DeepSeek 私有化部署 DocFlow sqlite3 seleium chromedriver 自动化测试 SSH 服务 SSH Server OpenSSH Server 目标跟踪 OpenVINO 推理应用 wordpress 无法访问wordpess后台 打开网站页面错乱 linux宝塔面板 wordpress更换服务器 RAGFLOW 游戏程序 cnn 邮件APP 免费软件 回显服务器 UDP的API使用 webstorm 做raid 装系统 怎么卸载MySQL MySQL怎么卸载干净 MySQL卸载重新安装教程 MySQL5.7卸载 Linux卸载MySQL8.0 如何卸载MySQL教程 MySQL卸载与安装 armbian u-boot 运维监控 fd 文件描述符 frp 内网服务器 内网代理 内网通信 VM搭建win2012 win2012应急响应靶机搭建 攻击者获取服务器权限 上传wakaung病毒 应急响应并溯源 挖矿病毒处置 应急响应综合性靶场 ios Ubuntu 24 常用命令 Ubuntu 24 Ubuntu vi 异常处理 ftp web 火绒安全 升级 CVE-2024-7347 uniapp web3 GoogLeNet lio-sam SLAM tcp vscode1.86 1.86版本 ssh远程连接 OpenManus xml big data opensearch helm 宝塔 deepseek r1 jvm IMM 反向代理 idm 软负载 单元测试 虚拟显示器 vue-i18n 国际化多语言 vue2中英文切换详细教程 如何动态加载i18n语言包 把语言json放到服务器调用 前端调用api获取语言配置文件 make命令 makefile文件 CLion Node-Red 编程工具 流编程 minio 单一职责原则 GCC aarch64 编译安装 HPC 数据库架构 数据管理 数据治理 数据编织 数据虚拟化 iphone open webui curl wget spark HistoryServer Spark YARN jobhistory ui asp.net大文件上传下载 树莓派 VNC VSCode 移动云 ros 鲲鹏 重启 排查 系统重启 日志 原因 健康医疗 zookeeper nfs 服务器部署ai模型 SSL 域名 技能大赛 RAGFlow Anolis nginx安装 linux插件下载 文件系统 路径解析 gcc 自定义客户端 SAS langchain deep learning 英语 3d GIS 遥感 WebGIS xcode 系统开发 binder 车载系统 framework 源码环境 c ROS 大大通 第三代半导体 碳化硅 Windows ai工具 v10 软件 ldap linux环境变量 技术共享 RAG 检索增强生成 文档解析 大模型垂直应用 虚拟局域网 多个客户端访问 IO多路复用 TCP相关API Linux PID h.264 CH340 串口驱动 CH341 uart 485 LInux URL MI300x SysBench 基准测试 ux 多线程 SSE open Euler dde LLM Web APP Streamlit 网卡的名称修改 eth0 ens33 显卡驱动 elk 大文件分片上传断点续传及进度条 如何批量上传超大文件并显示进度 axios大文件切片上传详细教 node服务器合并切片 vue3大文件上传报错提示错误 大文件秒传跨域报错cors bcompare Beyond Compare 网工 金仓数据库 2025 征文 数据库平替用金仓 Docker Compose docker compose docker-compose 搜索引擎 指令 cursor mq rocketmq 5G 3GPP 卫星通信 SRS 流媒体 直播 序列化反序列化 x64 SIGSEGV xmm0 Docker引擎已经停止 Docker无法使用 WSL进度一直是0 镜像加速地址 ip命令 新增网卡 新增IP 启动网卡 miniapp 真机调试 调试 debug 断点 网络API请求调试方法 Deepseek-R1 私有化部署 推理模型 人工智能生成内容 游戏引擎 Dell HPE 联想 浪潮 EMUI 回退 降级 崖山数据库 YashanDB python3.11 视频编解码 Ubuntu 24.04.1 轻量级服务器 dash 正则表达式 迁移指南 高级IO epoll openstack Xen docker run 数据卷挂载 交互模式 性能测试 线程 odoo 服务器动作 Server action 集成学习 集成测试 远程过程调用 Windows环境 HarmonyOS apt ceph QT 5.12.12 QT开发环境 Ubuntu18.04 高效日志打印 串口通信日志 服务器日志 系统状态监控日志 异常记录日志 设置代理 实用教程 相差8小时 UTC 时间 netty P2P HDLC 思科 uv 无桌面 相机 risc-v Claude eNSP 网络规划 VLAN 企业网络 swoole 程序 性能分析 AnythingLLM AnythingLLM安装 WebUI DeepSeek V3 FTP服务器 Kali 联想开天P90Z装win10 edge浏览器 匿名管道 wps qemu libvirt 软考 链表 计算机 图形化界面 换源 国内源 Debian rclone AList webdav fnOS tensorflow trae Xinference sublime text AD 域管理 网站搭建 serv00 宕机切换 服务器宕机 crosstool-ng ruby 执法记录仪 智能安全帽 smarteye rnn 大模型应用 ecm bpm 小智AI服务端 xiaozhi ASR TTS 昇腾 npu nac 802.1 portal Minecraft DOIT 四博智联 网络药理学 生信 gromacs 分子动力学模拟 MD 动力学模拟 linux上传下载 seatunnel USB网络共享 MCP server C/S Playwright windows日志 常用命令 文本命令 目录命令 上传视频至服务器代码 vue3批量上传多个视频并预览 如何实现将本地视频上传到网页 element plu视频上传 ant design vue vue3本地上传视频及预览移除 H3C iDRAC R720xd freebsd centos 7 服务器无法访问 ip地址无法访问 无法访问宝塔面板 宝塔面板打不开 监控k8s 监控kubernetes XFS xfs文件系统损坏 I_O error es 前后端分离 MacMini 迷你主机 mini Apple 用户缓冲区 uni-file-picker 拍摄从相册选择 uni.uploadFile H5上传图片 微信小程序上传图片 宠物 免费学习 宠物领养 宠物平台 深度求索 私域 知识库 pgpool glibc 端口测试 Jellyfin 田俊楠 FunASR file server http server web server 云桌面 微软 AD域控 证书服务器 个人博客 X11 Xming DBeaver 数据仓库 kerberos HiCar CarLife+ CarPlay QT RK3588 yolov8 jina CrewAI rime 我的世界 我的世界联机 数码 ISO镜像作为本地源 程序员创富 VR手套 数据手套 动捕手套 动捕数据手套 产测工具框架 IMX6ULL 管理框架 阻塞队列 生产者消费者模型 服务器崩坏原因 版本 jetty undertow grub 版本升级 扩容 av1 电视盒子 机顶盒ROM 魔百盒刷机 arcgis 磁盘镜像 服务器镜像 服务器实时复制 实时文件备份 数学建模 Erlang OTP gen_server 热代码交换 事务语义 MNN Qwen 监控k8s集群 集群内prometheus ip 备份SQL Server数据库 数据库备份 傲梅企业备份网络版 音乐服务器 Navidrome 音流 rustdesk 输入法 keepalived sonoma 自动更新 自动化任务管理 dns是什么 如何设置电脑dns dns应该如何设置 银河麒麟桌面操作系统 Kylin OS MQTT协议 消息服务器 代码 在线预览 xlsx xls文件 在浏览器直接打开解析xls表格 前端实现vue3打开excel 文件地址url或接口文档流二进 xshell termius iterm2 neo4j 数据库开发 database pppoe radius hugo Netty 即时通信 NIO AI agent 智能音箱 智能家居 思科模拟器 Cisco 北亚数据恢复 oracle数据恢复 selete AI作画 聊天室 chrome devtools 代理 Cursor 繁忙 解决办法 替代网站 汇总推荐 AI推理 Radius dity make qt项目 qt项目实战 qt教程 muduo XCC Lenovo 国标28181 视频监控 监控接入 语音广播 流程 SIP SDP IM即时通讯 剪切板对通 HTML FORMAT embedding 社交电子 SSH 密钥生成 SSH 公钥 私钥 生成 EMQX 通信协议 skynet 计算虚拟化 弹性裸金属 图形渲染 SEO 黑苹果 ShenTong chfs ubuntu 16.04 阿里云ECS 小番茄C盘清理 便捷易用C盘清理工具 小番茄C盘清理的优势尽显何处? 教你深度体验小番茄C盘清理 C盘变红?!不知所措? C盘瘦身后电脑会发生什么变化? LORA NLP 显示管理器 lightdm gdm sdkman sequoiaDB Linux的权限 vscode 1.86 Linux awk awk函数 awk结构 awk内置变量 awk参数 awk脚本 awk详解 直流充电桩 充电桩 远程服务 Nuxt.js alias unalias 别名 HTTP 服务器控制 ESP32 DeepSeek 备选 网站 调用 示例 AD域 DenseNet vasp安装 AI-native Docker Desktop AutoDL minicom 串口调试工具 私有化 交叉编译 业界资讯 tidb GLIBC 模拟退火算法 flash-attention 办公自动化 自动化生成 pdf教程 ubuntu24 vivado24 g++ g++13 flink Linux环境 CentOS 影刀 #影刀RPA# 移动魔百盒 企业网络规划 华为eNSP USB转串口 harmonyOS面试题 Ubuntu共享文件夹 共享目录 Linux共享文件夹 k8s二次开发 集群管理 lsb_release /etc/issue /proc/version uname -r 查看ubuntu版本 onlyoffice figma 推荐算法 单例模式 hosts autodl cd 目录切换 IDEA NLP模型 自学笔记 小米 澎湃OS Android 软件构建 PPI String Cytoscape CytoHubba lua sysctl.conf vm.nr_hugepages 网络建设与运维 域名服务 DHCP 符号链接 配置 端口 查看 ss Headless Linux 音乐库 飞牛 xpath定位元素 firewall 镜像 读写锁 AI Agent 字节智能运维 僵尸世界大战 游戏服务器搭建 强化学习 HarmonyOS NEXT 原生鸿蒙 计算生物学 生物信息 基因组 浏览器开发 AI浏览器 实习 知识图谱 less Attention easyui java-rocketmq etl 飞牛nas fnos 内网环境 本地部署AI大模型 项目部署到linux服务器 项目部署过程 架构与原理 banner DeepSeek r1 eclipse cpp-httplib react native 模拟器 教程 DIFY Unity Dedicated Server Host Client 无头主机 免费域名 域名解析 CentOS Stream gnu bat llama.cpp 嵌入式Linux IPC Doris搭建 docker搭建Doris Doris搭建过程 linux搭建Doris Doris搭建详细步骤 Doris部署 css3 本地知识库部署 DeepSeek R1 模型 sentinel 中兴光猫 换光猫 网络桥接 自己换光猫 区块链 midjourney ArkUI 多端开发 智慧分发 应用生态 鸿蒙OS 大模型部署 UOS1070e Python基础 Python教程 Python技巧 代码托管服务 nlp idea 策略模式 环境配置 佛山戴尔服务器维修 佛山三水服务器维修 基础环境 加解密 Yakit yaklang ubuntu20.04 开机黑屏 干货分享 黑客工具 密码爆破 元服务 应用上架 VS Code 沙盒 can 线程池 像素流送api 像素流送UE4 像素流送卡顿 像素流送并发支持 tailscale derp derper 中转 triton 模型分析 线性代数 电商平台 服务器时间 互信 Linux的基础指令 C++软件实战问题排查经验分享 0xfeeefeee 0xcdcdcdcd 动态库加载失败 程序启动失败 程序运行权限 标准用户权限与管理员权限 OpenSSH ssh远程登录 历史版本 下载 IPv4 子网掩码 公网IP 私有IP 防火墙 NAT转发 NAT Server 互联网医院 deekseek virtualbox 我的世界服务器搭建 minecraft perl 安防软件 Ubuntu22.04 开发人员主页 毕昇JDK iventoy VmWare OpenEuler IO模型 WSL2 上安装 Ubuntu WLAN c/c++ 串口 vpn 信号 yaml Ultralytics 可视化 rancher 支付 微信支付 开放平台 ArkTs PX4 Mac内存不够用怎么办 cocoapods 进程优先级 调度队列 进程切换 Spring Security perf WebVM rtsp服务器 rtsp server android rtsp服务 安卓rtsp服务器 移动端rtsp服务 大牛直播SDK 流水线 脚本式流水线 nftables IMX317 MIPI H265 VCU 游戏开发 增强现实 沉浸式体验 应用场景 技术实现 案例分析 AR TCP协议 渗透 composer 存储维护 NetApp存储 EMC存储 带外管理 TrueLicense Typore 超融合 开发 docker部署翻译组件 docker部署deepl docker搭建deepl java对接deepl 翻译组件使用 大模型推理 7z 虚幻引擎 Logstash 日志采集 su sudo xss 聚类 trea 服务网格 istio saltstack 分布式训练 语音识别 ubuntu24.04.1 fast CDN 键盘 powerpoint OpenHarmony 物联网开发 根服务器 clickhouse searxng 主从复制 嵌入式系统开发 语法 HAProxy 代理服务器 抗锯齿 junit 上传视频文件到服务器 uniApp本地上传视频并预览 uniapp移动端h5网页 uniapp微信小程序上传视频 uniapp app端视频上传 uniapp uview组件库 李心怡 捆绑 链接 谷歌浏览器 youtube google gmail laravel cfssl 虚拟现实 docker部署Python 需求分析 规格说明书 prometheus数据采集 prometheus数据模型 prometheus特点 双系统 GRUB引导 Linux技巧 linux内核 安卓模拟器 稳定性 看门狗 regedit 开机启动 软链接 硬链接 webgl 本地环回 bind 本地化部署 查询数据库服务IP地址 SQL Server 考研 dock 加速 SVN Server tortoise svn wpf db 玩机技巧 软件分享 软件图标 EtherNet/IP串口网关 EIP转RS485 EIP转Modbus EtherNet/IP网关协议 EIP转RS485网关 EIP串口服务器 容器技术 MDK 嵌入式开发工具 论文笔记 离线部署dify 搭建个人相关服务器 ebpf uprobe WebServer 国产数据库 瀚高数据库 数据迁移 下载安装 对比 工具 meld DiffMerge 服务器安全 网络安全策略 防御服务器攻击 安全威胁和解决方案 程序员博客保护 数据保护 安全最佳实践 mm-wiki搭建 linux搭建mm-wiki mm-wiki搭建与使用 mm-wiki使用 mm-wiki详解 端口聚合 windows11 达梦 DM8 宝塔面板无法访问 AI员工 System V共享内存 进程通信 欧标 OCPP 粘包问题 IO docker desktop image 服务器扩容没有扩容成功 deployment daemonset statefulset cronjob 视频平台 录像 视频转发 视频流 word navicat Qwen2.5-VL vllm fstab nosql top Linux top top命令详解 top命令重点 top常用参数 ros1 Noetic 20.04 apt 安装 钉钉 Web服务器 多线程下载工具 PYTHON 抓包工具 windows 服务器安装 win服务器架设 windows server yolov5 vnc notepad tar zip unzip 状态模式 隐藏文件 隐藏目录 管理器 通配符 visual studio 网络爬虫 ranger MySQL8.0 软件卸载 系统清理 多路转接 电视剧收视率分析与可视化平台 hexo 流量运营 磁盘清理 CPU 使用率 系统监控工具 linux 命令 java-rabbitmq kotlin Linux权限 权限命令 特殊权限 MAC SecureCRT iperf3 带宽测试 mcp服务器 client close Sealos 论文阅读 风扇控制软件 UEFI Legacy MBR GPT U盘安装操作系统 Unity插件 浏览器自动化 MacOS 网络搭建 神州数码 神州数码云平台 云平台 copilot ip协议 ELF加载 Linux 维护模式 ABAP UDP vu大文件秒传跨域报错cors 问题解决 milvus kernel fork wait waitpid exit MVS 海康威视相机 rpa MAVROS 四旋翼无人机 ubuntu 18.04 proxy模式 机柜 1U 2U 烟花代码 烟花 元旦 性能调优 安全代理 Linux find grep 在线office 合成模型 扩散模型 图像生成 内核 鸿蒙开发 移动开发 React Next.js 开源框架 export import save load 迁移镜像 wsgiref Web 服务器网关接口 云耀服务器 安装MySQL 通信工程 毕业 ardunio BLE docker搭建pg docker搭建pgsql pg授权 postgresql使用 postgresql搭建 极限编程 大模型技术 本地部署大模型 Reactor反应堆 conda配置 conda镜像源 powerbi 项目部署 话题通信 服务通信 lb 协议 开源软件 热榜 查看显卡进程 fuser ArtTS nvm whistle 智能电视 硅基流动 ChatBox K8S k8s管理系统 macOS 服务器正确解析请求体 接口优化 图片增强 增强数据 服务器部署 本地拉取打包 搜狗输入法 中文输入法 解决方案 MobaXterm Web应用服务器 WINCC Carla 智能驾驶 mysql安装报错 windows拒绝安装 samba 智慧农业 开源鸿蒙 团队开发 yum换源 fonts-noto-cjk 华为证书 HarmonyOS认证 华为证书考试 CosyVoice js 西门子PLC 通讯 jellyfin 网络文件系统 数字证书 签署证书