* 如何添加新的事件

有三种事件可供开发者添加:

Add a tracepoint event

Step 1: Define ppm_event_type

File

driver/ppm_events_public.h

Description

ppm_event_type是具体的事件类型,通过enum对应,需要注意的是enum值需后面的g_event_info表中的index值对应,需要保持一致。 定义ppm_event_type沿用sysdig的定义一致性,需要定义事件入口和出口事件,如入口事件PPME_SOCK_INET_SOCK_SET_STATE_E = 318, PPME_SOCK_INET_SOCK_SET_STATE_X = 319,实际只是用了入口事件,添加后需要修改对应的PPM_EVENT_MAX值。 e.g.

PPME_SOCK_INET_SOCK_SET_STATE_E = 318,
PPME_SOCK_INET_SOCK_SET_STATE_X = 319,

Result

// ppm_events_public.h
enum ppm_event_type {
        PPME_SOCK_INET_SOCK_SET_STATE_E = 318,
        PPME_SOCK_INET_SOCK_SET_STATE_X = 319,
        PPM_EVENT_MAX = 320
}

Step 2: Define g_event_info

File

driver/event_table.h

Description

g_event_info是关于struct ppm_event_info的数组,添加相关事件到ppm_event_type对应的位置。 ppm_event_info格式如下,指定name,category,flags,如果有参数还需要定义参数ppm_param_info(参数名、参数类型、打印格式、info指针(如果是flags变量,指向一个ppm_name_value数组;如果是动态变量,指向一个ppm_param_info数组),还有一个ninfo记录数组长度。

struct ppm_event_info {
	char name[PPM_MAX_NAME_LEN]; /**< Name. */
	enum ppm_event_category category; /**< Event category, e.g. 'file', 'net', etc. */
	enum ppm_event_flags flags; /**< flags for this event. */
	uint32_t nparams; /**< Number of parameter in the params array. */
	struct ppm_param_info params[PPM_MAX_EVENT_PARAMS]; /**< parameters descriptions. */
} _packed;

Result

// event_table.c
const struct ppm_event_info g_event_info[PPM_EVENT_MAX] = {
				...,
        /* PPME_SOCK_INET_SOCK_SET_STATE_E */{"inetsockstate", EC_NET, EF_USES_FD, 2, {{"sport", PT_UINT16, PF_DEC}, {"dport", PT_UINT16, PF_DEC} } },
				/* PPME_SOCK_INET_SOCK_SET_STATE_X */{"NA7", EC_NET, EF_UNUSED, 0}
}

Step 3: Define filler

File

driver/ppm_fillers.h, driver/filler_table.c

Description

事件push到ring buffer前需要根据之前定义的事件填充参数,需要使用到filler。在ppm_fillers.h中添加FN(sock_inet_sock_set_state_e)定义,以及filler_table.c中的g_ppm_events注册填充函数,[PPME_SOCK_INET_SOCK_SET_STATE_E] = {FILLER_REF(sock_inet_sock_set_state_e)}。需要注意,g_ppm_events添加的位置需要和PPME_SOCK_INET_SOCK_SET_STATE_E的enum值相同。

Result

// ppm_fillers.h + 
#define FILLER_LIST_MAPPER(FN)          \
    FN(sys_linkat_x)            \
    FN(sock_inet_sock_set_state_e)       \ 
    FN(terminate_filler)          

// filler_table.c +
const struct ppm_event_entry g_ppm_events[PPM_EVENT_MAX] = {
    ... ,
    [PPME_SOCK_INET_SOCK_SET_STATE_E] = {FILLER_REF(sock_inet_sock_set_state_e)},
    [PPME_SOCK_INET_SOCK_SET_STATE_X] = {FILLER_REF(sys_empty)}
}

Step 4: Write filler

File

driver/ppm_fillers.h, driver/filler_table.c, driver/bpf/types.h

Description

定义fillers能够让bpf program在触发tracepoint后找到对应的filler进行调用,实际需要填充什么参数,如何填充需要编写对应的fillers函数。利用宏定义FILLER(sock_inet_sock_set_state_e, false),其中sock_inet_sock_set_state_e和上一步定义fillers的填充函数名相同,false表示这不是一个系统调用。 这个宏的展开定义了两个函数bpf_xxx是bpf program中的section,存储在tail_map中,被bpf probe调用(下一步),完成evt_hdr填充,调用__bpf_xxx完成参数填充,最后调用push_evt_frame推到ring buffer中。

__attribute__((section(("tracepoint/filler/sys_open_x")), used)) static __attribute__((always_inline)) intbpf_sock_inet_sock_set_state_e(void *ctx)
static __attribute__((always_inline)) int __bpf_sock_inet_sock_set_state_e(struct filler_data *data)

bpf_xxx是宏定义中通用的代码,下面的实际上是如何做参数填充,调用如bpf_val_to_ring等辅助函数。为了读取tracepoint返回的参数,需要定义相应的结构体,具体和/sys/kernel/debug/tracing/events/sock/inet_sock_set_state/format中对应。tracepoint返回的参数中可能会有结构体基地址,使用结构体时需要引入相关的内核头文件。

Result

// types.h + struct
// # cat /sys/kernel/debug/tracing/events/sock/inet_sock_set_state/format
struct sock_args {
    __u64 pad;
    const void *skaddr;
    int oldstate;
    int newstate;
    __u16 sport;
    __u16 dport;
    __u16 family;
    __u8 protocol;
    __u8 saddr[4];
    __u8 daddr[4];
    __u8 saddr_v6[16];
    __u8 daddr_v6[16];
};

// fillers.h +
#include<net/sock.h>
#include<linux/tcp.h>
FILLER(sock_inet_sock_set_state_e, false)
{
    struct sock_args *ctx;
    unsigned long val;
    int res;
    struct sock *sk;
    u16 sport, dport;
            
    ctx = (struct sock_args*) data->ctx;
    sk = (struct sock*) ctx->skaddr;
    // sport
    sport = ctx->sport;
    res = bpf_val_to_ring(data, sport)
    if (res != PPM_SUCCESS)
        return res;
    // dport
    dport = ctx->dport
    res = bpf_val_to_ring(data, dport)
    if (res != PPM_SUCCESS)
        return res;
    return 0;
}

Step 5: Write probe

File

driver/bpf/probe.c

Description

#ifdef BPF_SUPPORTS_RAW_TRACEPOINTS
#define BPF_PROBE(prefix, event, type)			\
__bpf_section(TP_NAME #event)				\
int bpf_##event(struct type *ctx)
#else
#define BPF_PROBE(prefix, event, type)			\
__bpf_section(TP_NAME prefix #event)			\
int bpf_##event(struct type *ctx)
#endif

__bpf_section用于标示这是一段bpf程序,能够加载,本身还是一个宏定义。举例:BPF_PROBE(“sock/”, inet_sock_set_state, sock_args),TP_NAME是"tracepoint/",prefix是"sock/",type是sock_args。宏定义展开是__bpf_section(“tracepoint/sock/inet_sock_set_state”) int bpf_inet_sock_set_state(struct sock_args *ctx)。 sock_args是需要自己定义的(在前一部分),具体使用是在filler中,部分判断逻辑也可以放在probe.c中用于提前返回,如判断protocol是否属于TCP。 在probe中,需要完成bpf setting检查等基本工作,设置call_filler的参数,包括evt_type, flag等,然后调用call_filler。call_filler是一个辅助函数,查找并调用对应evt_type的filler。

Result

// driver/bpf/probe.c
BPF_PROBE("sock/", inet_sock_set_state, sock_args)
{
    struct sysdig_bpf_settings *settings;
    enum ppm_event_type evt_type;
    settings = get_bpf_settings();
    if (!settings)
        return 0;
    if (!settings->capture_enabled)
        return 0;
  
    evt_type = PPME_SOCK_INET_SOCK_SET_STATE_E;
    
    call_filler(ctx, ctx, evt_type, settings, UF_NEVER_DROP);
}

Add a syscall event (based on tracepoint)

所有系统调用共用两个tracepoint探针:sys_enter和sys_exit,因此每个系统调用包括两个事件:入口事件和出口事件。系统调用的分类处理位于探针函数中,根据获取到的系统调用号调用不同的填充函数,从寄存器中获取对应的数据。不同的系统调用使用相同格式的参数时,可能复用相同的填充函数。 通常情况下,新增系统调用事件流程与新增非系统调用事件流程类似,但不需编写probe。在sysdig的系统调用probe中,可通过系统调用号查表调用对应的filler,所以只需要添加syscall_table中的表项。

Step 1: Define ppm_event_type

Result

// ppm_events_public.h
// 定义事件编号
enum ppm_event_type {
	PPME_SYSCALL_PRCTL_E = 320,
	PPME_SYSCALL_PRCTL_X = 321,
	PPM_EVENT_MAX = 322
}

Step 2: Define g_event_info

Result

// event_table.c
// 对应编号的位置定义事件参数列表
const struct ppm_event_info g_event_info[PPM_EVENT_MAX] = {
				...,
	/* PPME_SYSCALL_PRCTL_E */{"prctl", EC_PROCESS, EF_NONE, 2, {{"option", PT_INT32, PF_DEC}, {"proc_new_name", PT_CHARBUF, PF_NA} } },
	/* PPME_STSCALL_PRCTL_X */{"prctl", EC_PROCESS, EF_NONE, 1, {{"res", PT_ERRNO, PF_DEC} }}
}

Step 3: Add g_syscall_table

File

driver/syscall_table.c

Description

g_syscall_table是关于系统调用号及对应事件的数组,将系统调用号映射到对应的系统调用入口事件和出口事件。 结构体syscall_evt_pair示意如下:

struct syscall_evt_pair {
	int flags;
	enum ppm_event_type enter_event_type;
	enum ppm_event_type exit_event_type;
} _packed;

其中第一个参数为系统调用标志位,可控制是否丢弃事件,是否启用事件等:

enum syscall_flags {
	UF_NONE = 0,
	UF_USED = (1 << 0),
	UF_NEVER_DROP = (1 << 1),
	UF_ALWAYS_DROP = (1 << 2),
	UF_SIMPLEDRIVER_KEEP = (1 << 3),
	UF_ATOMIC = (1 << 4), ///< The handler should not block (interrupt context)
};

Result

// syscall_table.c
// 定义系统调用号对应的事件
const struct syscall_evt_pair g_syscall_table[SYSCALL_TABLE_SIZE] = {
    ...,
	[__NR_prctl - SYSCALL_TABLE_ID0] = 			{UF_USED, PPME_SYSCALL_PRCTL_E, PPME_SYSCALL_PRCTL_X},
}

Step 4: Define fillers

Result

// ppm_fillers.h + 
// 注意宏定义的\后不要加额外的空格
#define FILLER_LIST_MAPPER(FN)			\
        FN(sys_prctl_e)					\
        FN(sys_prctl_x)					\
        FN(terminate_filler)
          

// filler_table.c + 用于注册filler_table map
const struct ppm_event_entry g_ppm_events[PPM_EVENT_MAX] = {
	... ,
	[PPME_SYSCALL_PRCTL_E] = {FILLER_REF(sys_prctl_e)},
	[PPME_SYSCALL_PRCTL_X] = {FILLER_REF(sys_prctl_x)}
}

Step 5: Write filler

Result

// types.h +
// fillers.h中需要的结构体,一般系统调用只需读取参数,不需要额外定义结构体

// fillers.h +
FILLER(sys_prctl_e, true)
{
	int option;
	unsigned long arg;
	int res;

	option = bpf_syscall_get_argument(data, 0);
	res = bpf_val_to_ring(data, option);
	if (res != PPM_SUCCESS)
		return res;

	if (option == 15){
		arg = bpf_syscall_get_argument(data, 1);
		res = bpf_val_to_ring(data, arg);
		if (res != PPM_SUCCESS)
			return res;
	}

	return res;
}

FILLER(sys_prctl_x, true)
{
	int res;
	long retval;

	retval = bpf_syscall_get_retval(data->ctx);
	res = bpf_val_to_ring(data, retval);

	return res;
}

Add event based on Kprobe and Kretprobe

通过编写相应的BPF program,在指定的内核函数入口或出口处添加探针。

Step 1: Define ppm_event_type

File

driver/ppm_events_public.h

Desciption

ppm_event_type是具体的事件类型,通过enum对应,需要注意的是enum值需后面的g_event_info表中的index值对应,需要保持一致。 定义ppm_event_type沿用sysdig的定义一致性,kprobe对应入口事件,kretprobe对应出口事件,如出口事件PPME_DO_SWAP_PAGE_X = 321,添加后需要修改对应的PPM_EVENT_MAX值。

Result

// ppm_events_public.h
enum ppm_event_type {
    PPME_SOCK_INET_SOCK_SET_STATE_E = 320,
    PPME_DO_SWAP_PAGE_X = 321,
    PPM_EVENT_MAX = 322
}

Step 2: Define g_event_info

File

driver/event_table.c

Result

// event_table.c + 最后
const struct ppm_event_info g_event_info[PPM_EVENT_MAX] = {
	...,
	/* PPME_DO_SWAP_PAGE_X */{"swap", EC_OTHER, EF_NONE, 1,{{"vm_fault", PT_UINT32, PF_HEX} } }
}

Step 3: Write filler

File

driver/bpf/fillers.h, driver/bpf/types.h

Description

filler由probe调用,负责向事件中填充参数。kprobe与kretprobe对应的filler宏均为KP_FILLER(填充函数名),函数中所需的结构体同样需要引入定义。与tracepoint不同,kprobe与kretprobe中能使用的参数只有寄存器结构体pt_regs,入参和返回值需要通过_READ()宏从寄存器中读取。

Result

// fillers.h +
KP_FILLER(do_swap_page_x)
{
	int res;
	struct pt_regs *regs = (struct pt_regs *) data->ctx;
	unsigned int fault;

	fault = _READ(regs->ax);

	res = bpf_val_to_ring(data, fault);

	return res;
}

Step 4: Write probe

Description

  • kprobe通过BPF_KPROBE(event)宏进行声明,事件名为需要跟踪的内核函数名
  • kretprobe通过BPF_KRET_PROBE(event)宏进行声明,事件名同样为需要跟踪的内核函数名

函数中需要显式调用prepare_filler函数进行填充前预处理,然后显式调用上一节定义的filler函数bpf_填充函数名

Result

// probe.c + 参数:ctx
BPF_KRET_PROBE(do_swap_page)
{
	struct sysdig_bpf_settings *settings;
	enum ppm_event_type evt_type;
	settings = get_bpf_settings();
	if (!settings)
		return 0;
	if (!settings->capture_enabled)
		return 0;

	evt_type = PPME_DO_SWAP_PAGE_X;

	prepare_filler(ctx, ctx, evt_type, settings, UF_NEVER_DROP);
	bpf_do_swap_page_x(ctx);
	return 0;
}

Tips

  • 可使用bpf_printk()将调试日志输出,通过cat /sys/kernel/debug/tracing/trace_pipe查看输出。bpf_printk()是对bpf_trace_printk()的一层封装,启用需要在Makefile中加上-DBPF_DEBUG。