本文章是前端时间读代码时的随手记录,没有做系统整理,估计也不会填坑了,大家随便看看就好。
1 主体结构
1.1 构建思路:
用户态代码:rust c,rust使用FFI(Foreign Function Interface)调用c,过程中使用 libc crate,它包含了 C 标准库中的类型别名和函数定义,编译时会静态连接libc。
1.2 构建结构:
build.rs
代码语言:make复制main()
|-- set_build_info() 这里还会检查git仓的信息
set_build_libtrace() 这里会执行src/ebpf下的make
set_linkage() 打印出链接信息
src/ebpf
代码语言:make复制|-- kernel 内核态ebpf程序
|-- user 用户态程序c实现
|-- mod.rs rust FFI对user封装的mod
src/ebpf/Makefile
代码语言:txt复制定义了函数compile_socket_trace_elf:构建socket_trace.elf,使用bintobuffer把字节码转成buffer放到一个.c文件(bintobuffer是这个项目自带的一个工具)
定义了函数compile_perf_profiler_elf:构建perf_profiler.elf,使用bintobuffer把字节码转成buffer放到一个.c文件
build:先编译内核态生成.elf,然后再编译用户态生成.a(.a会被rust静态链接)
src/ebpf/kernel/Makefile
代码语言:txt复制将socket_trace和perf_profiler编译成.elf(elf文件收敛到两个,其他.c被include到这两个文件里)
将编译后的文件反编译成.objdump
剥离掉对象文件中的调试信息
1.3 初始化流程
src/main.rs
代码语言:rust复制main()
|-- src/trident.rs
Trident::start()
-> Self::run()
-> Components::new()
-> AgentComponents::new()
|-- src/ebpf_dispatcher/ebpf_dispatcher.rs
EbpfCollector::new()
-> Self::ebpf_init()
|-- src/ebpf/mod.rs
bpf_tracer_init()
2 grpc接口
2.1 接口目录
- proto的目录在和agent同级的message目录下
- 生成的接口文件在agent/crates/public/src/proto下(其中telemetry是submodule,代码仓要git clone)
- 使用tonic进行build,build文件为agent/crates/public/build.rs
2.2 trident.proto
rpc
- Sync:向Server同步所在主机信息,Server会返回本机的容器列表
- Push:向Server同步所在主机信息,Server会持续应答(Stream,猜测用于实时变化)
- AnalyzerSync:和Sync的消息一样
- Upgrade:应答是Stream,返回一堆二进制报文
- Query:时钟同步
- GenesisSync:待研究
- KubernetesAPISync:k8 API信息,盲猜agent没用
- PrometheusAPISync:同上
3 用户态
3.1 src/ebpf/kernel/include/socket_trace_common.h
- pid这里是线程号,tgid是进程号,二者一致表示是单线程;
- coroutine_id是go的协程号
3.2 src/ebpf/kernel/socket_trace.c
- 这里定义了一个PERF_EVENT的map:socket_data,给用户态传数据
- 两个尾调跳转PROG_ARRAY的map:progs_jmp_kp_map(kprobe/uprobe),progs_jmp_tp_map(for tracepoint)
- Tracepoint 则更像是静态的,已经存在于内核中的 hook 点,不够灵活,但是相对固定,在不同版本的操作系统中变化不大,开销也更小;
- k retprobe 是动态追踪,我们可以在内核函数的开头和结尾进行追踪,相对更灵活;但是其开销也更大。
- infer_tcp_seq_offset:这个方法获取tcp_sock的copied_seq_offset,这里实现和操作系统有耦合,tcp_seq_offset对于不支持的btf的系统是通过推断得到的,对于支持btf的内核直接从btf文件读取得到的bpf_skc_to_tcp_sock() 这个辅助函数从5.9内核引入。
PROGTP(io_event)(void *ctx)
= SEC("prog/tp/io_event") int bpf_prog_tp__io_event(void *ctx)
{
获取tgid,pid
查询active_read_args_map
如果被标记跟踪:
{
trace_io_event_common(ctx, data_args, T_INGRESS, id)
{
从trace_conf_map获取一下配置
从trace_map中获取trace_info
从io_event_buffer(map)中获取内存块读取buffer内容
从data_buf(map)中获取存放socket_buffer的内存块v_buff(这个v_buff可能存多个__socket_data)
将socket_buffer放到v_buff后面
bpf_get_current_comm(用当前进程名填充socket_buffer的comm)
设置尾调上下文socket_buffer->data
触发尾调progs_jmp_tp_map(具体尾调函数看用户态代码)
}
删除跟踪
return // 读写跟踪只能开启一个
}
查询active_write_args_map
如果被标记跟踪:
{
trace_io_event_common(ctx, data_args, T_EGRESS, id)
删除跟踪
return // 读写跟踪只能开启一个
}
}
4 probe挂载
4.1 src/ebpf_dispatcher/ebpf_dispatcher.rs
代码语言:rust复制ebpf_init
|-- src/ebpf/mod.rs
running_socket_tracer
|-- src/ebpf/user/socket.c
running_socket_tracer
-> process_events_handle_main
-> process_probes_act
|-- src/ebpf/user/trace.c
tracer_hooks_attach/tracer_hooks_dettach
-> tracer_hooks_process
-> probe_attach
-> exec_attach_kprobe/exec_attach_uprobe
|-- src/ebpf/user/probe.c
program__attach_kprobe/program__attach_uprobe
-> program__attach_probe
-> bpf_attach_kprobe/bpf_attach_uprobe(bcc)
4.2 src/ebpf/user/socket.c
代码语言:c复制running_socket_tracer
bpf_bin_buffer指向ebpf字节码
buffer_sz字节码长度
|-- src/ebpf/user/trace.c
tracer_bpf_load
|-- src/ebpf/user/load.c
ebpf_open_buffer
|-- src/ebpf/user/elf.c
elf_info_collect
--> set_obj__version
--> set_obj__license
--> ebpf_obj__maps_collect
--> ebpf_btf_collect
--> ebpf_btf_ext_collect
ebpf_obj_load
--> bcc_create_map(bcc)
|-- src/ebpf/user/btf_vmlinux.c
ebpf_obj__load_vmlinux_btf
--> load_obj__progs
4.3 用户编写的 eBPF 程序最终会被编译成 eBPF 字节码
4.3.1 eBPF 字节码使用 bpf_insn 结构来表示,如下:
代码语言:c复制struct bpf_insn {
__u8 code; // 操作码
__u8 dst_reg:4; // 目标寄存器
__u8 src_reg:4; // 源寄存器
__s16 off; // 偏移量
__s32 imm; // 立即操作数
};
4.3.2 bpf_insn 结构各个字段的作用:
- code:指令操作码,如 mov、add 等。
- dst_reg:目标寄存器,用于指定要操作哪个寄存器。
- src_reg:源寄存器,用于指定数据来源于哪个寄存器。
- off:偏移量,用于指定某个结构体的成员。
- imm:立即操作数,当数据是一个常数时,直接在这里指定。 eBPF 程序会被 LLVM/Clang 编译成 bpf_insn 结构数组,当内核要执行 eBPF 字节码时,会调用 __bpf_prog_run() 函数来执行。 如果开启了 JIT(即时编译技术),内核会将 eBPF 字节码编译成本地机器码(Native Code)。这样就可以直接执行,而不需要虚拟机来执行。
4.4 agent是怎样拿到本机的socket句柄信息的?
socket 句柄是通过 hook 系统调用获取 (得到socket fd),socket相关信息是通过 get_socket_from_fd() 得到 socket address,相关 socket 信息(比如 元组信息,tcpseq number 等)是通过 socket 内核结构读取的.
5 tracepoint
syscalls::sys_enter_write
代码语言:c复制TPPROG(sys_enter_write)
先把系统调用写到map里
代码语言:c复制TPPROG(sys_exit_write)
从map里读出来处理
--> process_syscall_data
--> process_data
|-- src/ebpf/kernel/include/task_struct_utils.h
get_socket_from_fd 在这里判断是不是socket