一、BPF映射
eBPF 中实现内核态代码与用户态代码是可以实时通信的,这主要靠 BPF映射
来实现。
BPF映射
是内核空间的一段内存,以键值对
的方式存储。内核态程序可以直接访问 BPF 映射,用户态需要通过系统调用才能访问这段地址。
BPF 映射有很多种类型,如下表所示:
类型 | 说明 |
---|---|
BPF_HASH | 哈希表 |
BPF_ARRAY | 数组 |
BPF_HISTOGRAM | 直方图 |
BPF_STACK_TRACE | 跟踪栈 |
BPF_PERF_ARRAY | 硬件性能数组 |
BPF_PERCPU_HASH | 单CPU哈希表 |
BPF_PERCPU_ARRAY | 单CPU数组 |
BPF_LPM_TRIE | 最长前缀匹配映射 |
BPF_PROG_ARRAY | 尾调用程序数组 |
... | ... |
二、BPF_STACK_TRACE
跟踪栈
跟踪栈的用法:
-
BPF_STACK_TRACE(stack_traces, 128)
:定义一个跟踪栈,深度为 128。 -
stack_traces.get_stackid(ctx, 0)
:遍历通过 ctx 找到的堆栈,返回它的唯一 ID。 -
stack_traces = b.get_table("stack_traces")
:用户态获取跟踪栈。 -
for addr in stack_traces.walk(event.stack_id)
:根据跟踪栈的唯一 id 遍历栈内元素,函数调用地址信息。拿到地址信息后,通过b.ksym()
函数将其翻译为内核函数名。注意,b.ksym()
函数 接收一个show_offset
参数,用于控制是否显示偏移地址。 -
matched = b.num_open_kprobes()
:另外,这段程序最终接收一个参数,作为跟踪的内核函数名。因此需要判断其是否合法。num_open_kprobes()
将返回能够匹配上的内核探针数量,这里被应用于检测输入的内核函数是否合法。
三、stacksnoop源码解析
# stacksnoop.py
from __future__ import print_function
from bcc import BPF
import argparse
import time
#初始化命令项选项与参数解析
parser = argparse.ArgumentParser(
description="Trace and print kernel stack traces for a kernel function",
formatter_class=argparse.RawDescriptionHelpFormatter)
#跟踪函数参数
parser.add_argument("function", help="kernel function name")
#指定pid跟踪
parser.add_argument("-p", "--pid",help="trace this PID only")
args = parser.parse_args()
function = parser.parse_args().function
offset = False
# define BPF program
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
//定义了内核Probe程序与用户空间程序通信的结构体data_t
struct data_t {
u64 stack_id;
u32 pid;
char comm[TASK_COMM_LEN];
};
// 定义跟踪栈 BPF_STACK_TRACE(name, max_entries),创建名称为name的 stack trace map,并设置最大的entry条目。map用于存储stack栈的调用记录信息。
BPF_STACK_TRACE(stack_traces, 256);
//通过BBC宏定义内核中events变量
BPF_PERF_OUTPUT(events);
void trace_stack(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
FILTER
struct data_t data = {};
//int map.get_stackid(void *ctx, u64 flags) //在内核端,通过stack_traces.get_stackid(ctx,0)通过当前进程ctx上下文保存栈,获取当前进程的内核栈的栈编号,其中ctx是上下文信息,0的意义暂时未知;该工具将每个进程的stackid都传送到了用户端。
data.stack_id = stack_traces.get_stackid(ctx, 0);
data.pid = pid;
//bpf_get_current_comm(char *buf, int size_of_buf) 用当前进程名字填充buf参数地址。
bpf_get_current_comm(&data.comm, sizeof(data.comm));
//通过perf_submit方法将event数据发送至用户空间
events.perf_submit(ctx, &data, sizeof(data));
}
"""
#增加bpf pid过滤条件
if args.pid:
bpf_text = bpf_text.replace('FILTER',
'if (pid != %s) { return; }' % args.pid)
else:
bpf_text = bpf_text.replace('FILTER', '')
# initialize BPF
b = BPF(text=bpf_text)
#在eBPF用户态程序中,可以通过attach_kprobe函数将内核态eBPF程序通过kprobes机制附加到某个内核函数中。attach_kprobe 函数会创建一个 perf event,再将 eBPF 内核态程序附加到 perf event。每个 perf event 的 kprobe probe handler 都是 kprobe_dispatch 函数,他会去 perf event 中获取注册在当前 perf event 的回调函数列表并依次执行,同时将指向 perf ringbuffer 的指针的传递给 eBPF 程序,eBPF 程序可以通过 libbpf 封装好的 PT_REGS_PARAMx 宏定义来获取缓冲区中的数据。 (tracepoint 类型的 eBPF 程序需要定义好 tracepoint 关联的函数的参数的数据结构 /sys/kernel/tracing/events)
#该kprobe会执行自定义的trace_stack()函数。可以通过多次执行attach_kprobe() ,将自定义的函数附加到多个内核函数上。
b.attach_kprobe(event=function, fn_name="trace_stack")
# linux/sched.h
TASK_COMM_LEN = 16
# 判断输入的 function 是否合法
matched = b.num_open_kprobes()
if matched == 0:
print("Function \"%s\" not found. Exiting." % function)
exit()
# 获取跟踪栈,返回表对象。此函数已经淘汰,现在BFP可以将表作为items来读取,例如BFP[name].
stack_traces = b.get_table("stack_traces")
start_ts = time.time()
# header
print("%-18s %-12s %-6s %-3s %s" % ("TIME(s)", "COMM", "PID", "CPU", "FUNCTION"))
#用户空间Python程序则需要定义 print_event事件处理函数,并使用 perf_buffer_poll 函数轮训消费。
def print_event(cpu, data, size):
#读取出对应的数据并生成结构数据
event = b["events"].event(data)
ts = time.time() - start_ts
print("%-18.9f %-12.12s %-6d %-3d %s" % (ts, event.comm.decode('utf-8', 'replace'), event.pid, cpu, function))
#在用户端,通过stack_traces.walk(event.stack_id)获取了stackid对应的栈列表。
#根据 stack.id 遍历堆栈
for addr in stack_traces.walk(event.stack_id):
# BPF.ksym(addr) 将一个内核内存地址转成一个内核函数名字
sym = b.ksym(addr, show_offset=offset).decode('utf-8', 'replace')
print("\t%s" % sym)
print()
#从perf ring buffers等待数据,有数据会调用open_perf_buffer指定的回调函数。
b["events"].open_perf_buffer(print_event)
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
exit()
# python stacksnoop.py __kmalloc -p 1138
kprobe内核函数查找:
# bpftrace -l | grep kprobe | grep malloc
kprobe:__kmalloc
kprobe:__kmalloc_node
kprobe:__kmalloc_node_track_caller
kprobe:__kmalloc_reserve.isra.54
kprobe:__kmalloc_track_caller
kprobe:__kmem_vmalloc
kprobe:__vmalloc
kprobe:__vmalloc_node
kprobe:__vmalloc_node_flags_caller
kprobe:__vmalloc_node_range
kprobe:bpf_map_kmalloc_node
kprobe:debug_kmalloc
kprobe:dev_memalloc_noio
kprobe:devm_kmalloc
kprobe:devm_kmalloc_match
kprobe:devm_kmalloc_release
kprobe:drm_dp_mst_get_port_malloc
kprobe:drm_dp_mst_put_mstb_malloc
kprobe:drm_dp_mst_put_port_malloc
kprobe:drmm_kmalloc
kprobe:gfp_pfmemalloc_allowed
kprobe:is_vmalloc_or_module_addr
kprobe:kmalloc_fix_flags
kprobe:kmalloc_large_node
kprobe:kmalloc_order
kprobe:kmalloc_order_trace
kprobe:kmalloc_slab
kprobe:kvmalloc_node
kprobe:mempool_kmalloc
kprobe:obj_malloc.isra.34
kprobe:pm_runtime_set_memalloc_noio
kprobe:remap_vmalloc_range
kprobe:remap_vmalloc_range_partial
kprobe:sk_clear_memalloc
kprobe:sk_set_memalloc
kprobe:sock_kmalloc
kprobe:sock_omalloc
kprobe:sock_wmalloc
kprobe:vmalloc
kprobe:vmalloc_32
kprobe:vmalloc_32_user
kprobe:vmalloc_exec
kprobe:vmalloc_fault
kprobe:vmalloc_node
kprobe:vmalloc_sync_mappings
kprobe:vmalloc_sync_unmappings
kprobe:vmalloc_to_page
kprobe:vmalloc_to_pfn
kprobe:vmalloc_user
kprobe:vmalloc_user_node_flags
kprobe:zbud_zpool_malloc
kprobe:zpool_malloc
kprobe:zs_malloc
kprobe:zs_zpool_malloc