eBPF

EEEFFEE·2024년 1월 27일

eBPF

목록 보기
1/3

24.01.20 최초 작성

1. eBPF

  • Extended Berkeley Packet filter

  • 바이트 코드로 변환되어 리눅스 커널에 로드에 실행 가능할 수 있는 프로그램

  • 이벤트 발생 시점 등을 등록해 코드를 실행 가능함

  1. C언어로 eBPF 프로그램 작성

  2. LLVMeBPF를 바이트 코드로 변환

  3. 리눅스의 API를 통해 변환된 바이트 코드를 커널에 로드

  4. 프로그램이 특정 이벤트를 감지하면 프로그램 호출

1.1 LLVM

  • eBPF를 바이트 코드로 변환해주는 컴파일러 (C, C++, RUST)

  • eBPF > LLVM IR (최적화 수행) > 머신 코드

  1. 메모리 접근, 무한 루프 등 프로그램을 검증

  2. JIT 컴파일을 통해 바이트 코드 > 네이티브 코드로 변환

  3. 프로그램이 감지할 이벤트를 등록

1.2 관련 config

  • CONFIG_BPF, CONFIG_BPF_SYSCALL, CONFIG_DEBUG_INFO, CONFIG_IKHEADERS : BPF 기능 활성화, 시스템 콜 활성화, BTF 디버깅 정보

  • CONFIG_NET, CONFIG_BPF_STREAM_PARSER : 네트워크 관련

  • CONFIG_HAVE_BPF_JIT, CONFIG_BPF_JIT : JIT 컴파일관련

  • CONFIG_BPF_EVENTS : tracepoint 활성화

  • CONFIG_CGROUP_BPF : cgroup에 연결 가능

1.3 장점

  • 커널 공간에서 직접 실행되어 공유된 메모리에 데이터를 저장해 성능이 뛰어남

  • 사용자가 직접 프로그램 작성 가능

  • 다양한 분야에서 사용 가능

  • 컴파일 과정에서 검증을 거쳐 안전함

1.4 사용 시 주의사항

  • 커널을 잘 알고 있어야 함

  • trace point를 잘 잡아낼 수 있어야 함

  • 사용 시 오버헤드를 고려해야 함

1.5 tracepoint

  • system call을 추적할 수 있는 매크로
  • tracepoint는 커널 영역에 로드되고 나머지 프로그램은 사용자 영역에 로드되어 커널 영역에서 유저 영역으로 결과를 전달함

from bcc import BPF

prog = """ 												#tracepoint 구축
TRACEPOINT_PROBLE(syscalls, sys_enter_execve){			#execve 시스템 콜이 호출될 때 아래 코드 실행
	bpf_trace_printk("Hello, World!\\n");
    return 0;
}
"""

b = BPF(text=prog)								#해당 클래스를 tracepoint를 바이트코드로 변환해 메모리에 올려 줌

while True:
	try:
    	(_, _, _, _, _, msg) = b.trace_fields()
        print(msg)
    except KeyboardInterrupt:
    	break

2. BCC 주요 기능

  • BCC (BPF Compiler Collection) : BPF를 사용해 리눅스 커널에서 사용되는 프로그램을 개발/디버깅하는데 도움을 주는 도구
    (파이썬을 통해 프로그램을 구성하고 C언어를 통해 데이터를 수집하는 프로그램을 구현)

  • Kprobe, Kretprobe : 지정한 함수가 실행되기 전/후에 유저 레벨에 데이터 전송

from bcc import BPF

prog = """
#include <uapi/linux/ptrace.h>

struct data_t {
	u32 pid;
    char fname[256];
};

// 유저 레벨에 정보를 전송하는 매크로
BPF_PERF_OUTPUT(events);

// do_sys_open이 실행되기 전 kprobe를 통해 유저 공간에 전달 
// 뒤 인자들은 do_sys_open의 인자들
int kprobe__do_sys_open(struct pt_regs *ctx, int dfd, const char __user *filename, int flags) {
	struct data_t data = {};
    data.pid = bpf_get_current_pid_tgid();
    bpf_probe_read_user(&data.fname, sizeof(data.fname), (void*)filename);
    events.perf_submit(ctx, &data, sizeof(data));
    return 0;
};
"""

b = BPF(text=prog)

