Split View: eBPF 완벽 가이드 — 커널 안의 작은 가상 머신: Verifier, JIT, CO-RE, Maps, Attach Points, XDP, LSM, sched_ext (2025)
eBPF 완벽 가이드 — 커널 안의 작은 가상 머신: Verifier, JIT, CO-RE, Maps, Attach Points, XDP, LSM, sched_ext (2025)
들어가며 — eBPF는 Linux의 새로운 신경계다
eBPF를 처음 접한 사람의 반응은 보통 "이게 진짜 가능해?"이다. 사용자 공간 프로그램이 커널 내부의 임의 지점에 작은 코드 조각을 끼워 넣고, 그 코드가 커널의 자료구조를 안전하게 읽고, 결과를 사용자 공간으로 돌려준다. 게다가 그 코드는 검증기를 통과해야만 실행되므로, 잘못된 코드가 커널을 무너뜨릴 수 없다.
이는 30년 전이라면 미친 소리로 들렸을 것이다. 그러나 2025년의 Linux는 정확히 이 모델 위에서 모니터링, 보안, 네트워킹, 그리고 심지어 스케줄링까지 확장하고 있다. Cilium은 eBPF로 쿠버네티스 네트워킹 전체를 다시 썼다. Falco와 Tetragon은 eBPF로 런타임 보안을 한다. Datadog의 시스템 메트릭 수집 에이전트는 eBPF에 의존한다. Linux 6.12에서는 eBPF가 스케줄러까지 확장되었다 (sched_ext).
이 글은 eBPF를 처음 들어보는 사람부터 이미 BCC 도구를 써본 사람까지 모두를 위한 것이다. 1992년 cBPF의 14줄짜리 ISA에서 시작해, 2014년 Alexei Starovoitov의 첫 eBPF 패치, 검증기의 마법, JIT 컴파일, CO-RE, 그리고 모던 Linux의 모든 어태치 포인트까지 1,400줄로 정리한다.
이 글은 Linux 내부 구조 시리즈와 자매 작품이다. 시리즈는 "커널이 무엇을 하는가"를 다뤘다면, 이 글은 "사용자가 커널을 어떻게 확장할 수 있는가"를 다룬다.
1. 역사 — cBPF에서 eBPF까지
1.1 1992 — Berkeley Packet Filter
1992년, Steven McCanne과 Van Jacobson은 BSD 운영체제용 새 패킷 필터링 메커니즘을 발표했다. 이전 패킷 필터(CSPF)는 트리 기반 표현식이었고 매우 느렸다. 그들의 새 모델은 단순한 가상 머신이었다:
- 32비트 누산기(
A)와 인덱스 레지스터(X) - 16개의 32비트 스크래치 메모리 슬롯
- 패킷에서 데이터를 읽거나 슬롯과 비교하는 단순한 명령어들
- 점프 명령어로 결정 트리 표현
이 모델은 BSD에서 큰 성공을 거두었고, 곧 Linux와 Solaris로 포팅되었다. tcpdump, libpcap이 이 위에서 동작한다. 표현식 tcp port 80은 내부적으로 약 20개의 cBPF 명령어로 컴파일된다.
cBPF는 30년 동안 실질적으로 변하지 않았다. 단순했고, 잘 동작했고, 패킷 필터링이라는 좁은 영역에서는 충분했다.
1.2 2013 — Alexei Starovoitov의 첫 패치
2013년, PLUMgrid에서 일하던 Alexei Starovoitov는 LKML에 큰 패치를 제출했다. 제목: "extended BPF". 핵심 변경:
- 32비트 → 64비트 레지스터
- 2개 → 11개 레지스터 (
R0-R10) - 함수 호출 명령어
- 더 많은 산술/비트 연산
- x86_64에 매우 가까운 ISA — JIT 컴파일이 거의 1:1
처음에는 회의적인 반응이 많았다. "왜 BPF를 확장하나? 그건 패킷 필터링용 아닌가?" 그러나 Alexei의 비전은 더 컸다 — 사용자 공간이 안전하게 커널 안에서 실행할 수 있는 작은 코드의 일반 인터페이스.
1.3 2014 — Linux 3.18 메인라인
2014년 9월, Linux 3.18에 첫 eBPF 패치가 머지되었다. 처음에는 socket filter 용도였다 (cBPF의 직접 후속). 그러나 곧 매 릴리스마다 새 어태치 포인트가 추가되었다:
- 2014 (3.18): 기본 인프라, socket filter
- 2015 (3.19): kprobe 어태치
- 2015 (4.1): tc (traffic control) 어태치
- 2016 (4.4): tracepoint 어태치
- 2016 (4.8): XDP 어태치
- 2017 (4.10): cgroup 어태치, perf_event 어태치
- 2018 (4.15): BTF 도입 (CO-RE의 토대)
- 2018 (4.18): socket lookup
- 2019 (5.7): LSM 어태치 (KRSI)
- 2020 (5.8): BPF_RINGBUF
- 2021 (5.13): 스택 트레이스 BPF helper
- 2022 (5.15): bpf_loop helper
- 2023 (6.4): BPF stack walking 향상
- 2024 (6.12): sched_ext 메인라인 머지
오늘의 eBPF는 거의 모든 커널 서브시스템과 통합되어 있다. 30년 전 패킷 필터링용 작은 가상 머신이 Linux의 일부 운영 모델 자체를 바꿔놓았다.
★ Insight ─────────────────────────────────────
- 이름의 혼란: "eBPF"는 공식 이름이 아니다. 커널 코드는 그냥 "BPF"라고 부른다. 외부에서는 "extended BPF"의 줄임말 "eBPF"가 더 일반적이다. 옛날 BPF는 "classic BPF" 또는 "cBPF"로 구별한다.
- 왜 "패킷 필터"가 일반 가상 머신이 되었나: 핵심 통찰은 "검증된 안전한 코드를 커널 안에서 실행한다"는 모델이 패킷 필터링뿐 아니라 거의 모든 커널 확장에 적용 가능하다는 것이었다. cBPF가 가졌던 "자체 종료 보장 + 메모리 안전" 속성을 더 풍부한 ISA로 가져온 것이 eBPF.
- Linux 외부에는 거의 없다: BSD 진영에는 일부 eBPF 포팅이 있지만 (DTrace를 가진 BSD는 eBPF가 덜 매력적), Windows는 2021년부터 ebpf-for-windows라는 별도 프로젝트가 있다. 그러나 "eBPF는 곧 Linux"라고 해도 거의 맞다.
─────────────────────────────────────────────────
2. eBPF 가상 머신 — ISA와 레지스터
2.1 11개의 64비트 레지스터
eBPF VM은 매우 단순하다:
R0: 함수 반환값, 프로그램 종료값R1~R5: 함수 인자 (1-5)R6~R9: 호출자 저장 (callee-saved)R10: 스택 프레임 포인터 (read-only)
이 인터페이스는 의도적으로 x86_64의 calling convention과 매우 비슷하다. JIT 컴파일이 1:1에 가까워진다.
2.2 스택
각 프로그램은 512바이트의 스택을 가진다. R10이 그 base를 가리킨다. 작아 보이지만 검증기가 모든 사용을 추적해야 하므로 작게 잡혀 있다.
2.3 명령어 인코딩
eBPF 명령어는 64비트 고정 크기:
+----+----+----+--------+
| op | dst | src | offset | imm |
| 8 | 4 | 4 | 16 | 32 |
+----+----+----+--------+
op: 8비트 opcodedst/src: 4비트 레지스터 인덱스offset: 16비트 부호 있는 오프셋 (점프, 메모리 액세스)imm: 32비트 즉치값
2.4 명령어 카테고리
핵심 카테고리:
- ALU: 산술/논리 연산 (32비트와 64비트)
- Memory: load/store
- Branch: 조건 점프
- Call: 헬퍼 함수 호출
- Exit: 프로그램 종료
예시:
BPF_MOV64_IMM(BPF_REG_0, 0) // R0 = 0
BPF_MOV64_REG(BPF_REG_1, BPF_REG_10) // R1 = R10 (frame pointer)
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, -8) // R1 += -8
BPF_LD_MAP_FD(BPF_REG_1, map_fd) // R1 = map fd
BPF_CALL_FUNC(BPF_FUNC_map_lookup_elem) // call helper
BPF_EXIT_INSN() // exit
2.5 200줄짜리 ISA
전체 eBPF ISA는 약 200줄의 C 코드로 표현 가능하다 (include/uapi/linux/bpf.h 참고). 매우 작다. 그러나 이 작은 ISA로 매우 풍부한 일을 할 수 있다.
3. eBPF 프로그램의 라이프사이클
3.1 작성 — C에서 BPF 바이트코드까지
전형적인 흐름:
// hello.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
SEC("kprobe/sys_open")
int hello(struct pt_regs *ctx) {
char fmt[] = "Hello from kprobe!\n";
bpf_trace_printk(fmt, sizeof(fmt));
return 0;
}
컴파일:
clang -O2 -target bpf -c hello.bpf.c -o hello.bpf.o
-target bpf가 핵심. clang의 BPF 백엔드가 eBPF 명령어로 컴파일한다. 결과는 ELF 파일이고, 그 안에 BPF 바이트코드가 들어 있다.
3.2 로드 — bpf() 시스템 콜
ELF에서 바이트코드를 추출해 bpf(BPF_PROG_LOAD, ...) 시스템 콜로 커널에 보낸다. libbpf가 이 과정을 캡슐화한다:
struct bpf_object *obj = bpf_object__open("hello.bpf.o");
bpf_object__load(obj); // 검증 + JIT
struct bpf_program *prog = bpf_object__find_program_by_name(obj, "hello");
bpf_program__attach(prog); // kprobe에 어태치
3.3 검증기
bpf(BPF_PROG_LOAD, ...)이 호출되면 커널은 먼저 **검증기(verifier)**를 돌린다. 검증기는 프로그램이 안전한지 검사한다:
- 모든 메모리 액세스가 유효한가
- 모든 점프가 정의된 위치로 가는가
- 무한 루프는 없는가
- 헬퍼 호출이 적절한 컨텍스트에서 이루어지는가
- 스택 사용량이 한도 안인가
검증기는 일반적으로 5단계로 진행된다 (다음 절에서 자세히).
3.4 JIT 컴파일
검증을 통과하면 JIT 컴파일러가 BPF 바이트코드를 네이티브 머신 코드로 컴파일한다. x86_64에서는 거의 1:1 매핑. ARM64에서도 비슷하다.
JIT는 옵션이지만 거의 모든 모던 시스템에서 켜져 있다 (net.core.bpf_jit_enable=1). 인터프리터보다 약 10-100배 빠르다.
3.5 어태치
JIT된 프로그램을 특정 어태치 포인트에 연결한다. 예를 들어 kprobe/sys_open이라는 SEC 이름은 sys_open 함수의 진입점에 kprobe를 걸겠다는 의미다. libbpf가 BPF_PROG_ATTACH 시스템 콜을 호출해 연결한다.
이 시점부터 sys_open이 호출될 때마다 BPF 프로그램이 실행된다.
4. 검증기 — eBPF의 진짜 마법
4.1 무엇을 보장하는가
검증기는 다음을 정적으로 보장한다:
- 메모리 안전성: 모든 load/store가 유효한 영역
- 타입 안전성: 포인터를 정수처럼 취급하지 않음
- 종료 보장: 무한 루프 없음 (또는 명시적 bound)
- 스택 안전성: 스택 오버플로우 없음
- 호출 안전성: 헬퍼 호출이 적절한 인자와 컨텍스트로 이루어짐
이 모든 것을 코드를 실행하지 않고 정적 분석으로 보장한다. 이는 매우 어려운 문제다.
4.2 어떻게 동작하나 — 추상 해석
검증기의 핵심 알고리즘은 **추상 해석(abstract interpretation)**이다. 모든 가능한 실행 경로를 시뮬레이션하면서, 각 시점의 레지스터/스택 상태를 "추상 값"으로 추적한다.
추상 값의 예:
R0 = SCALAR_VALUE, range [0, 100]R1 = PTR_TO_MAP_VALUE, off 0..16R2 = PTR_TO_PACKET, off 14..1500R3 = NOT_INIT
각 명령어를 처리하면서 이 추상 값을 갱신한다. 분기에서는 양쪽 경로를 모두 탐색.
4.3 path explosion 회피 — pruning
순진하게 모든 경로를 탐색하면 지수 폭발이 일어난다. 검증기는 이미 본 상태를 기억하고 (states_cache), 같은 상태에 재진입하면 그 경로를 가지치기한다.
이는 매우 효과적이지만, 복잡한 프로그램에서는 여전히 10초 이상 검증에 걸릴 수 있다. 검증 상태 수가 백만 개를 넘어가면 검증기는 포기한다 (-EFBIG 또는 -ENOSPC).
4.4 메모리 액세스 검증
int *p = bpf_map_lookup_elem(&my_map, &key);
*p = 42; // 검증 실패!
이 코드는 검증을 실패한다. bpf_map_lookup_elem은 NULL을 반환할 수 있는데, NULL 체크 없이 역참조하기 때문이다. 올바른 코드:
int *p = bpf_map_lookup_elem(&my_map, &key);
if (p) {
*p = 42; // OK
}
검증기는 if (p)를 보고 그 분기 안에서 p가 NULL이 아니라는 사실을 추적한다. 그래서 *p 액세스가 안전함을 알 수 있다.
4.5 종료 보장
루프는 검증기의 골칫거리다. eBPF는 처음에는 루프를 아예 금지했다. 모든 루프는 컴파일 타임에 unroll되어야 했다.
5.3부터 bounded loops가 허용되었다. 검증기가 루프 카운트가 유한함을 증명할 수 있으면 OK:
#pragma unroll
for (int i = 0; i < 10; i++) {
/* ... */
}
5.13부터는 bpf_loop 헬퍼가 추가되어, 더 유연한 루프가 가능해졌다:
static int callback(__u32 idx, void *data) {
/* ... */
return 0; // 0 = continue, 1 = stop
}
bpf_loop(1000, callback, &my_data, 0);
4.6 스택 사용량
각 프로그램은 512바이트 스택을 가진다. 큰 구조체를 스택에 두면 빠르게 한도를 넘는다.
SEC("kprobe/sys_open")
int hello(struct pt_regs *ctx) {
char buf[600]; // 검증 실패! 스택 한도 초과
return 0;
}
대안: BPF_MAP_TYPE_PERCPU_ARRAY를 "큰 스크래치 영역"으로 사용.
4.7 검증기 디버깅
검증기가 실패하면 매우 긴 에러 메시지가 나온다. bpftool prog show + verifier_log_level=2로 자세히 볼 수 있다. 한 줄 한 줄 명령어와 함께 검증기의 추상 상태가 출력된다:
0: (b7) r1 = 0
R1_w=0 R10=fp0
1: (61) r2 = *(u32 *)(r1 +0)
R1 invalid mem access 'inv'
processed 2 insns ...
이 로그를 읽는 능력이 eBPF 개발자의 핵심 스킬이다.
5. BPF Maps — 사용자 공간과의 통신
5.1 무엇이 필요한가
BPF 프로그램은 휘발적이다 — 호출이 끝나면 모든 로컬 상태가 사라진다. 데이터를 영속화하거나 사용자 공간과 공유하려면 별도의 메커니즘이 필요하다. 그것이 BPF Maps.
Maps는 키-값 저장소다. BPF 프로그램이 헬퍼 함수로 접근하고, 사용자 공간이 시스템 콜로 접근한다.
5.2 Map 종류 (17가지+)
핵심 종류:
| 종류 | 용도 |
|---|---|
BPF_MAP_TYPE_HASH | 해시 테이블 (가장 일반적) |
BPF_MAP_TYPE_ARRAY | 고정 크기 배열, 인덱스 기반 |
BPF_MAP_TYPE_PERCPU_HASH | CPU별 해시 (락 없음) |
BPF_MAP_TYPE_PERCPU_ARRAY | CPU별 배열 |
BPF_MAP_TYPE_LRU_HASH | LRU 회수가 있는 해시 |
BPF_MAP_TYPE_LPM_TRIE | Longest prefix match trie (라우팅용) |
BPF_MAP_TYPE_PROG_ARRAY | BPF 프로그램 배열 (tail call용) |
BPF_MAP_TYPE_PERF_EVENT_ARRAY | perf 이벤트 출력용 |
BPF_MAP_TYPE_RINGBUF | 새로운 ring buffer (5.8+) |
BPF_MAP_TYPE_QUEUE | FIFO |
BPF_MAP_TYPE_STACK | LIFO |
BPF_MAP_TYPE_SK_STORAGE | 소켓별 저장소 |
BPF_MAP_TYPE_TASK_STORAGE | 태스크별 저장소 |
BPF_MAP_TYPE_INODE_STORAGE | inode별 저장소 |
BPF_MAP_TYPE_CGROUP_STORAGE | cgroup별 저장소 |
BPF_MAP_TYPE_BLOOM_FILTER | 블룸 필터 |
BPF_MAP_TYPE_USER_RINGBUF | 사용자 → 커널 ringbuf (5.19+) |
5.3 Hash Map 예제
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, __u64);
} counter_map SEC(".maps");
SEC("kprobe/sys_open")
int count_opens(struct pt_regs *ctx) {
__u32 pid = bpf_get_current_pid_tgid() >> 32;
__u64 *count = bpf_map_lookup_elem(&counter_map, &pid);
if (count) {
__sync_fetch_and_add(count, 1);
} else {
__u64 init = 1;
bpf_map_update_elem(&counter_map, &pid, &init, BPF_ANY);
}
return 0;
}
이 프로그램은 매 sys_open 호출마다 PID별 카운터를 증가시킨다. 사용자 공간에서는 bpf_map__lookup_elem으로 같은 맵을 조회한다.
5.4 PERCPU 변형 — 락 없는 카운터
BPF_MAP_TYPE_PERCPU_HASH는 각 CPU별로 별도의 해시 테이블을 가진다. 락이 필요 없다 — 같은 CPU의 BPF 프로그램만 그 CPU의 맵에 접근하기 때문.
사용자 공간이 PERCPU 맵을 읽을 때는 모든 CPU의 값을 다 받아온다. 합치는 것은 사용자 공간의 책임.
PERCPU 맵은 카운터, 통계, 히스토그램에 매우 유용하다. 락 경쟁이 없어서 매우 빠르다.
5.5 RINGBUF — 새로운 이벤트 출력
전통적으로 BPF 프로그램이 사용자 공간으로 이벤트를 보낼 때는 BPF_MAP_TYPE_PERF_EVENT_ARRAY를 썼다. 이는 CPU별 perf 링 버퍼로, 각 CPU에 별도의 ring을 가진다.
5.8에서 도입된 BPF_MAP_TYPE_RINGBUF는 더 우아하다:
- 단일 공유 ring buffer (CPU 간 분배 없음)
- 더 적은 메모리
- BPF 프로그램이 가변 크기 이벤트를 직접 reserve/commit 가능
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("tp/sched/sched_process_exec")
int on_exec(struct trace_event_raw_sched_process_exec *ctx) {
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e) return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0);
return 0;
}
대부분의 새 BPF 도구는 ringbuf를 쓴다. perf event array는 호환성을 위해 남아 있다.
6. 헬퍼 함수 — 200개가 넘는 커널 인터페이스
6.1 헬퍼란
BPF 프로그램은 임의의 커널 함수를 호출할 수 없다. 대신 검증된 "헬퍼 함수" 집합만 호출 가능하다. 헬퍼는 커널이 명시적으로 노출한 안전한 인터페이스이다.
5.0 시점에 약 100개였던 헬퍼는 6.x 시점에 200개를 훨씬 넘는다.
6.2 핵심 헬퍼들
가장 자주 쓰는 것들:
| 헬퍼 | 용도 |
|---|---|
bpf_map_lookup_elem | 맵에서 값 읽기 |
bpf_map_update_elem | 맵에 값 쓰기 |
bpf_map_delete_elem | 맵에서 값 삭제 |
bpf_get_current_pid_tgid | 현재 PID/TID |
bpf_get_current_uid_gid | 현재 UID/GID |
bpf_get_current_comm | 현재 프로세스 이름 |
bpf_get_current_task | 현재 task_struct 포인터 |
bpf_ktime_get_ns | 단조 시간 (나노초) |
bpf_trace_printk | 디버그 printf (/sys/kernel/debug/tracing/trace_pipe) |
bpf_perf_event_output | perf event array에 이벤트 출력 |
bpf_ringbuf_reserve/bpf_ringbuf_submit | ringbuf 출력 |
bpf_get_stack/bpf_get_stackid | 스택 트레이스 |
bpf_probe_read_kernel/bpf_probe_read_user | 안전한 메모리 읽기 |
bpf_skb_load_bytes | 패킷에서 바이트 읽기 |
bpf_redirect | 패킷 리다이렉션 |
bpf_xdp_adjust_head | XDP 패킷 헤더 조정 |
bpf_jiffies64 | 현재 jiffies |
bpf_send_signal | 시그널 보내기 (5.3+) |
6.3 헬퍼의 안전성
각 헬퍼는 자기 인자 타입을 명시한다. 검증기가 호출 시점에 인자 타입을 검사한다.
예를 들어 bpf_map_lookup_elem의 시그니처:
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)
검증기는 map이 BPF_MAP_TYPE_HASH 같은 타입의 포인터인지, key가 그 맵의 key_size만큼의 메모리를 가리키는지 확인한다.
6.4 GPL과 비-GPL 헬퍼
일부 헬퍼는 GPL-only이다. 비-GPL 라이선스의 BPF 프로그램은 호출할 수 없다. bpf_trace_printk가 그렇다 (디버깅용이므로).
char LICENSE[] SEC("license") = "GPL"; // GPL helper 사용 가능
상업 BPF 도구의 대부분은 GPL이다.
7. Attach Points — 어디에 붙일 수 있나
eBPF의 진짜 힘은 다양한 어태치 포인트에서 나온다. 각 어태치 포인트는 자기만의 "context"(인자)와 헬퍼 집합을 가진다.
7.1 kprobe / kretprobe
거의 모든 커널 함수의 진입/리턴 지점에 어태치할 수 있다.
SEC("kprobe/vfs_read")
int on_vfs_read(struct pt_regs *ctx) {
/* ... */
return 0;
}
진입점에서는 pt_regs를 통해 인자에 접근. 리턴점에서는 PT_REGS_RC(ctx)로 반환값에 접근.
장점: 거의 모든 커널 함수에 붙을 수 있음. 단점: 함수 시그니처가 커널 버전마다 다를 수 있음. CO-RE로 일부 완화.
7.2 fentry / fexit (BPF Trampoline)
5.5에서 도입된 더 빠른 대안. kprobe는 INT3 명령어를 끼워 넣지만, fentry/fexit는 ftrace 인프라를 활용해 직접 호출한다. 약 10배 빠르다.
SEC("fentry/vfs_read")
int BPF_PROG(on_vfs_read_entry, struct file *file, char *buf, size_t count) {
/* 인자에 직접 접근 가능, BTF 덕분 */
return 0;
}
7.3 tracepoint
커널 코드에 미리 박힌 안정적인 추적 포인트. kprobe와 달리 함수 이름이 변해도 깨지지 않는다.
SEC("tp/sched/sched_process_exec")
int on_exec(struct trace_event_raw_sched_process_exec *ctx) {
/* tracepoint 인자에 직접 접근 */
return 0;
}
/sys/kernel/debug/tracing/events/에서 모든 사용 가능한 tracepoint를 볼 수 있다.
7.4 raw_tracepoint
tracepoint의 더 빠른 버전. tracepoint 인자를 디코딩하지 않고 그대로 받는다 — 약간의 코드 작성 부담이 있지만 더 빠르다.
7.5 uprobe / uretprobe
사용자 공간 함수에도 붙일 수 있다. 예: libc의 malloc에 어태치해서 모든 호출 추적.
SEC("uprobe//usr/lib/libc.so.6:malloc")
int on_malloc(struct pt_regs *ctx) {
size_t size = PT_REGS_PARM1(ctx);
/* ... */
return 0;
}
매우 강력하지만 비싸다 — 매 호출마다 사용자→커널 트랩이 일어난다.
7.6 perf_event
perf 이벤트 (CPU 사이클, 캐시 미스 등)에 어태치. CPU 프로파일링의 토대.
SEC("perf_event")
int sample(struct bpf_perf_event_data *ctx) {
/* 매 N 사이클마다 호출됨 */
return 0;
}
7.7 XDP — eXpress Data Path
네트워크 인터페이스의 수신 경로 가장 앞에 어태치. 패킷이 sk_buff로 변환되기 전, 드라이버 직후에 호출된다. 매우 빠르다.
SEC("xdp")
int xdp_prog(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
if (data + sizeof(struct ethhdr) > data_end)
return XDP_PASS;
struct ethhdr *eth = data;
if (eth->h_proto == bpf_htons(ETH_P_IP)) {
/* IP packet */
}
return XDP_PASS; // 또는 XDP_DROP, XDP_TX, XDP_REDIRECT
}
XDP 액션:
XDP_PASS: 일반 네트워크 스택으로XDP_DROP: 드롭XDP_TX: 같은 NIC로 다시 전송XDP_REDIRECT: 다른 NIC로 전송XDP_ABORTED: 에러
XDP는 DDoS 보호, 로드 밸런싱, 패킷 재작성에 매우 인기 있다. Cloudflare가 자기 인프라에 XDP를 광범위하게 사용한다.
7.8 tc (Traffic Control)
XDP보다 약간 늦은 단계에서 어태치. sk_buff가 이미 만들어졌으므로 더 풍부한 정보를 가진다 (cgroup, socket 등). XDP만큼 빠르지는 않지만 더 유연.
tc-bpf로 어태치:
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj my_prog.bpf.o
7.9 cgroup hooks
cgroup 안의 모든 프로세스의 특정 시스템 콜에 어태치 가능. 예를 들어 한 cgroup의 모든 connect 호출을 막는 등:
SEC("cgroup/connect4")
int restrict_connect(struct bpf_sock_addr *ctx) {
if (ctx->user_port == bpf_htons(22)) {
return 0; // SSH 차단
}
return 1;
}
컨테이너 보안 정책의 토대이다.
7.10 LSM hooks (KRSI)
5.7에서 도입. Linux Security Module의 모든 후크에 BPF 프로그램을 끼울 수 있다. SELinux/AppArmor를 BPF로 대체하거나 보강할 수 있다.
SEC("lsm/file_open")
int BPF_PROG(check_file_open, struct file *file, int ret) {
/* 파일 열기를 검사하고 거부할 수 있음 */
return -EPERM; // 또는 0 = OK
}
Tetragon이 이 모델 위에서 작동한다.
7.11 sched_ext (6.12+)
가장 새로운 어태치 포인트. 사용자 공간이 BPF로 스케줄링 정책을 작성할 수 있게 한다. Linux 스케줄러 글에서 자세히 다뤘다.
7.12 socket lookup, sock_ops, sk_msg
소켓 처리의 다양한 단계에 어태치할 수 있다. Cilium의 사이드카 없는 서비스 메시가 이를 활용한다.
8. CO-RE — Compile Once, Run Everywhere
8.1 문제
eBPF 프로그램은 종종 커널 자료구조 (task_struct, sk_buff 등)를 읽는다. 그러나 이 구조체의 레이아웃은 커널 버전, 컴파일 옵션마다 다르다. 빌드한 머신과 실행할 머신이 다르면 깨진다.
옛날 BCC는 이 문제를 "런타임에 컴파일"로 해결했다. 사용자 머신에 clang과 커널 헤더를 모두 설치하고, 매 실행마다 컴파일했다. 느렸고, 디스크 공간을 많이 먹었고, 운영에 부적합.
8.2 BTF — BPF Type Format
BTF는 커널 자료구조의 메타데이터를 임베드하는 경량 디버깅 정보 포맷이다. DWARF의 단순화 버전. 5.2부터 커널이 자기 BTF를 /sys/kernel/btf/vmlinux에 노출한다.
BTF는 모든 커널 구조체의 필드 이름과 오프셋을 포함한다. 사용자 공간 도구가 이를 읽으면 "이 커널에서 task_struct->mm은 어디에 있는가"를 알 수 있다.
8.3 CO-RE의 동작
CO-RE는 BTF를 기반으로 BPF 프로그램이 다른 커널 버전에서도 동작하게 한다.
#include <vmlinux.h> // 호스트 커널의 BTF에서 생성한 헤더
#include <bpf/bpf_core_read.h>
SEC("kprobe/sys_open")
int hello(struct pt_regs *ctx) {
struct task_struct *task = (void *)bpf_get_current_task();
pid_t pid = BPF_CORE_READ(task, pid); // 매크로 마법
/* ... */
return 0;
}
BPF_CORE_READ 매크로는 컴파일 타임에 "이 필드의 오프셋"을 직접 인라인하지 않는다. 대신 "필드 위치를 BTF로 lookup하라"는 재배치(relocation) 정보를 ELF에 남긴다.
런타임에 libbpf가 그 재배치를 처리한다 — 호스트 커널의 BTF를 보고 실제 오프셋을 채워 넣는다. 같은 BPF ELF가 5.10 커널과 6.5 커널에서 모두 동작한다.
8.4 vmlinux.h 생성
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
이 파일은 약 4MB이고 모든 커널 구조체를 정의한다. BPF 코드는 이를 include한다.
8.5 Field existence 검사
CO-RE의 또 다른 기능: 필드가 존재하는지 검사. 필드가 새로 추가/제거되었을 때 유연하게 대응.
if (bpf_core_field_exists(task->cgroups)) {
/* 이 필드가 있는 커널 */
} else {
/* 없는 커널 */
}
8.6 영향
CO-RE 덕분에 BPF 도구가 정말로 portable해졌다. Falco, Cilium, Tetragon, Pixie 같은 모던 도구는 모두 CO-RE를 쓴다. 한 번 빌드한 바이너리가 어떤 커널에서도 동작한다 (BTF가 있다는 전제 하에).
9. 사용자 공간 도구 — libbpf, BCC, bpftrace
9.1 BCC — 옛날 방식
BCC(BPF Compiler Collection)는 가장 오래된 BPF 도구셋이다. Python/Lua wrapper로 BPF 프로그램을 쉽게 짤 수 있게 해준다.
from bcc import BPF
prog = """
int hello(void *ctx) {
bpf_trace_printk("Hello!\\n");
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event="sys_open", fn_name="hello")
b.trace_print()
문제: 매 실행마다 컴파일. clang + 커널 헤더 필요. 큰 디스크 공간. 운영 환경에 부적합.
BCC는 여전히 많은 도구에서 쓰이고, 예제 보관소로서 가치가 크다 (/usr/share/bcc/tools/에 200개 이상의 도구가 있다).
9.2 libbpf — 모던 방식
libbpf는 C 라이브러리로, BPF 프로그램의 로드/어태치를 캡슐화한다. CO-RE를 지원한다.
#include <bpf/libbpf.h>
int main() {
struct bpf_object *obj = bpf_object__open_file("hello.bpf.o", NULL);
bpf_object__load(obj);
struct bpf_program *prog = bpf_object__find_program_by_name(obj, "hello");
bpf_program__attach(prog);
while (1) sleep(1);
return 0;
}
빌드 후 한 번만 배포하면 된다. Datadog, Cilium, Tetragon이 모두 libbpf 기반.
9.3 bpftrace — DSL
가장 빠른 입문 도구. awk와 비슷한 DSL로 BPF 프로그램을 한 줄로 짠다.
# 매 sys_open 호출 카운트
bpftrace -e 'kprobe:sys_open { @[comm] = count(); }'
# vfs_read의 latency 분포 (히스토그램)
bpftrace -e '
kprobe:vfs_read { @start[tid] = nsecs; }
kretprobe:vfs_read /@start[tid]/ {
@lat = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# 스택 트레이스가 포함된 페이지 폴트 추적
bpftrace -e 'tracepoint:exceptions:page_fault_user { @[ustack] = count(); }'
bpftrace는 내부적으로 libbpf를 쓴다. DSL을 BPF C 코드로 변환하고, clang으로 컴파일하고, libbpf로 로드한다.
9.4 어떤 도구를 쓸 것인가
- 즉석 진단: bpftrace
- 상용 도구 / 오랫동안 돌릴 에이전트: libbpf (C/Rust/Go)
- 참고용 / 기존 BCC 도구 활용: BCC
대부분의 새 BPF 코드는 libbpf로 이주하고 있다. BCC는 점점 "예제 보관소" 역할로 한정되고 있다.
10. 사례 1 — bpftrace 한 줄 진단
bpftrace의 강력함을 보여주는 실전 예제들:
10.1 어떤 프로세스가 디스크를 가장 많이 읽나
bpftrace -e '
tracepoint:block:block_rq_issue { @[comm] = sum(args->bytes); }
'
10.2 누가 시스템 콜을 가장 많이 호출하나
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
10.3 TCP retransmission 추적
bpftrace -e '
kprobe:tcp_retransmit_skb {
@[comm] = count();
}
'
10.4 어떤 함수가 가장 오래 걸리나
bpftrace -e '
kprobe:vfs_read { @start[tid] = nsecs; }
kretprobe:vfs_read /@start[tid]/ {
$duration = nsecs - @start[tid];
@hist = hist($duration / 1000);
delete(@start[tid]);
}'
10.5 OOM kill 시 전체 컨텍스트
bpftrace -e '
kprobe:oom_kill_process {
printf("OOM kill: comm=%s pid=%d ustack=%s\n",
comm, pid, ustack);
}'
각 한 줄이 정교한 도구가 했어야 할 일을 한다. 운영 디버깅의 새 기준점.
11. 사례 2 — XDP DDoS 방어
11.1 시나리오
UDP 플러드 공격을 받고 있다. 초당 수백만 개의 패킷이 들어오고 NIC가 마비되고 있다. 방어 코드는 패킷이 sk_buff로 변환되기 전에 작동해야 한다.
11.2 XDP 프로그램
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__type(key, struct bpf_lpm_trie_key);
__type(value, __u32);
__uint(max_entries, 1024);
__uint(map_flags, BPF_F_NO_PREALLOC);
} blacklist SEC(".maps");
SEC("xdp")
int xdp_drop(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end) return XDP_PASS;
/* IP를 LPM trie에서 lookup */
struct {
__u32 prefixlen;
__u32 addr;
} key = { .prefixlen = 32, .addr = ip->saddr };
if (bpf_map_lookup_elem(&blacklist, &key)) {
return XDP_DROP; // 블랙리스트면 즉시 드롭
}
return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";
11.3 어태치
ip link set dev eth0 xdpgeneric obj xdp_drop.bpf.o sec xdp
11.4 결과
이 프로그램은 패킷이 sk_buff로 변환되기 전에 드롭한다. 약 10배 빠르다 (24Mpps vs 2.5Mpps 같은 수치). Cloudflare가 자기 인프라에 매우 비슷한 패턴을 사용한다.
XDP가 드롭한 패킷은 시스템 metric에 거의 영향을 주지 않는다 — sk_buff alloc도 하지 않으므로 메모리도 안 쓰고 CPU도 거의 안 쓴다.
12. 사례 3 — Cilium의 쿠버네티스 네트워킹
12.1 비전
Cilium은 쿠버네티스의 네트워킹/보안/관찰성을 eBPF로 다시 쓰는 프로젝트다. iptables 기반의 kube-proxy를 완전히 대체한다.
12.2 무엇이 다른가
전통적인 쿠버네티스 네트워킹:
- iptables 규칙 수만 개 (서비스 수에 비례)
- 새 서비스마다 모든 노드에서 iptables 갱신
- 패킷 처리에 conntrack, NAT, 라우팅 모두 거침
- 큰 클러스터에서는 정말 느림
Cilium의 모델:
- BPF maps에 서비스/엔드포인트 정보 저장
- 패킷 처리는 BPF 프로그램이 직접 수행
- iptables 거의 필요 없음
- 큰 클러스터에서도 일관된 성능
12.3 사이드카 없는 서비스 메시
Cilium은 sock_ops와 sk_msg를 활용해 사이드카 프록시 없이 L7 통신을 가로챌 수 있다. 전통적인 Istio/Linkerd 모델은 매 파드에 Envoy를 사이드카로 띄우는데, 이는 메모리/CPU/지연 비용이 크다.
Cilium의 sidecarless 모델은 노드별로 한 개의 프록시 (또는 0개)만 띄우고, BPF로 트래픽을 그 프록시로 리다이렉트한다.
12.4 Tetragon — 보안
Cilium 팀이 만든 또 다른 도구. LSM 후크와 tracepoint를 활용해 모든 컨테이너 활동을 모니터링한다. "이 컨테이너가 /etc/passwd를 읽었다"를 실시간으로 알 수 있다.
전통적인 Falco와 비슷하지만 더 깊다. 정책 위반 시 시그널을 보내거나 즉시 차단할 수 있다.
★ Insight ─────────────────────────────────────
- eBPF가 만든 새로운 회사들: Isovalent (Cilium 회사, 2024년 Cisco가 인수), Polar Signals (continuous profiling), Pixie (관찰성), Groundcover, Levitate Security. 모두 eBPF가 가능하게 한 새 카테고리이다.
- iptables 시대의 종료: 쿠버네티스 인프라에서 iptables는 이제 레거시로 취급된다. nftables가 일부 대체했지만, 진짜 미래는 eBPF다. Cilium이 사실상 표준이 되어가고 있다.
- 사이드카 없는 메시의 의미: 쿠버네티스 클러스터에서 사이드카 프록시는 종종 노드 메모리의 30% 이상을 차지한다. eBPF로 대체하면 그 메모리가 해방된다. 이는 비용 측면에서 매우 큰 차이다.
─────────────────────────────────────────────────
13. 사례 4 — Falco 런타임 보안
Falco는 sysdig가 만든 런타임 보안 도구다. 컨테이너의 비정상 동작을 감지해 알람을 보낸다. 예: /etc/shadow를 읽는 컨테이너, root로 escalate 시도, suspicious shell spawn 등.
전통적으로는 sysdig 커널 모듈을 사용했지만, 최신 버전은 eBPF로 이전했다. 모든 시스템 콜을 가로채고 규칙 엔진에 보낸다.
# Falco 규칙 예시
- rule: Read sensitive file
desc: An attempt to read sensitive file
condition: open_read and sensitive_files
output: Sensitive file opened (user=%user.name file=%fd.name)
priority: WARNING
eBPF 덕분에 커널 모듈이 필요 없고, 어떤 커널 버전에서도 동작한다.
14. 사례 5 — bpftune 자동 튜닝
Oracle이 만든 도구. eBPF로 시스템 메트릭을 모니터링하고, 자동으로 sysctl 값을 조정한다. 예:
- TCP 연결이 자주 timeout →
tcp_keepalive_time줄임 - 메모리 압박 자주 발생 →
vm.swappiness조정 - 디스크 IO bottleneck → readahead 크기 늘림
전통적으로 시스템 튜닝은 사람이 수동으로 했다. bpftune은 이를 데이터 기반으로 자동화한다. eBPF가 없었다면 매 메트릭마다 별도의 도구를 띄워야 했을 것이다.
15. 보안 — eBPF의 위험
eBPF는 강력한 만큼 잘못된 손에 들어가면 위험하다.
15.1 BPF 권한
BPF 프로그램 로드는 보통 CAP_BPF (5.8+)와 CAP_PERF_MON 또는 CAP_NET_ADMIN이 필요하다. 이전에는 CAP_SYS_ADMIN (root 권한과 거의 동일)이 필요했다.
15.2 검증기 우회
검증기는 정적 분석이고, 100% 완벽하지 않다. 과거 몇 차례의 CVE가 검증기를 속여 임의 메모리 읽기/쓰기를 가능하게 했다:
- CVE-2022-23222: BPF pointer arithmetic 검증 결함
- CVE-2021-45402: 32비트 분기 검증 결함
- CVE-2021-3490: ALU32 boundary tracking 결함
이런 결함은 발견될 때마다 빠르게 수정되지만, 검증기가 점점 복잡해지면서 새 결함의 가능성도 늘어난다.
15.3 unprivileged_bpf_disabled
Linux는 일반 사용자의 BPF 사용을 기본적으로 막을 수 있다:
echo 1 > /proc/sys/kernel/unprivileged_bpf_disabled
대부분의 배포판이 이를 기본으로 켠다. 일반 사용자는 BPF 프로그램을 로드할 수 없다.
15.4 BPF LSM과 BPF 자체 보호
BPF LSM이 있다는 것은 BPF로 BPF를 제어할 수 있다는 의미이다. "이 cgroup의 BPF 프로그램 로드는 거부한다" 같은 정책을 BPF로 표현할 수 있다.
15.5 사이드 채널
BPF 프로그램은 어떤 권한 정보도 노출할 수 있다. Spectre 같은 사이드 채널 공격이 BPF로 가능하다는 연구가 있다. 검증기는 일부 우려스러운 패턴을 거부하지만, 완벽한 방어는 어렵다.
16. 디버깅 — bpftool
bpftool은 BPF 인프라의 만능 도구이다.
16.1 로드된 프로그램 보기
bpftool prog list
1: kprobe name hello tag a1b2c3d4e5f60718
loaded_at 2026-04-15T10:30:00+0900 uid 0
xlated 200B jited 256B memlock 4096B
btf_id 5
16.2 프로그램 BPF 코드 덤프
bpftool prog dump xlated id 1
검증을 통과한 BPF 명령어들을 보여준다.
bpftool prog dump jited id 1
JIT된 네이티브 코드를 보여준다.
16.3 맵 보기
bpftool map list
bpftool map dump id 5
16.4 BTF 덤프
bpftool btf dump file /sys/kernel/btf/vmlinux | less
16.5 검증기 로그
프로그램 로드 시 자세한 검증기 로그를 보고 싶으면:
bpftool prog load my.bpf.o /sys/fs/bpf/my_prog --log_level 7
17. 미래 — eBPF의 다음 단계
17.1 sched_ext
Linux 스케줄러 글에서 다뤘다. 사용자 공간이 BPF로 스케줄링 정책을 작성할 수 있게 한다. 6.12에서 메인라인 머지.
17.2 struct_ops
BPF 프로그램이 커널 인터페이스의 구현체가 될 수 있게 한다. 예를 들어 bpf_struct_ops 메커니즘으로 TCP congestion control 알고리즘을 BPF로 구현할 수 있다.
17.3 BPF for filesystem operations
파일시스템 후크에 BPF를 어태치할 수 있는 작업이 진행 중이다. 사용자 공간 정의 파일시스템 정책 (예: 캐시 정책, 배치 정책)이 가능해진다.
17.4 BPF in eBPF
자기 자신을 호출하는 BPF? 일부 추상화 작업에서 가능. tail call의 일반화.
17.5 다른 OS로의 확장
- ebpf-for-windows: Microsoft가 후원. Windows 커널에 eBPF 인프라를 가져오려는 시도.
- uBPF: 사용자 공간에서 BPF VM을 돌리는 라이브러리. AWS Firecracker가 사용.
eBPF가 OS 경계를 넘는 표준이 될 가능성이 있다.
18. 결론 — eBPF는 끝나지 않는다
이 글을 다 읽었다면, 다음 질문에 답할 수 있을 것이다:
- eBPF는 무엇이고 cBPF와 무엇이 다른가?
- 검증기는 어떻게 안전성을 보장하는가?
- BPF Map은 어떻게 사용자 공간과 통신하나?
- 어떤 어태치 포인트들이 있나?
- CO-RE는 무엇을 푸는가?
- libbpf, BCC, bpftrace의 차이는?
- XDP는 어떻게 빠른가?
- Cilium이 무엇을 다시 썼는가?
그러나 이 글은 시작에 불과하다. eBPF는 매년 새 기능이 들어오고, 매년 새 영역으로 확장된다. 1년 후의 eBPF는 오늘과 매우 다를 것이다.
eBPF를 배우는 가장 좋은 방법은 직접 해보는 것이다:
bpftrace한 줄 명령어로 시스템 진단 시작- BCC
/usr/share/bcc/tools/의 도구들을 읽고 수정 - libbpf 예제로 자기 도구 작성
- 검증기 에러를 만나면 한 줄 한 줄 읽기
이 단계를 거치면 eBPF가 더 이상 마법이 아니라, 강력하지만 이해 가능한 도구로 보이게 된다.
이 글로 Linux 내부 구조 시리즈와의 자매 작품도 마무리된다. 시리즈가 "커널이 무엇을 하는가"를 다뤘다면, 이 글은 "사용자가 커널을 어떻게 안전하게 확장하는가"를 다뤘다. 둘이 모이면 모던 Linux 시스템의 기본 정신이 그려진다.
다음 글에서는 [Cilium 내부 구조 딥다이브] 또는 [BPF로 만든 새 카테고리 도구들]을 다룰 예정이다.
부록 A — 참고 자료
- eBPF.io — eBPF 입문 허브.
- Linux Kernel Documentation: BPF — 공식 커널 문서.
- Brendan Gregg, "BPF Performance Tools" — BPF의 사실상 표준 교과서.
- Liz Rice, "Learning eBPF" — 입문서.
- Quentin Monnet의 BPF 블로그 — BPF 내부 구조 글들.
- Cilium Documentation — Cilium 사용/내부 구조.
- libbpf-bootstrap — libbpf 예제 모음, 가장 좋은 시작점.
- bpftrace one-liners — 한 줄 명령어 대백과.
- BCC tools — 200개 이상의 즉시 사용 가능한 도구.
부록 B — 자주 묻는 질문
Q: eBPF를 배우려면 어떻게 시작해야 하나? A: bpftrace로 시작. 한 줄 명령어를 따라하다 보면 자연스럽게 BPF 모델을 익히게 된다. 그 후 BCC 도구의 코드를 읽고, 마지막으로 libbpf-bootstrap으로 자기 도구 작성.
Q: eBPF 프로그램이 커널을 무너뜨릴 수 있나? A: 검증기가 실수로 통과시킨 결함이 있다면 가능. 그러나 이는 매우 드물고 빠르게 패치된다. 정상적인 사용에서는 BPF가 커널 충돌을 일으키지 않는다.
Q: kprobe와 tracepoint, 어떤 것을 써야 하나? A: tracepoint가 있으면 tracepoint. 안정적이고 빠르다. tracepoint가 없는 곳에는 kprobe (또는 fentry).
Q: BCC와 libbpf 중 무엇을 쓸 것인가? A: 새 코드는 무조건 libbpf. BCC는 옛 도구 유지 보수 또는 학습용.
Q: XDP와 tc 중 무엇이 더 빠른가? A: XDP. 패킷이 sk_buff로 변환되기 전에 처리. tc는 sk_buff 이후라서 약간 느리지만 더 풍부한 정보를 가진다.
Q: Cilium은 정말 iptables를 완전히 대체하나? A: 거의 그렇다. Cilium 모드에서는 kube-proxy의 iptables 규칙을 만들지 않는다. 다만 host의 일부 기본 규칙은 여전히 있을 수 있다.
Q: eBPF와 DTrace의 관계는? A: DTrace는 Solaris의 동적 추적 인프라. 비슷한 모델이지만 더 일찍 만들어졌고 다른 길을 갔다. eBPF는 cBPF에서 출발했고 더 일반적이다. 오늘날의 eBPF는 DTrace가 했던 거의 모든 일을 할 수 있다.
Q: eBPF가 Java/Go GC pause를 추적할 수 있나? A: 가능. uprobe로 GC 진입/리턴 함수에 어태치하면 GC 시간을 측정할 수 있다. JVM Flight Recorder 대안으로 사용 가능.
부록 C — 미니 용어집
- BPF: Berkeley Packet Filter. 1992년 BSD에서 시작.
- cBPF: classic BPF. 옛날 패킷 필터 ISA.
- eBPF: extended BPF. 2014+ 모던 BPF.
- Verifier: BPF 프로그램의 안전성을 정적 검증하는 모듈.
- JIT: Just-In-Time 컴파일러. BPF 바이트코드를 네이티브 코드로 변환.
- BTF: BPF Type Format. 커널 자료구조의 메타데이터.
- CO-RE: Compile Once, Run Everywhere. BTF 기반 portable BPF.
- libbpf: BPF 프로그램 로드/어태치 라이브러리.
- BCC: BPF Compiler Collection. 옛날 BPF 도구셋.
- bpftrace: BPF용 awk-like DSL.
- bpftool: BPF 인프라 디버깅 도구.
- kprobe: 커널 함수 진입점에 후크.
- kretprobe: 커널 함수 리턴점에 후크.
- fentry/fexit: ftrace trampoline 기반의 빠른 kprobe 대체.
- tracepoint: 커널에 박힌 안정적인 추적 포인트.
- uprobe: 사용자 공간 함수에 후크.
- XDP: eXpress Data Path. NIC 드라이버 직후의 BPF 후크.
- tc: Traffic Control. sk_buff 이후의 BPF 후크.
- LSM: Linux Security Module. 보안 후크.
- KRSI: Kernel Runtime Security Instrumentation. BPF LSM의 다른 이름.
- sched_ext: BPF로 작성하는 스케줄러 (6.12+).
- struct_ops: BPF가 커널 인터페이스의 구현체가 되는 메커니즘.
- PERCPU map: CPU별로 분리된 맵. 락 없음.
- RINGBUF: 새로운 BPF event ring buffer (5.8+).
- BPF tail call: BPF 프로그램이 다른 BPF 프로그램으로 점프.
- Cilium: BPF로 다시 쓴 쿠버네티스 네트워킹/보안.
- Tetragon: BPF 기반 런타임 보안 (Cilium 팀).
- Falco: BPF 기반 런타임 보안 (Sysdig).
- bpftune: BPF로 자동 시스템 튜닝 (Oracle).
이 글이 Linux 내부 구조 시리즈와의 자매 작품이다. 시리즈는 커널이 사용자에게 해주는 일을 다뤘다. 이 글은 사용자가 안전하게 커널 안으로 들어가는 방법을 다뤘다. 두 풍경이 모이면 모던 Linux의 정신이 그려진다.
eBPF Complete Guide — A Tiny VM Inside the Kernel: Verifier, JIT, CO-RE, Maps, Attach Points, XDP, LSM, sched_ext (2025)
Intro — eBPF is Linux's New Nervous System
People hearing about eBPF for the first time usually react with "Wait, that's actually possible?" A userspace program injects a small piece of code at arbitrary points inside the kernel, the code safely reads kernel data structures, and returns results back to userspace. On top of that, the code must pass a verifier before it runs, so bad code cannot bring down the kernel.
This would have sounded insane 30 years ago. Yet in 2025, Linux extends monitoring, security, networking, and even scheduling on exactly this model. Cilium rewrote all of Kubernetes networking with eBPF. Falco and Tetragon do runtime security with eBPF. Datadog's system metrics agent depends on eBPF. Linux 6.12 extended eBPF into the scheduler itself (sched_ext).
This article is for everyone — from those hearing about eBPF for the first time to those who have already used BCC tools. Starting from the 14-line ISA of cBPF in 1992, through Alexei Starovoitov's first eBPF patch in 2014, the magic of the verifier, JIT compilation, CO-RE, and every attach point of modern Linux — all in 1,400 lines.
This article is a sister piece to the Linux Internals Series. While the series covers "what the kernel does," this article covers "how users can extend the kernel."
1. History — From cBPF to eBPF
1.1 1992 — Berkeley Packet Filter
In 1992, Steven McCanne and Van Jacobson published a new packet-filtering mechanism for BSD. The previous packet filter (CSPF) was a tree-based expression and was very slow. Their new model was a simple virtual machine:
- A 32-bit accumulator (
A) and index register (X) - 16 32-bit scratch memory slots
- Simple instructions to read data from a packet or compare with slots
- Jump instructions to express decision trees
This model was a huge success on BSD and was soon ported to Linux and Solaris. tcpdump and libpcap run on top of it. The expression tcp port 80 is internally compiled into around 20 cBPF instructions.
cBPF practically did not change for 30 years. It was simple, it worked, and it was sufficient for the narrow domain of packet filtering.
1.2 2013 — Alexei Starovoitov's First Patch
In 2013, Alexei Starovoitov, then at PLUMgrid, submitted a large patch to LKML. The title: "extended BPF". Key changes:
- 32-bit → 64-bit registers
- 2 → 11 registers (
R0-R10) - Function call instructions
- More arithmetic/bit operations
- An ISA very close to x86_64 — JIT compilation is nearly 1:1
Reactions were initially skeptical. "Why extend BPF? Isn't it for packet filtering?" But Alexei's vision was bigger — a general interface for small, safe code that userspace can run inside the kernel.
1.3 2014 — Linux 3.18 Mainline
In September 2014, the first eBPF patch was merged in Linux 3.18. Initially it was for socket filters (a direct successor to cBPF). But new attach points were added in nearly every release:
- 2014 (3.18): Basic infrastructure, socket filter
- 2015 (3.19): kprobe attach
- 2015 (4.1): tc (traffic control) attach
- 2016 (4.4): tracepoint attach
- 2016 (4.8): XDP attach
- 2017 (4.10): cgroup attach, perf_event attach
- 2018 (4.15): BTF introduced (foundation of CO-RE)
- 2018 (4.18): socket lookup
- 2019 (5.7): LSM attach (KRSI)
- 2020 (5.8): BPF_RINGBUF
- 2021 (5.13): stack trace BPF helper
- 2022 (5.15): bpf_loop helper
- 2023 (6.4): BPF stack walking improvements
- 2024 (6.12): sched_ext mainline merge
Today's eBPF is integrated with almost every kernel subsystem. A small VM originally made for packet filtering 30 years ago has changed parts of the very operating model of Linux.
★ Insight ─────────────────────────────────────
- The name confusion: "eBPF" is not the official name. Kernel code just calls it "BPF". Outside, "eBPF" (short for "extended BPF") is more common. The old BPF is distinguished as "classic BPF" or "cBPF".
- Why a "packet filter" became a general VM: The key insight was that the model of "running verified, safe code inside the kernel" applies not just to packet filtering but to almost all kernel extensions. eBPF took cBPF's properties of "self-termination guarantee + memory safety" and brought them to a richer ISA.
- Almost nonexistent outside Linux: BSD has some eBPF ports (BSD with DTrace finds eBPF less compelling), and Windows has had a separate project called ebpf-for-windows since 2021. But saying "eBPF is essentially Linux" is almost correct.
─────────────────────────────────────────────────
2. The eBPF Virtual Machine — ISA and Registers
2.1 Eleven 64-bit Registers
The eBPF VM is very simple:
R0: function return value, program exit valueR1toR5: function arguments (1-5)R6toR9: callee-savedR10: stack frame pointer (read-only)
This interface is intentionally very similar to the x86_64 calling convention. JIT compilation becomes nearly 1:1.
2.2 Stack
Each program has a 512-byte stack. R10 points to its base. It looks small, but it is kept small because the verifier has to track every usage.
2.3 Instruction Encoding
eBPF instructions are fixed at 64 bits:
+----+----+----+--------+
| op | dst | src | offset | imm |
| 8 | 4 | 4 | 16 | 32 |
+----+----+----+--------+
op: 8-bit opcodedst/src: 4-bit register indicesoffset: 16-bit signed offset (jumps, memory accesses)imm: 32-bit immediate value
2.4 Instruction Categories
Core categories:
- ALU: arithmetic/logic operations (32-bit and 64-bit)
- Memory: load/store
- Branch: conditional jumps
- Call: helper function calls
- Exit: program exit
Example:
BPF_MOV64_IMM(BPF_REG_0, 0) // R0 = 0
BPF_MOV64_REG(BPF_REG_1, BPF_REG_10) // R1 = R10 (frame pointer)
BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, -8) // R1 += -8
BPF_LD_MAP_FD(BPF_REG_1, map_fd) // R1 = map fd
BPF_CALL_FUNC(BPF_FUNC_map_lookup_elem) // call helper
BPF_EXIT_INSN() // exit
2.5 A 200-line ISA
The entire eBPF ISA can be expressed in about 200 lines of C (see include/uapi/linux/bpf.h). It is very small. Yet a tremendous amount of work can be done with this small ISA.
3. The Lifecycle of an eBPF Program
3.1 Writing — from C to BPF Bytecode
The typical flow:
// hello.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "GPL";
SEC("kprobe/sys_open")
int hello(struct pt_regs *ctx) {
char fmt[] = "Hello from kprobe!\n";
bpf_trace_printk(fmt, sizeof(fmt));
return 0;
}
Compile:
clang -O2 -target bpf -c hello.bpf.c -o hello.bpf.o
-target bpf is the key. Clang's BPF backend compiles into eBPF instructions. The output is an ELF file containing BPF bytecode.
3.2 Loading — the bpf() System Call
You extract the bytecode from the ELF and send it to the kernel via the bpf(BPF_PROG_LOAD, ...) system call. libbpf encapsulates this process:
struct bpf_object *obj = bpf_object__open("hello.bpf.o");
bpf_object__load(obj); // verify + JIT
struct bpf_program *prog = bpf_object__find_program_by_name(obj, "hello");
bpf_program__attach(prog); // attach to kprobe
3.3 The Verifier
When bpf(BPF_PROG_LOAD, ...) is called, the kernel first runs the verifier. The verifier checks whether the program is safe:
- Are all memory accesses valid?
- Do all jumps go to defined locations?
- Are there no infinite loops?
- Are helper calls made in the appropriate context?
- Is stack usage within limits?
The verifier generally proceeds in 5 stages (detailed in the next section).
3.4 JIT Compilation
Once verification passes, the JIT compiler compiles BPF bytecode into native machine code. On x86_64, the mapping is nearly 1:1. ARM64 is similar.
JIT is optional but is enabled on almost all modern systems (net.core.bpf_jit_enable=1). It is about 10-100x faster than the interpreter.
3.5 Attach
The JIT-compiled program is connected to a specific attach point. For example, the SEC name kprobe/sys_open means "put a kprobe on the entry of the sys_open function." libbpf calls the BPF_PROG_ATTACH system call to connect it.
From this point on, the BPF program runs every time sys_open is called.
4. The Verifier — The Real Magic of eBPF
4.1 What It Guarantees
The verifier statically guarantees the following:
- Memory safety: all loads/stores access valid regions
- Type safety: pointers are not treated as integers
- Termination guarantee: no infinite loops (or explicit bounds)
- Stack safety: no stack overflow
- Call safety: helper calls are made with appropriate arguments and context
It guarantees all of this through static analysis without actually running the code. This is a very hard problem.
4.2 How It Works — Abstract Interpretation
The core algorithm of the verifier is abstract interpretation. It simulates every possible execution path, tracking the register/stack state at each point as an "abstract value."
Examples of abstract values:
R0 = SCALAR_VALUE, range [0, 100]R1 = PTR_TO_MAP_VALUE, off 0..16R2 = PTR_TO_PACKET, off 14..1500R3 = NOT_INIT
These abstract values are updated as each instruction is processed. At branches, both paths are explored.
4.3 Avoiding Path Explosion — Pruning
Naively exploring every path would cause exponential blowup. The verifier remembers states it has already seen (states_cache) and prunes a path when it re-enters the same state.
This is very effective, but complex programs may still take 10+ seconds to verify. When the number of verified states exceeds a million, the verifier gives up (-EFBIG or -ENOSPC).
4.4 Memory Access Verification
int *p = bpf_map_lookup_elem(&my_map, &key);
*p = 42; // verification fails!
This code fails verification. bpf_map_lookup_elem can return NULL, but the code dereferences it without a NULL check. Correct code:
int *p = bpf_map_lookup_elem(&my_map, &key);
if (p) {
*p = 42; // OK
}
The verifier sees the if (p) check and, inside that branch, tracks that p is not NULL. That is how it knows the *p access is safe.
4.5 Termination Guarantee
Loops are the verifier's headache. Initially, eBPF banned loops entirely. All loops had to be unrolled at compile time.
From 5.3, bounded loops are allowed. If the verifier can prove the loop count is finite, OK:
#pragma unroll
for (int i = 0; i < 10; i++) {
/* ... */
}
From 5.13, the bpf_loop helper was added, enabling more flexible loops:
static int callback(__u32 idx, void *data) {
/* ... */
return 0; // 0 = continue, 1 = stop
}
bpf_loop(1000, callback, &my_data, 0);
4.6 Stack Usage
Each program has a 512-byte stack. Putting a large struct on the stack quickly exceeds the limit.
SEC("kprobe/sys_open")
int hello(struct pt_regs *ctx) {
char buf[600]; // verification fails! stack limit exceeded
return 0;
}
Alternative: use a BPF_MAP_TYPE_PERCPU_ARRAY as a "large scratch region."
4.7 Debugging the Verifier
When the verifier fails, it produces very long error messages. You can view them in detail with bpftool prog show + verifier_log_level=2. The verifier's abstract state is printed alongside each instruction, line by line:
0: (b7) r1 = 0
R1_w=0 R10=fp0
1: (61) r2 = *(u32 *)(r1 +0)
R1 invalid mem access 'inv'
processed 2 insns ...
The ability to read these logs is the core skill of an eBPF developer.
5. BPF Maps — Communicating with Userspace
5.1 Why They Are Needed
BPF programs are volatile — all local state disappears when the call ends. To persist data or share it with userspace, you need a separate mechanism. That is BPF Maps.
Maps are key-value stores. BPF programs access them via helper functions, and userspace accesses them via system calls.
5.2 Map Types (17+)
Core types:
| Type | Purpose |
|---|---|
BPF_MAP_TYPE_HASH | Hash table (most common) |
BPF_MAP_TYPE_ARRAY | Fixed-size array, index-based |
BPF_MAP_TYPE_PERCPU_HASH | Per-CPU hash (lock-free) |
BPF_MAP_TYPE_PERCPU_ARRAY | Per-CPU array |
BPF_MAP_TYPE_LRU_HASH | Hash with LRU eviction |
BPF_MAP_TYPE_LPM_TRIE | Longest-prefix-match trie (for routing) |
BPF_MAP_TYPE_PROG_ARRAY | BPF program array (for tail calls) |
BPF_MAP_TYPE_PERF_EVENT_ARRAY | For perf event output |
BPF_MAP_TYPE_RINGBUF | Newer ring buffer (5.8+) |
BPF_MAP_TYPE_QUEUE | FIFO |
BPF_MAP_TYPE_STACK | LIFO |
BPF_MAP_TYPE_SK_STORAGE | Per-socket storage |
BPF_MAP_TYPE_TASK_STORAGE | Per-task storage |
BPF_MAP_TYPE_INODE_STORAGE | Per-inode storage |
BPF_MAP_TYPE_CGROUP_STORAGE | Per-cgroup storage |
BPF_MAP_TYPE_BLOOM_FILTER | Bloom filter |
BPF_MAP_TYPE_USER_RINGBUF | User → kernel ringbuf (5.19+) |
5.3 Hash Map Example
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, __u32);
__type(value, __u64);
} counter_map SEC(".maps");
SEC("kprobe/sys_open")
int count_opens(struct pt_regs *ctx) {
__u32 pid = bpf_get_current_pid_tgid() >> 32;
__u64 *count = bpf_map_lookup_elem(&counter_map, &pid);
if (count) {
__sync_fetch_and_add(count, 1);
} else {
__u64 init = 1;
bpf_map_update_elem(&counter_map, &pid, &init, BPF_ANY);
}
return 0;
}
This program increments a per-PID counter on each sys_open call. In userspace, you query the same map with bpf_map__lookup_elem.
5.4 PERCPU Variants — Lock-free Counters
BPF_MAP_TYPE_PERCPU_HASH has a separate hash table per CPU. No locks are needed — only BPF programs on the same CPU access that CPU's map.
When userspace reads a PERCPU map, it receives values from all CPUs. Aggregating them is userspace's responsibility.
PERCPU maps are very useful for counters, statistics, and histograms. There is no lock contention, so they are very fast.
5.5 RINGBUF — The New Event Output
Traditionally, when BPF programs sent events to userspace, they used BPF_MAP_TYPE_PERF_EVENT_ARRAY. This is a per-CPU perf ring buffer with a separate ring for each CPU.
BPF_MAP_TYPE_RINGBUF, introduced in 5.8, is more elegant:
- A single shared ring buffer (no distribution across CPUs)
- Uses less memory
- BPF programs can directly reserve/commit variable-size events
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("tp/sched/sched_process_exec")
int on_exec(struct trace_event_raw_sched_process_exec *ctx) {
struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e) return 0;
e->pid = bpf_get_current_pid_tgid() >> 32;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_ringbuf_submit(e, 0);
return 0;
}
Most new BPF tools use ringbuf. The perf event array is kept for compatibility.
6. Helper Functions — 200+ Kernel Interfaces
6.1 What Is a Helper
BPF programs cannot call arbitrary kernel functions. Instead, they can only call a verified set of "helper functions." Helpers are safe interfaces that the kernel explicitly exposes.
Helpers numbered around 100 at 5.0 and far exceed 200 by 6.x.
6.2 Core Helpers
The most frequently used ones:
| Helper | Purpose |
|---|---|
bpf_map_lookup_elem | Read a value from a map |
bpf_map_update_elem | Write a value to a map |
bpf_map_delete_elem | Delete a value from a map |
bpf_get_current_pid_tgid | Current PID/TID |
bpf_get_current_uid_gid | Current UID/GID |
bpf_get_current_comm | Current process name |
bpf_get_current_task | Current task_struct pointer |
bpf_ktime_get_ns | Monotonic time (nanoseconds) |
bpf_trace_printk | Debug printf (/sys/kernel/debug/tracing/trace_pipe) |
bpf_perf_event_output | Output event to perf event array |
bpf_ringbuf_reserve/bpf_ringbuf_submit | ringbuf output |
bpf_get_stack/bpf_get_stackid | Stack trace |
bpf_probe_read_kernel/bpf_probe_read_user | Safe memory reads |
bpf_skb_load_bytes | Read bytes from a packet |
bpf_redirect | Packet redirection |
bpf_xdp_adjust_head | XDP packet header adjustment |
bpf_jiffies64 | Current jiffies |
bpf_send_signal | Send a signal (5.3+) |
6.3 Helper Safety
Each helper specifies its argument types. The verifier checks argument types at call time.
For example, the signature of bpf_map_lookup_elem:
void *bpf_map_lookup_elem(struct bpf_map *map, const void *key)
The verifier checks whether map is a pointer to a map of a type like BPF_MAP_TYPE_HASH and whether key points to memory at least as large as that map's key_size.
6.4 GPL vs non-GPL Helpers
Some helpers are GPL-only. BPF programs with non-GPL licenses cannot call them. bpf_trace_printk is one such helper (being a debug aid).
char LICENSE[] SEC("license") = "GPL"; // can use GPL helpers
Most commercial BPF tools are GPL.
7. Attach Points — Where You Can Hook
The real power of eBPF comes from its variety of attach points. Each attach point has its own "context" (arguments) and set of helpers.
7.1 kprobe / kretprobe
Can attach to the entry/return of nearly any kernel function.
SEC("kprobe/vfs_read")
int on_vfs_read(struct pt_regs *ctx) {
/* ... */
return 0;
}
At the entry point, you access arguments via pt_regs. At the return point, you access the return value via PT_REGS_RC(ctx).
Pros: can hook nearly any kernel function. Cons: function signatures may vary across kernel versions. CO-RE mitigates this somewhat.
7.2 fentry / fexit (BPF Trampoline)
A faster alternative introduced in 5.5. kprobe inserts an INT3 instruction, whereas fentry/fexit leverages the ftrace infrastructure to call directly. About 10x faster.
SEC("fentry/vfs_read")
int BPF_PROG(on_vfs_read_entry, struct file *file, char *buf, size_t count) {
/* direct access to arguments, thanks to BTF */
return 0;
}
7.3 tracepoint
Stable trace points pre-embedded in kernel code. Unlike kprobes, they do not break when function names change.
SEC("tp/sched/sched_process_exec")
int on_exec(struct trace_event_raw_sched_process_exec *ctx) {
/* direct access to tracepoint arguments */
return 0;
}
All available tracepoints can be listed under /sys/kernel/debug/tracing/events/.
7.4 raw_tracepoint
A faster version of tracepoint. It receives tracepoint arguments as-is without decoding them — slightly more coding burden, but faster.
7.5 uprobe / uretprobe
Can also attach to userspace functions. For example, attach to libc's malloc to trace every call.
SEC("uprobe//usr/lib/libc.so.6:malloc")
int on_malloc(struct pt_regs *ctx) {
size_t size = PT_REGS_PARM1(ctx);
/* ... */
return 0;
}
Very powerful but expensive — each call triggers a user→kernel trap.
7.6 perf_event
Attach to perf events (CPU cycles, cache misses, etc.). The foundation of CPU profiling.
SEC("perf_event")
int sample(struct bpf_perf_event_data *ctx) {
/* called every N cycles */
return 0;
}
7.7 XDP — eXpress Data Path
Attaches at the very front of the network interface's receive path. Called right after the driver, before a packet is converted into an sk_buff. Very fast.
SEC("xdp")
int xdp_prog(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
if (data + sizeof(struct ethhdr) > data_end)
return XDP_PASS;
struct ethhdr *eth = data;
if (eth->h_proto == bpf_htons(ETH_P_IP)) {
/* IP packet */
}
return XDP_PASS; // or XDP_DROP, XDP_TX, XDP_REDIRECT
}
XDP actions:
XDP_PASS: forward to the normal network stackXDP_DROP: dropXDP_TX: retransmit back out the same NICXDP_REDIRECT: send to another NICXDP_ABORTED: error
XDP is very popular for DDoS protection, load balancing, and packet rewriting. Cloudflare uses XDP extensively in its infrastructure.
7.8 tc (Traffic Control)
Attaches at a slightly later stage than XDP. Since the sk_buff has already been built, it has richer information (cgroup, socket, etc.). Not as fast as XDP but more flexible.
Attach via tc-bpf:
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj my_prog.bpf.o
7.9 cgroup hooks
Can attach to specific syscalls of all processes within a cgroup. For example, blocking all connect calls in a cgroup:
SEC("cgroup/connect4")
int restrict_connect(struct bpf_sock_addr *ctx) {
if (ctx->user_port == bpf_htons(22)) {
return 0; // block SSH
}
return 1;
}
The foundation of container security policies.
7.10 LSM hooks (KRSI)
Introduced in 5.7. BPF programs can be attached to all Linux Security Module hooks. You can replace or augment SELinux/AppArmor with BPF.
SEC("lsm/file_open")
int BPF_PROG(check_file_open, struct file *file, int ret) {
/* can inspect file opens and deny them */
return -EPERM; // or 0 = OK
}
Tetragon operates on this model.
7.11 sched_ext (6.12+)
The newest attach point. Allows userspace to write scheduling policies in BPF. Covered in detail in the Linux scheduler article.
7.12 socket lookup, sock_ops, sk_msg
Can attach at various stages of socket processing. Cilium's sidecar-less service mesh leverages this.
8. CO-RE — Compile Once, Run Everywhere
8.1 The Problem
eBPF programs often read kernel data structures (task_struct, sk_buff, etc.). But the layouts of these structs differ across kernel versions and compile options. If the machine you built on differs from the machine you run on, things break.
Old BCC solved this with "compile at runtime." It required clang and kernel headers installed on every machine, recompiling every run. Slow, disk-hungry, and unfit for production.
8.2 BTF — BPF Type Format
BTF is a lightweight debug-info format that embeds metadata of kernel data structures. A simplified version of DWARF. From 5.2, the kernel exposes its own BTF at /sys/kernel/btf/vmlinux.
BTF contains the field names and offsets of every kernel struct. With it, userspace tools can figure out "where is task_struct->mm in this kernel."
8.3 How CO-RE Works
CO-RE builds on BTF to let BPF programs run across different kernel versions.
#include <vmlinux.h> // header generated from the host kernel's BTF
#include <bpf/bpf_core_read.h>
SEC("kprobe/sys_open")
int hello(struct pt_regs *ctx) {
struct task_struct *task = (void *)bpf_get_current_task();
pid_t pid = BPF_CORE_READ(task, pid); // macro magic
/* ... */
return 0;
}
The BPF_CORE_READ macro does not inline "the field offset" at compile time. Instead, it leaves relocation information in the ELF saying "look this field up via BTF."
At runtime, libbpf processes those relocations — it looks at the host kernel's BTF and fills in the actual offsets. The same BPF ELF works on both 5.10 and 6.5 kernels.
8.4 Generating vmlinux.h
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
This file is about 4MB and defines every kernel struct. BPF code includes it.
8.5 Field Existence Checks
Another CO-RE feature: checking whether a field exists. Lets you respond flexibly when fields are newly added or removed.
if (bpf_core_field_exists(task->cgroups)) {
/* kernel has this field */
} else {
/* kernel does not */
}
8.6 Impact
Thanks to CO-RE, BPF tools have become truly portable. Modern tools like Falco, Cilium, Tetragon, and Pixie all use CO-RE. A binary built once works on any kernel (given that BTF is available).
9. Userspace Tools — libbpf, BCC, bpftrace
9.1 BCC — The Old Way
BCC (BPF Compiler Collection) is the oldest BPF toolkit. It makes writing BPF programs easy via Python/Lua wrappers.
from bcc import BPF
prog = """
int hello(void *ctx) {
bpf_trace_printk("Hello!\\n");
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event="sys_open", fn_name="hello")
b.trace_print()
Problems: compiles on every run. Requires clang + kernel headers. Large disk footprint. Unfit for production.
BCC is still used in many tools and has great value as an example archive (there are over 200 tools in /usr/share/bcc/tools/).
9.2 libbpf — The Modern Way
libbpf is a C library that encapsulates loading/attaching BPF programs. It supports CO-RE.
#include <bpf/libbpf.h>
int main() {
struct bpf_object *obj = bpf_object__open_file("hello.bpf.o", NULL);
bpf_object__load(obj);
struct bpf_program *prog = bpf_object__find_program_by_name(obj, "hello");
bpf_program__attach(prog);
while (1) sleep(1);
return 0;
}
Build once and deploy. Datadog, Cilium, and Tetragon are all libbpf-based.
9.3 bpftrace — A DSL
The fastest entry-level tool. You write BPF programs in a one-liner via an awk-like DSL.
# count every sys_open call
bpftrace -e 'kprobe:sys_open { @[comm] = count(); }'
# latency distribution of vfs_read (histogram)
bpftrace -e '
kprobe:vfs_read { @start[tid] = nsecs; }
kretprobe:vfs_read /@start[tid]/ {
@lat = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# page fault tracking with stack traces
bpftrace -e 'tracepoint:exceptions:page_fault_user { @[ustack] = count(); }'
bpftrace uses libbpf under the hood. It converts the DSL into BPF C code, compiles it with clang, and loads it with libbpf.
9.4 Which Tool to Use
- Ad hoc diagnostics: bpftrace
- Production tools / long-running agents: libbpf (C/Rust/Go)
- Reference / reusing existing BCC tools: BCC
Most new BPF code is migrating to libbpf. BCC is increasingly being relegated to the role of "example archive."
10. Case 1 — bpftrace One-liner Diagnostics
Real-world examples showcasing the power of bpftrace:
10.1 Which process reads the most from disk
bpftrace -e '
tracepoint:block:block_rq_issue { @[comm] = sum(args->bytes); }
'
10.2 Who makes the most syscalls
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'
10.3 Track TCP retransmissions
bpftrace -e '
kprobe:tcp_retransmit_skb {
@[comm] = count();
}
'
10.4 Which function takes the longest
bpftrace -e '
kprobe:vfs_read { @start[tid] = nsecs; }
kretprobe:vfs_read /@start[tid]/ {
$duration = nsecs - @start[tid];
@hist = hist($duration / 1000);
delete(@start[tid]);
}'
10.5 Full context on OOM kill
bpftrace -e '
kprobe:oom_kill_process {
printf("OOM kill: comm=%s pid=%d ustack=%s\n",
comm, pid, ustack);
}'
Each one-liner does what a sophisticated tool would have had to. A new benchmark for operations debugging.
11. Case 2 — XDP DDoS Defense
11.1 Scenario
You are under a UDP flood attack. Hundreds of millions of packets per second pour in and the NIC is overwhelmed. The defense code must run before a packet is converted into an sk_buff.
11.2 The XDP Program
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__type(key, struct bpf_lpm_trie_key);
__type(value, __u32);
__uint(max_entries, 1024);
__uint(map_flags, BPF_F_NO_PREALLOC);
} blacklist SEC(".maps");
SEC("xdp")
int xdp_drop(struct xdp_md *ctx) {
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) return XDP_PASS;
if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end) return XDP_PASS;
/* look up the IP in the LPM trie */
struct {
__u32 prefixlen;
__u32 addr;
} key = { .prefixlen = 32, .addr = ip->saddr };
if (bpf_map_lookup_elem(&blacklist, &key)) {
return XDP_DROP; // drop immediately if blacklisted
}
return XDP_PASS;
}
char LICENSE[] SEC("license") = "GPL";
11.3 Attach
ip link set dev eth0 xdpgeneric obj xdp_drop.bpf.o sec xdp
11.4 Results
This program drops packets before they are converted into sk_buffs. Roughly 10x faster (numbers like 24Mpps vs 2.5Mpps). Cloudflare uses very similar patterns in its infrastructure.
Packets XDP drops have almost no impact on system metrics — no sk_buff allocation, so no memory use and almost no CPU.
12. Case 3 — Cilium's Kubernetes Networking
12.1 The Vision
Cilium is a project that rewrites Kubernetes networking/security/observability with eBPF. It completely replaces iptables-based kube-proxy.
12.2 What Is Different
Traditional Kubernetes networking:
- Tens of thousands of iptables rules (scales with the number of services)
- Every new service updates iptables on every node
- Packet processing goes through conntrack, NAT, and routing
- Genuinely slow in large clusters
Cilium's model:
- Service/endpoint info stored in BPF maps
- Packet processing is done directly by BPF programs
- Almost no need for iptables
- Consistent performance even in large clusters
12.3 Sidecar-less Service Mesh
Cilium leverages sock_ops and sk_msg to intercept L7 traffic without a sidecar proxy. The traditional Istio/Linkerd model puts an Envoy sidecar in every pod, which is expensive in memory/CPU/latency.
Cilium's sidecarless model runs one (or zero) proxy per node and redirects traffic to that proxy with BPF.
12.4 Tetragon — Security
Another tool by the Cilium team. Uses LSM hooks and tracepoints to monitor all container activity. You can know in real time "this container just read /etc/passwd."
Similar to traditional Falco but deeper. On policy violation, it can send a signal or block immediately.
★ Insight ─────────────────────────────────────
- New companies made by eBPF: Isovalent (the Cilium company, acquired by Cisco in 2024), Polar Signals (continuous profiling), Pixie (observability), Groundcover, Levitate Security. All new categories made possible by eBPF.
- The end of the iptables era: In Kubernetes infrastructure, iptables is now treated as legacy. nftables replaced some of it, but the real future is eBPF. Cilium is becoming the de facto standard.
- The meaning of a sidecar-less mesh: In Kubernetes clusters, sidecar proxies often consume more than 30% of node memory. Replacing them with eBPF frees that memory. That is a huge cost difference.
─────────────────────────────────────────────────
13. Case 4 — Falco Runtime Security
Falco is a runtime security tool by Sysdig. It detects abnormal behavior in containers and sends alerts. Examples: containers reading /etc/shadow, attempts to escalate to root, suspicious shell spawns, etc.
Traditionally it used a sysdig kernel module, but recent versions migrated to eBPF. It intercepts every syscall and feeds them to a rule engine.
# Example Falco rule
- rule: Read sensitive file
desc: An attempt to read sensitive file
condition: open_read and sensitive_files
output: Sensitive file opened (user=%user.name file=%fd.name)
priority: WARNING
Thanks to eBPF, no kernel module is needed and it works across any kernel version.
14. Case 5 — bpftune Automatic Tuning
A tool by Oracle. It monitors system metrics with eBPF and automatically adjusts sysctl values. Examples:
- TCP connections time out frequently → lower
tcp_keepalive_time - Memory pressure occurs frequently → adjust
vm.swappiness - Disk IO bottleneck → increase readahead size
Traditionally, system tuning was done manually by humans. bpftune automates this in a data-driven way. Without eBPF, you would have had to run a separate tool for every metric.
15. Security — The Dangers of eBPF
As powerful as eBPF is, it is dangerous in the wrong hands.
15.1 BPF Permissions
Loading a BPF program typically requires CAP_BPF (5.8+) and CAP_PERF_MON or CAP_NET_ADMIN. Previously it required CAP_SYS_ADMIN (near-equivalent to root).
15.2 Bypassing the Verifier
The verifier is static analysis and not 100% perfect. Several past CVEs allowed tricking the verifier into enabling arbitrary memory reads/writes:
- CVE-2022-23222: flaw in BPF pointer-arithmetic verification
- CVE-2021-45402: flaw in 32-bit branch verification
- CVE-2021-3490: flaw in ALU32 boundary tracking
Such flaws are patched quickly when discovered, but as the verifier grows more complex, the chance of new flaws also rises.
15.3 unprivileged_bpf_disabled
Linux can block normal users from using BPF by default:
echo 1 > /proc/sys/kernel/unprivileged_bpf_disabled
Most distros turn this on by default. Normal users cannot load BPF programs.
15.4 BPF LSM and BPF Self-Protection
The existence of BPF LSM means that BPF can govern BPF. Policies like "deny BPF program loads in this cgroup" can themselves be expressed in BPF.
15.5 Side Channels
BPF programs can leak privileged information. There is research showing side-channel attacks like Spectre are possible with BPF. The verifier rejects some concerning patterns, but perfect defense is hard.
16. Debugging — bpftool
bpftool is the Swiss army knife of BPF infrastructure.
16.1 Listing Loaded Programs
bpftool prog list
1: kprobe name hello tag a1b2c3d4e5f60718
loaded_at 2026-04-15T10:30:00+0900 uid 0
xlated 200B jited 256B memlock 4096B
btf_id 5
16.2 Dumping a Program's BPF Code
bpftool prog dump xlated id 1
Shows the BPF instructions that passed verification.
bpftool prog dump jited id 1
Shows the JIT-compiled native code.
16.3 Viewing Maps
bpftool map list
bpftool map dump id 5
16.4 Dumping BTF
bpftool btf dump file /sys/kernel/btf/vmlinux | less
16.5 Verifier Logs
To see detailed verifier logs on program load:
bpftool prog load my.bpf.o /sys/fs/bpf/my_prog --log_level 7
17. The Future — eBPF's Next Steps
17.1 sched_ext
Covered in the Linux scheduler article. Allows userspace to write scheduling policies in BPF. Mainline-merged in 6.12.
17.2 struct_ops
Enables BPF programs to become implementations of kernel interfaces. For example, you can implement a TCP congestion-control algorithm in BPF via the bpf_struct_ops mechanism.
17.3 BPF for Filesystem Operations
Work is ongoing to allow attaching BPF to filesystem hooks. User-defined filesystem policies (e.g., cache policies, placement policies) become possible.
17.4 BPF in eBPF
BPF calling itself? Possible in some abstraction work. A generalization of tail calls.
17.5 Expansion to Other OSes
- ebpf-for-windows: Backed by Microsoft. An attempt to bring eBPF infrastructure into the Windows kernel.
- uBPF: A library for running a BPF VM in userspace. Used by AWS Firecracker.
eBPF may become a cross-OS standard.
18. Conclusion — eBPF Is Not Ending
If you have read this far, you should be able to answer:
- What is eBPF, and how is it different from cBPF?
- How does the verifier guarantee safety?
- How do BPF maps communicate with userspace?
- What attach points exist?
- What does CO-RE solve?
- What is the difference between libbpf, BCC, and bpftrace?
- Why is XDP fast?
- What did Cilium rewrite?
But this article is only the beginning. eBPF gets new features every year and expands into new areas every year. eBPF a year from now will be very different from today.
The best way to learn eBPF is to do it:
- Start diagnosing your system with
bpftraceone-liners - Read and modify the tools in BCC's
/usr/share/bcc/tools/ - Write your own tool with libbpf examples
- When you hit verifier errors, read them line by line
Going through these steps makes eBPF no longer feel like magic, but like a powerful yet comprehensible tool.
This article also wraps up its sister-piece relationship with the Linux Internals Series. While the series covered "what the kernel does," this article covered "how users can safely extend the kernel." Put together, they paint the basic spirit of modern Linux systems.
Next articles will cover a [Cilium internals deep dive] or [new categories of tools made with BPF].
Appendix A — References
- eBPF.io — eBPF introduction hub.
- Linux Kernel Documentation: BPF — official kernel docs.
- Brendan Gregg, "BPF Performance Tools" — de facto standard textbook for BPF.
- Liz Rice, "Learning eBPF" — introductory book.
- Quentin Monnet's BPF blog — BPF internals posts.
- Cilium Documentation — Cilium usage/internals.
- libbpf-bootstrap — libbpf examples, the best starting point.
- bpftrace one-liners — encyclopedia of one-liners.
- BCC tools — 200+ ready-to-use tools.
Appendix B — FAQ
Q: How should I start learning eBPF? A: Start with bpftrace. Following along with one-liners naturally teaches you the BPF model. After that, read BCC tool code, and finally write your own tool with libbpf-bootstrap.
Q: Can an eBPF program crash the kernel? A: Possible if the verifier mistakenly lets through a flawed program. But that is very rare and patched quickly. In normal use, BPF does not cause kernel crashes.
Q: Should I use kprobe or tracepoint? A: Tracepoint, if one is available. It is stable and fast. For places without a tracepoint, use kprobe (or fentry).
Q: BCC or libbpf? A: New code should be libbpf, no question. BCC is for maintaining old tools or for learning.
Q: Is XDP or tc faster? A: XDP. It processes packets before they become sk_buffs. tc is after sk_buff, so it is slightly slower but has richer information.
Q: Does Cilium really replace iptables entirely? A: Almost. In Cilium mode, kube-proxy's iptables rules are not created. Some basic host rules may still exist.
Q: What is the relationship between eBPF and DTrace? A: DTrace is Solaris's dynamic tracing infrastructure. A similar model, but it was built earlier and went a different direction. eBPF started from cBPF and is more general. Today's eBPF can do nearly everything DTrace did.
Q: Can eBPF track Java/Go GC pauses? A: Yes. Attaching uprobes to the GC entry/return functions lets you measure GC time. A usable alternative to JVM Flight Recorder.
Appendix C — Mini Glossary
- BPF: Berkeley Packet Filter. Started in BSD in 1992.
- cBPF: classic BPF. The old packet-filter ISA.
- eBPF: extended BPF. Modern BPF from 2014+.
- Verifier: Module that statically verifies the safety of BPF programs.
- JIT: Just-In-Time compiler. Converts BPF bytecode into native code.
- BTF: BPF Type Format. Metadata for kernel data structures.
- CO-RE: Compile Once, Run Everywhere. Portable BPF based on BTF.
- libbpf: Library for loading/attaching BPF programs.
- BCC: BPF Compiler Collection. The old BPF toolkit.
- bpftrace: An awk-like DSL for BPF.
- bpftool: BPF infrastructure debugging tool.
- kprobe: Hook at the entry of a kernel function.
- kretprobe: Hook at the return of a kernel function.
- fentry/fexit: Faster kprobe alternatives based on the ftrace trampoline.
- tracepoint: Stable tracing points embedded in the kernel.
- uprobe: Hook on a userspace function.
- XDP: eXpress Data Path. BPF hook right after the NIC driver.
- tc: Traffic Control. BPF hook after the sk_buff is built.
- LSM: Linux Security Module. Security hooks.
- KRSI: Kernel Runtime Security Instrumentation. Another name for BPF LSM.
- sched_ext: Scheduler written in BPF (6.12+).
- struct_ops: Mechanism where BPF becomes the implementation of a kernel interface.
- PERCPU map: A map split per CPU. No locks.
- RINGBUF: The newer BPF event ring buffer (5.8+).
- BPF tail call: A BPF program jumps to another BPF program.
- Cilium: Kubernetes networking/security rewritten in BPF.
- Tetragon: BPF-based runtime security (Cilium team).
- Falco: BPF-based runtime security (Sysdig).
- bpftune: Automatic system tuning with BPF (Oracle).
This article is a sister piece to the Linux Internals Series. The series covered what the kernel does for users. This article covered how users can safely step into the kernel. Together, the two scenes paint the spirit of modern Linux.