int bpf(int cmd, union bpf_attr *attr, unsigned int size);
해당 함수는 ebpf map이나 program에 대한 명령을 수행하기 위한 친구이다.
- cmd: 말그대로 수행할 command
eBPF map과program을 조절하기 위한 많은 command들이 있다.
아래의 예시는 기본적으로 사용되는 load, create map, attach programs 등등이다.
attr: 말그대로 각종 속성들을 저장하는 곳인데 워낙 포함되는 데이터들이 많기에 kernel code를 통해 확인하는 것을 추천한다.
이제 본격적으로 bpf syscall을 파헤치기 위해 syscall을 tracing하는 strace를 활용하여 보자.
활용될 예시 프로그램은 아래와 같으며 이는 실행될때마다 perf buffer에 message를 보내고, execve() syscall event에 대한 정보를 userspace로 전달한다. chapter2인가...에서 있었던 프로그램과 유사하지만 각 사용자 ID에 대해 서로 다른 message를 구성할 수 있다는 차이점이 있다.
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from bcc import BPF
import ctypes as ct
program = r"""
struct user_msg_t {
char message[12];
};
BPF_HASH(config, u32, struct user_msg_t);
BPF_PERF_OUTPUT(output);
struct data_t {
int pid;
int uid;
char command[16];
char message[12];
};
int hello(void *ctx) {
struct data_t data = {};
struct user_msg_t *p;
char message[12] = "Hello World";
data.pid = bpf_get_current_pid_tgid() >> 32;
data.uid = bpf_get_current_uid_gid() & 0xFFFFFFFF;
bpf_get_current_comm(&data.command, sizeof(data.command));
p = config.lookup(&data.uid);
if (p != 0) {
bpf_probe_read_kernel(&data.message, sizeof(data.message), p->message);
} else {
bpf_probe_read_kernel(&data.message, sizeof(data.message), message);
}
output.perf_submit(ctx, &data, sizeof(data));
return 0;
}
"""
b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")
b["config"][ct.c_int(0)] = ct.create_string_buffer(b"Hey root!")
b["config"][ct.c_int(501)] = ct.create_string_buffer(b"Hi user 501!")
def print_event(cpu, data, size):
data = b["output"].event(data)
print(f"{data.pid} {data.uid} {data.command.decode()} {data.message.decode()}")
b["output"].open_perf_buffer(print_event)
while True:
b.perf_buffer_poll()
line 7~9: message출력할 배열 선언
line 11: data를 저장할 hash table map(type을 따로 지정하지 않을 시에 default는 u64)
line 32: helper function을 통해 userID를 받아온다. 매칭되는 userID에 따라 출력될 메세지를 결정한다.
pinning되지 않은 채로 ebpf program이 load되는 것을 허용한다면, bpftool이 종료될 때 fd가 해제되고 reference가 0이 될시에 program이 삭제될 것이다. 허나 pinng된 ebpf program의 경우 command가 수행된 이후에도 지속될 것이다.
reference counter는 trigger되는 hook이 연결될 때에도 증가한다. count는 eBPF program type에 의존적이며 tracing과 연관있고(chapter 7에서 다뤄질 예정) 항상 user space process와 연결된다. 이러한 경우, process가 종료되면 kernel의 참조 횟수가 감소한다.
netwrok stack이나 cgroup과 함께 부착된 program은 어떠한 user space process와 연관있지 않기에 load가 종료된 이후에도 유지된다. 이전에 봤던 XDP program에 대한 예시와 같다.
(아마 이전 챕터에서 실습 때, 따로 unload해주지 않으면 유지되어 있는 모습을 떠올리면 될 듯하다.)
ip link set dev eth0 xdp obj hello.bpf.o sec xdp
ip명령어가 수행된 이후 pinned location에 대한 정의가 없지만, bpftool은 XDP program이 kernel에 load된 상태를 보여준다.
$ bpftool prog list
…
1255: xdp name hello tag 9d0e949f89f1a82c gpl
loaded_at 2022-11-01T19:21:14+0000 uid 0
xlated 48B jited 108B memlock 4096B map_ids 612
reference count는 0이 아니며, 이는 XDP hook이 ip link명령어가 수행된 이후에 지속적으로 부착되어 있기 때문이다.
eBPF map역시 reference count를 가지며 0이 될 시에 초기화 된다. map역시 사용하는 각 eBPF program의 counter를 증가시켜 user space의 program이 map을 가지고 있게 해준다.
map은 file system에 고정될 수도 있으며, user space program은 map 경로를 알면 map에 접근할 수 있다.
위와 같이 fd와 reference counter를 통해 참조하게 할 수도 있지만 다른 방법으로는 eBPF link도 있다.
● BPF link
BPF link는 eBPF program과 event같의 추상화된 layer를 제공한다. BPF link는 자체적으로 file system에 고정되어 program에 대해 추가적인 참조를 생성할 수 있다. 이는 kernel에 load한 user space process가 종료되어도 program이 load된 상태로 남아 있을 수 있다는 의미라고 한다.
user space loader program의 fd가 해제되어도 program에 대한 reference count는 감소하지만, BPF link에 의해 0이 되지는 않는다.
이후의 내용들은 너무 deep해서 현시점의 나에겐 이해하기에 너무 어려워 생략한다.
대략적으로 다루는 내용은 perf buffers, ring buffers, kprobes, and map iterations에 관련된 syscall에 대해 자세히 본다.