eBPF program은 tracing messages를 debugging 목적으로 작성할 수 있다./sys/kernel/debug/tracing/trace_pipe에서 읽을 수 있는 trace_pipe를 통해 일반적으로 수행되는 간단한 예이다.그러나 몇가지 제약사항들이 있따. 3 arg가 max이며, trace_pipe는 전역적으로 공유(동시출력되는 프로그램은 충돌 발생가능)되어야한다. 그렇기에 productive eBPF code를 사용하면 안된다고 한다.
===============================
productive code가 무엇을 의미하는 바인지는 잘 모르겠다..글의 맥락상 trace_pipe를 활용하면 안되는 것 같다.혹여 비슷한 뜻일까 하여 production code의 의미를 찾아보니 프로그램 구현을 담당하는 부분으로 사용자가 실제로 사용하는 code를 의미한다고 한다.
===============================BPF_PERF_OUTPUT() 인터페이스를 통해 수행해야 한다.본 실습에서는 실플함을 위해 trace_pipe를 통해 이를 수행하고 opensnoop에 자체 메시지를 추가한다.
현재 머신에서는 opensnoop이 실행되고 있지 않다. 이제 bpftool을 활용하여 eBPF 프로그램들이 machine에서 동작하는 것을 관찰해보자.
우선 아래의 명령어를 실행한다.
bpftool prog list
출력 결과는 아래와 같다.
출력 결과에는 상기와 같이 cgroup_skb 및 cgroup_device의 두 가지 항목이 표시되어 있어야 한다. 둘다 Systemd의 구성요소인 systemd.resource-control에 의해 관리되며 네트워크장치 및 파일 시스템에 대한 Systemd 장치의 엑세스를 관리하는데에 사용된다고 한다.
그리고 현재는 tracepoint 종류의 항목이 없어야 한다.
이번엔 다른 터미널에서 아래의 명령을 수행한 이후에 다시 bpftool prog list를 원래의 터미널에서 실행해본다.
./opensnoop
출력 결과는 아까 처럼 82번까지 나오고, 이후의 출력 결과가 다르다.
4개의 BPF program이 적재된 것을 확인할 수 있다. 상기의 4개의 program은 이전의 tutorial 2에서 언급된 4가지 opensnoop BPF program에 해당된다. 이것은 모두 tracepoint유형이다. 그러나, 이름이 짤려 있어서 entry point와 exit point를 구별할 수 없다.
그리고 두세개의 숫자로 이뤄진 map_ids를 확인할 수 있다.(숫자는 실행마다 다를 수 있다.)
이제 기존에 실행해둔 ./opensnoop을 그대로 두고, 아래의 명령어를 수행한다.
bpftool map list
그리고 출력의 결과로 현재 존재하는 BPF map들을 볼 수 있다.
- 이름이 start인 hash table과 이름이 events인 perf 이벤트 배열을 확인할 수 있다.
그리고 이는 ~/bcc/libbpf-tools/opensnoop.bpf.c 의 line 13~24 의 source code에 정의 되어 있다.
// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2019 Facebook
// Copyright (c) 2020 Netflix
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include "opensnoop.h"
const volatile pid_t targ_pid = 0;
const volatile pid_t targ_tgid = 0;
const volatile uid_t targ_uid = 0;
const volatile bool targ_failed = false;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, u32);
__type(value, struct args_t);
} start SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
static __always_inline bool valid_uid(uid_t uid) {
return uid != INVALID_UID;
}
static __always_inline
bool trace_allowed(u32 tgid, u32 pid)
{
u32 uid;
/* filters */
if (targ_tgid && targ_tgid != tgid)
return false;
if (targ_pid && targ_pid != pid)
return false;
if (valid_uid(targ_uid)) {
uid = (u32)bpf_get_current_uid_gid();
if (targ_uid != uid) {
return false;
}
}
return true;
}
SEC("tracepoint/syscalls/sys_enter_open")
int tracepoint__syscalls__sys_enter_open(struct trace_event_raw_sys_enter* ctx)
{
u64 id = bpf_get_current_pid_tgid();
/* use kernel terminology here for tgid/pid: */
u32 tgid = id >> 32;
u32 pid = id;
/* store arg info for later lookup */
if (trace_allowed(tgid, pid)) {
struct args_t args = {};
args.fname = (const char *)ctx->args[0];
args.flags = (int)ctx->args[1];
bpf_map_update_elem(&start, &pid, &args, 0);
}
return 0;
}
SEC("tracepoint/syscalls/sys_enter_openat")
int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter* ctx)
{
u64 id = bpf_get_current_pid_tgid();
/* use kernel terminology here for tgid/pid: */
u32 tgid = id >> 32;
u32 pid = id;
/* store arg info for later lookup */
if (trace_allowed(tgid, pid)) {
struct args_t args = {};
args.fname = (const char *)ctx->args[1];
args.flags = (int)ctx->args[2];
bpf_map_update_elem(&start, &pid, &args, 0);
}
return 0;
}
static __always_inline
int trace_exit(struct trace_event_raw_sys_exit* ctx)
{
struct event event = {};
struct args_t *ap;
uintptr_t stack[3];
int ret;
u32 pid = bpf_get_current_pid_tgid();
ap = bpf_map_lookup_elem(&start, &pid);
if (!ap)
return 0; /* missed entry */
ret = ctx->ret;
if (targ_failed && ret >= 0)
goto cleanup; /* want failed only */
/* event data */
event.pid = bpf_get_current_pid_tgid() >> 32;
event.uid = bpf_get_current_uid_gid();
bpf_get_current_comm(&event.comm, sizeof(event.comm));
bpf_probe_read_user_str(&event.fname, sizeof(event.fname), ap->fname);
event.flags = ap->flags;
event.ret = ret;
bpf_get_stack(ctx, &stack, sizeof(stack),
BPF_F_USER_STACK);
/* Skip the first address that is usually the syscall it-self */
event.callers[0] = stack[1];
event.callers[1] = stack[2];
/* emit event */
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&event, sizeof(event));
cleanup:
bpf_map_delete_elem(&start, &pid);
return 0;
}
SEC("tracepoint/syscalls/sys_exit_open")
int tracepoint__syscalls__sys_exit_open(struct trace_event_raw_sys_exit* ctx)
{
return trace_exit(ctx);
}
SEC("tracepoint/syscalls/sys_exit_openat")
int tracepoint__syscalls__sys_exit_openat(struct trace_event_raw_sys_exit* ctx)
{
return trace_exit(ctx);
}
char LICENSE[] SEC("license") = "GPL";
- opensnoop 의 read only data를 위한 배열도 있다.(array name opensnoo.rodata)
-마지막으로 각 행의 시작에 있는 map ID가 이전의 bpftool prog list 에서 참조한 ID인지도 대응해보아야한다.
object file은 ELF형식 중 하나이다. ELF(Executable and Linkable Format)는 executable file, object code, shared library, core dump에 대한 공통 표준 파일 형식을 나타낸다. 또한 x86 processor의 binary file에 대한 표준 파일 형식이기도 하다.
● 중점적으로 봐야할 사항들은 아래와 같다.
- 현재 실습하는 machine은 Linux BPF이다. 하기에 이 binary code는 kernel 내의 BPF 가상머신 내에서 실행되는 것이다.
- 이 파일에는 BTF 정보가 포함되어 있다. BTF란, BPF program/map과 관련된 debug정보를 encoding하는 metadata 형식이다. 이 디버그 정보는 map pretty print나 fuction signatures 등에 사용된다.
- table중 .text section header 뒤에 tracepoint로 시작하는 4개의 실행 가능한 section header가 있다. 이들은 4개의 BPF program에 해당된다.
이제 4개의program들을 BPF source code에서 찾아볼 것이다.
두번째 탭에 있는 editor를 통해 opensnoop.bpf.c로 접근한다.
// SPDX-License-Identifier: GPL-2.0
// Copyright (c) 2019 Facebook
// Copyright (c) 2020 Netflix
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
#include "opensnoop.h"
const volatile pid_t targ_pid = 0;
const volatile pid_t targ_tgid = 0;
const volatile uid_t targ_uid = 0;
const volatile bool targ_failed = false;
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, u32);
__type(value, struct args_t);
} start SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
__uint(key_size, sizeof(u32));
__uint(value_size, sizeof(u32));
} events SEC(".maps");
static __always_inline bool valid_uid(uid_t uid) {
return uid != INVALID_UID;
}
static __always_inline
bool trace_allowed(u32 tgid, u32 pid)
{
u32 uid;
/* filters */
if (targ_tgid && targ_tgid != tgid)
return false;
if (targ_pid && targ_pid != pid)
return false;
if (valid_uid(targ_uid)) {
uid = (u32)bpf_get_current_uid_gid();
if (targ_uid != uid) {
return false;
}
}
return true;
}
SEC("tracepoint/syscalls/sys_enter_open")
int tracepoint__syscalls__sys_enter_open(struct trace_event_raw_sys_enter* ctx) //here
{
u64 id = bpf_get_current_pid_tgid();
/* use kernel terminology here for tgid/pid: */
u32 tgid = id >> 32;
u32 pid = id;
/* store arg info for later lookup */
if (trace_allowed(tgid, pid)) {
struct args_t args = {};
args.fname = (const char *)ctx->args[0];
args.flags = (int)ctx->args[1];
bpf_map_update_elem(&start, &pid, &args, 0);
}
return 0;
}
SEC("tracepoint/syscalls/sys_enter_openat")
int tracepoint__syscalls__sys_enter_openat(struct trace_event_raw_sys_enter* ctx) //here
{
u64 id = bpf_get_current_pid_tgid();
/* use kernel terminology here for tgid/pid: */
u32 tgid = id >> 32;
u32 pid = id;
/* store arg info for later lookup */
if (trace_allowed(tgid, pid)) {
struct args_t args = {};
args.fname = (const char *)ctx->args[1];
args.flags = (int)ctx->args[2];
bpf_map_update_elem(&start, &pid, &args, 0);
}
return 0;
}
static __always_inline
int trace_exit(struct trace_event_raw_sys_exit* ctx)
{
struct event event = {};
struct args_t *ap;
uintptr_t stack[3];
int ret;
u32 pid = bpf_get_current_pid_tgid();
ap = bpf_map_lookup_elem(&start, &pid);
if (!ap)
return 0; /* missed entry */
ret = ctx->ret;
if (targ_failed && ret >= 0)
goto cleanup; /* want failed only */
/* event data */
event.pid = bpf_get_current_pid_tgid() >> 32;
event.uid = bpf_get_current_uid_gid();
bpf_get_current_comm(&event.comm, sizeof(event.comm));
bpf_probe_read_user_str(&event.fname, sizeof(event.fname), ap->fname);
event.flags = ap->flags;
event.ret = ret;
bpf_get_stack(ctx, &stack, sizeof(stack),
BPF_F_USER_STACK);
/* Skip the first address that is usually the syscall it-self */
event.callers[0] = stack[1];
event.callers[1] = stack[2];
/* emit event */
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&event, sizeof(event));
cleanup:
bpf_map_delete_elem(&start, &pid);
return 0;
}
SEC("tracepoint/syscalls/sys_exit_open")
int tracepoint__syscalls__sys_exit_open(struct trace_event_raw_sys_exit* ctx) //here
{
return trace_exit(ctx);
}
SEC("tracepoint/syscalls/sys_exit_openat")
int tracepoint__syscalls__sys_exit_openat(struct trace_event_raw_sys_exit* ctx) //here
{
return trace_exit(ctx);
}
char LICENSE[] SEC("license") = "GPL";
상기의 code내에서 int tracepoint__syscalls ....로 시작하는 function들을 4개 찾을 수 있다. 해당되는 부분에 //here을 통해 표기하였다.
이들은 각각 readelf에 의해 나열된 실행 가능한 section에 해당하는 SEC() macro가 앞에 온다. 그리고 코드를 부착해야하는 eBPF hook를 정의한다SEC("tracepoint/<category>/<name>")). sys_enter_open과 sys_enter_openat에 대한 본 실습의 경우 eBPF code는 open 및 openat syscall이 수행될 때마다 호출되어야한다. Tracepoint들은 실행중인 kernel에서 코드를 부탁하는 데 사용할 수 있는 kernel code의 static marker이다. 이러한 tracepoint는 종종 퍼포먼스(성능)를 측정하기 위해 다양한 위치에 배치될 수 있다.
tracepoint__syscalls__sys_enter_open 및 tracepoint__syscalls__sys_enter_openat 함수는 open()/openat() syscall이 실행될 때마다 실행된다. 이후에 호출의 인자(파일 이름 등)를 parsing하고 이 정보를 BPF map에 기록한다. 기록된 곳에서 여기에서 우리의 compile된 opensnoop.c binary부분인 사용자 공간 프로그램(USP)이 이를 읽고 STDOUT으로 출력할 수 있다.
opensnoop : trace open() syscalls system-wide, and prints various details.
-> 리눅스 시스템에서 파일 오픈(open) 시스템 콜을 실시간으로 모니터링하는 도구!
eBPF는 아래와 같이 두가지 이상의 부분의 구성딘다.
● A User-space program (USP) that declared the kernel space program and attaches it to the relevant tracepoint/probe
● A kernel-spave program (KSP) is what gets triggerd and runs inside the kernel once the tracepoint/probe is met.
상기의 두 프로그램은 직접적으로 communication이 불가능하기에 buffer를 활용하여 data를 주고받습니다.
eBPF의 경우 다른 종류의 BPF map을 통해 구현되는 경우를 예시로 할 수 있다.
우선적으로 build를 수행한다.
make opensnoop
Compile된 opensnoop binary를 실행하기 위해서는 CAP_BPF 권한이 요구된다. 이는 우리가 수행하여는 로직(code)이 권한이 부여된 BPF작업( ex. eBPF code를 kernel에 load)을 수행하고 많은 Linux 배포판들이 eBPF를 허용하지 않기에 필수적이다. CAP_BPF는 Linux kernel 5.8이후부터 사용가능하며 모든 타입의 BPF program 적재, 대부분의 맵 유형 생성, BTF 적재, 프로그램 및 맵의 반복(아마 중복사용을 의미하는 것 같다)을 허용한다. 굳이 권한을 나눈 이유는 overload된 CAP_SYS_ADMIN기능으로부터 BPF의 기능을 독립하기 위해 도입되었다.
다시 console로 돌아가서 openscoop을 수행해보자.
./opensnoop
opensnoop은 file이 열릴때마다 출력을 표시할 것이다. 하지만 현재는 주어진 vm에서 진행되고 있으므로 관찰을 위해 event들을 생성할 것이다. 다른 terminal에서 아래의 명령을 수행한 이후, 다시 원래의 terminal로 돌아가서 다수의 file들이 cat을 수행하기 위해 접근된 것을 확인할 수 있다.
cat /etc/os-release
또한, 마지막 line(systemd)과 같이 vm에서 실행 중인 다른 process에서 생성된 출력 역시 확인가능하다.