def print_event(cpu, data, size):
	event = b["events"].event(data)
    print("PID %d called open on file %s" % (event.pid, event.fname))
    
b["events"].open_perf_buffer(print_event)

while True:
	b.perf_buffer_poll()

  • USDT (User Statically Defined Tracing) : 동작 중인 프로그램의 특정 부분에 대한 정보를 얻을 수 있음

from bcc import BPF, USDT
from bcc.utils import printb

# load BPF program
bpf_text = """
#include <uapi/linux/ptrace.h>
int do_trace(struct pt_regs *ctx) {
    uint64_t addr;
    char query[128];
    /*
     * Read the first argument from the query-start probe, which is the query.
     * The format of this probe is:
     * query-start(query, connectionid, database, user, host)
     * see: https://dev.mysql.com/doc/refman/5.7/en/dba-dtrace-ref-query.html
     */
    bpf_usdt_readarg(1, ctx, &addr);
    bpf_probe_read_user(&query, sizeof(query), (void *)addr);
    bpf_trace_printk("%s\\n", query);
    return 0;
};
"""

  • Raw Trace Point : 리눅스에서 제공하는 직접적인 접근이 가능한 trace point

from bcc import BPF

bpf_text_raw_tp = """
RAW_TRACEPOINT_PROBE(nfs_initiate_commit)
{
    // TP_PROTO(const struct nfs_commit_data *data)
    struct nfs_commit_data *cd = (struct nfs_commit_data *)ctx->args[0];
    return trace_initiate_commit(cd);
}
"""

  • Systemcall tracepoint : 리눅스 커널에서 지원하는 동적 trace point

bpf_text = """
int syscall__execve(struct pt_regs *ctx,
    const char __user *filename,
    const char __user *const __user *__argv,
    const char __user *const __user *__envp)
{
    // create data here and pass to submit_arg to save stack space (#555)
    struct data_t data = {};
    struct task_struct *task;

    data.pid = bpf_get_current_pid_tgid() >> 32;

    task = (struct task_struct *)bpf_get_current_task();
    // Some kernels, like Ubuntu 4.13.0-generic, return 0
    // as the real_parent->tgid.
    // We use the get_ppid function as a fallback in those cases. (#1883)
    data.ppid = task->real_parent->tgid;

    bpf_get_current_comm(&data.comm, sizeof(data.comm));
    data.type = EVENT_ARG;

    __submit_arg(ctx, (void *)filename, &data);

    // skip first arg, as we submitted filename
    #pragma unroll
    for (int i = 1; i < MAXARG; i++) {
        if (submit_arg(ctx, (void *)&__argv[i], &data) == 0)
             goto out;
    }

    // handle truncated argument list
    char ellipsis[] = "...";
    __submit_arg(ctx, (void *)ellipsis, &data);
out:
    return 0;
}
"""

  • Kfuncs / Kretfuncs : 등록한 함수가 호출되었을 때 실행되는 동작

bpf_text_kfunc = """
KFUNC_PROBE(vfs_read)         { stats_try_increment(S_READ); return 0; }
KFUNC_PROBE(vfs_write)        { stats_try_increment(S_WRITE); return 0; }
KFUNC_PROBE(vfs_fsync_range)  { stats_try_increment(S_FSYNC); return 0; }
KFUNC_PROBE(vfs_open)         { stats_try_increment(S_OPEN); return 0; }
KFUNC_PROBE(vfs_create)       { stats_try_increment(S_CREATE); return 0; }
"""

  • LSM probes : 커널의 보안 관련 동작을 추적

from bcc import BPF, BPFAttachType, BPFProgType
from bcc.libbcc import lib

    @skipUnless(kernel_version_ge(5,7), "requires kernel >= 5.7")
    def test_lsm_probe(self):
        # Skip if the kernel is not compiled with CONFIG_BPF_LSM
        if not BPF.support_lsm():
            return
        b = BPF(text=b"""
LSM_PROBE(bpf, int cmd, union bpf_attr *uattr, unsigned int size) {
    return 0;
}""")

  • BPF Iterators : 연결된 자료구조를 순회하는 동작

BPF_ITER(task)
{
  struct seq_file *seq = ctx->meta->seq;
  struct task_struct *task = ctx->task;

  if (task == (void *)0)
    return 0;

  ... task->pid, task->tgid, task->comm, ...
  return 0;
}

0개의 댓글