Linux eBPF(extended Berkeley Packet Filter) 프레임워크를 실무 관점에서 심층 분석합니다. Verifier의 상태 추적과 pruning, BTF·CO-RE 기반 이식성, BPF 맵 설계 패턴, libbpf 스켈레톤 API, BPF 명령어 집합(ISA), 프로그램 타입별 hook point, 개발 도구(bpftool/bpftrace), 보안 취약점 사례와 성능 최적화까지 단계적으로 정리합니다.
전제 조건:네트워크 스택(Network Stack)과 네트워크 디바이스 드라이버 문서를 먼저 읽으세요.
BPF는 네트워킹 외에도 보안, 추적, 스케줄링 등 다양한 커널 기능을 확장하지만, 네트워크 경로를 먼저 이해하면 BPF의 동작 원리를 가장 빠르게 파악할 수 있습니다.
/* xdp_pass.c — 모든 패킷을 통과시키는 최소 프로그램 */#include<linux/bpf.h>#include<bpf/bpf_helpers.h>SEC("xdp")
intxdp_pass_func(structxdp_md *ctx)
{
return XDP_PASS;
}
char _license[] SEC("license") = "GPL";
로드 및 확인
# XDP 프로그램 로드 (generic 모드, 모든 드라이버 지원)
sudo ip link set dev eth0 xdpgeneric obj xdp_pass.o sec xdp
# XDP 프로그램 로드 (native 모드, 드라이버 지원 필요)
sudo ip link set dev eth0 xdp obj xdp_pass.o sec xdp
# 로드 확인
ip link show dev eth0
# ... xdp/id:42 ... 형태로 표시# bpftool로 상세 확인
sudo bpftool prog list
sudo bpftool prog show id 42
# XDP 프로그램 제거
sudo ip link set dev eth0 xdp off
XDP 모드 차이:
xdpgeneric — 소프트웨어 에뮬레이션, 모든 드라이버 지원, 성능 낮음 (개발/테스트용)
xdpdrv (native) — 드라이버 레벨 실행, 고성능 (프로덕션 권장)
xdpoffload — NIC 하드웨어에서 실행 (Netronome 등 지원 NIC 필요)
BPF Maps
맵 유형
설명
용도
BPF_MAP_TYPE_HASH
해시 테이블(Hash Table)
키-값 저장
BPF_MAP_TYPE_ARRAY
배열
인덱스 기반 접근
BPF_MAP_TYPE_PERCPU_HASH
Per-CPU 해시(Hash)
고속 카운터
BPF_MAP_TYPE_RINGBUF
링 버퍼(Ring Buffer)
이벤트 스트리밍
BPF_MAP_TYPE_LRU_HASH
LRU 해시
연결 추적(Connection Tracking)
BPF_MAP_TYPE_PROG_ARRAY
프로그램 배열
tail call
CO-RE (Compile Once, Run Everywhere)
CO-RE는 BPF 프로그램을 한 번 컴파일하여 다양한 커널 버전에서 실행할 수 있게 합니다. BTF(BPF Type Format)를 활용하여 구조체(Struct) 레이아웃 차이를 자동으로 조정합니다.
💡
bpftool prog list로 현재 로드된 BPF 프로그램을, bpftool map list로 BPF 맵을 확인할 수 있습니다. bpftool prog dump xlated id <ID>로 JIT 컴파일된 코드를 볼 수 있습니다.
BPF Verifier
BPF verifier는 프로그램 로드 시 안전성을 검증합니다. 모든 경로가 종료되는지, 메모리 범위를 벗어나지 않는지, 무한 루프가 없는지 등을 정적으로 분석합니다.
/* Verifier가 검증하는 주요 사항 *//* 1. 경계 검사: 모든 포인터 접근에 범위 확인 필요 */if (data + sizeof(structethhdr) > data_end)
return XDP_PASS; /* 이 검사 없으면 verifier 거부 *//* 2. 레지스터 상태 추적: R0-R10의 타입과 범위 *//* 3. 루프 제한: bounded loop만 허용 (5.3+) */for (int i = 0; i < 256; i++) { /* OK: bounded */if (condition) break;
}
/* 4. 명령어 수 제한: 최대 1M instructions (5.2+) *//* 5. 맵 접근: NULL 체크 필수 */__u64 *val = bpf_map_lookup_elem(&my_map, &key);
if (val) /* NULL 체크 없으면 verifier 거부 */
*val += 1;
/* 유저스페이스: libbpf 스켈레톤 활용 */#include"trace.skel.h"/* bpftool gen skeleton으로 생성 */intmain(void)
{
structtrace_bpf *skel;
skel = trace_bpf__open_and_load(); /* BPF 프로그램 로드 */trace_bpf__attach(skel); /* hook에 연결 *//* Ring buffer 폴링 */structring_buffer *rb;
rb = ring_buffer__new(
bpf_map__fd(skel->maps.events),
handle_event, NULL, NULL);
while (!exiting)
ring_buffer__poll(rb, 100); /* 100ms 타임아웃 */trace_bpf__destroy(skel);
}
TC (Traffic Control) BPF
TC BPF는 XDP보다 상위 계층에서 실행되며, 전체 sk_buff 구조체에 접근할 수 있어 더 풍부한 패킷 조작이 가능합니다.
# TC BPF 프로그램 로드
tc qdisc add dev eth0 clsact
tc filter add dev eth0 ingress bpf da obj my_prog.o sec tc_ingress
tc filter add dev eth0 egress bpf da obj my_prog.o sec tc_egress
# 확인
tc filter show dev eth0 ingress
bpftool prog list
⚠️
BPF 프로그램은 CAP_BPF (또는 CAP_SYS_ADMIN) 권한이 필요합니다. sysctl kernel.unprivileged_bpf_disabled=1이 설정된 환경에서는 root만 BPF 프로그램을 로드할 수 있습니다.
BPF/eBPF 주요 보안 취약점(Vulnerability) 사례
BPF/eBPF는 커널 내에서 사용자 정의 코드를 실행하는 강력한 메커니즘이지만, Verifier 우회, JIT 취약점, 투기적 실행(Speculative Execution) 등 다양한 공격 벡터가 존재합니다. 아래는 실제로 보고된 주요 보안 취약점 사례와 그 대응 방법입니다.
BPF Verifier 우회 취약점 (CVE-2021-3490)
심각도: 높음 (로컬 권한 상승) — ALU32 비트 연산에서 Verifier의 범위 추적(bound tracking) 결함으로 비특권 사용자가 커널 메모리에 임의 읽기/쓰기가 가능했습니다.
이 취약점은 BPF Verifier가 ALU32 연산에서 비트 연산(AND, OR, XOR)의 결과 범위를 잘못 계산하는 데서 발생합니다. Verifier는 각 레지스터(Register)의 가능한 값 범위를 tnum(tracked number) 구조체로 추적하는데, 비트 연산 후 tnum 값과 min/max 경계가 불일치하면 실제로는 불가능한 값 범위를 허용하게 됩니다.
/* 취약점을 유발하는 BPF 코드 패턴 (개념 예시) *//* Verifier가 r0의 범위를 [0, 1]로 추적하지만 *//* 실제 ALU32 비트 연산 후 범위가 확장될 수 있음 */BPF_ALU64_IMM(BPF_MOV, BPF_REG_0, 0),
BPF_ALU64_IMM(BPF_OR, BPF_REG_0, 0x7FFFFF00),
/* Verifier는 r0 ≤ 0x7FFFFFFF로 추적하지만 *//* tnum 분석과 범위 분석의 불일치로 *//* out-of-bounds 맵 접근이 허용됨 */BPF_ALU32_IMM(BPF_AND, BPF_REG_0, idx),
/* 이 시점에서 Verifier의 범위 계산 오류 발생 */BPF_STX_MEM(BPF_DW, BPF_REG_MAP, BPF_REG_0, 0),
/* 맵 경계 밖의 커널 메모리에 쓰기 가능 */
수정 패치(Patch)에서는 ALU 연산의 tnum 검증을 강화하여 비트 연산 후 tnum.value와 tnum.mask가 min/max 경계와 일관성을 유지하도록 했습니다.
권장 완화 조치: 비특권 사용자의 BPF 프로그램 로드를 차단하는 것이 가장 효과적입니다.
# 비특권 BPF 비활성화 (재부팅 시 초기화)sysctl -w kernel.unprivileged_bpf_disabled=1# 영구 설정 (/etc/sysctl.d/99-bpf.conf)
kernel.unprivileged_bpf_disabled = 1
BPF JIT Spraying (CVE-2020-8835)
심각도: 높음 (로컬 권한 상승) — JIT 컴파일된 BPF 코드에서 32비트와 64비트 레지스터 값 불일치를 악용하여 커널 메모리에 대한 범위 밖 접근이 가능했습니다.
이 취약점은 BPF JIT 컴파일러가 ALU32 연산을 처리할 때 32비트 값의 상위 32비트를 적절히 truncation하지 않아 발생합니다. Verifier는 32비트 연산의 결과를 올바르게 추적하지만, JIT가 생성한 네이티브 코드에서는 64비트 레지스터의 상위 비트가 보존되어 실제 값이 Verifier가 추적한 범위를 벗어나게 됩니다.
/* CVE-2020-8835: 32비트/64비트 불일치 악용 패턴 *//* 1단계: 64비트 레지스터에 큰 값 로드 */
r1 = 0x100000001/* 상위 32비트: 1, 하위 32비트: 1 *//* 2단계: ALU32 연산 — Verifier는 하위 32비트만 추적 */
w1 += 0/* 32비트 ADD: Verifier는 w1=1로 추적 *//* 그러나 JIT 코드는 상위 비트를 truncate하지 않음 *//* 실제 r1 = 0x100000001 (Verifier 추적 범위 초과) *//* 3단계: 이 불일치를 이용한 OOB 접근 */
r2 = map_ptr /* BPF 맵 포인터 */
r2 += r1 /* Verifier: map + 1 (안전) *//* 실제: map + 0x100000001 (OOB!) */
*(r2) = 0/* 커널 메모리 arbitrary write */
수정 패치에서는 JIT 컴파일러가 모든 ALU32 연산 후에 명시적으로 상위 32비트를 zero-extend하도록 하여 레지스터 값이 Verifier의 추적 범위와 일치하도록 했습니다.
CONFIG_BPF_JIT_ALWAYS_ON의 보안 영향: 이 옵션을 활성화하면 BPF 인터프리터가 비활성화되고 항상 JIT를 사용합니다. JIT는 성능에 유리하지만, JIT 관련 취약점이 발견되면 인터프리터로 fallback할 수 없습니다. 반면 인터프리터 방식은 Spectre 기반 공격에 더 취약할 수 있으므로, 보안 정책에 따라 신중하게 선택해야 합니다.
# JIT 상태 확인cat /proc/sys/net/core/bpf_jit_enable
# 0: 비활성화 (인터프리터)# 1: 활성화 (기본값, fallback 가능)# 2: 디버깅 모드 (JIT 이미지 덤프)# CONFIG_BPF_JIT_ALWAYS_ON=y 시 인터프리터 완전 제거# JIT 비활성화 불가 — 항상 네이티브 코드 실행
Spectre v1과 BPF (CVE-2019-7308)
심각도: 중간 (정보 누출) — BPF 프로그램에서 배열 인덱싱 시 투기적 실행(speculative execution)을 통해 커널 메모리 정보가 누출될 수 있었습니다.
Spectre v1(bounds check bypass) 공격은 CPU의 분기 예측(Branch Prediction)기를 훈련시켜, 배열 경계 검사가 완료되기 전에 투기적으로 범위 밖의 데이터를 로드하게 만듭니다. BPF 프로그램은 사용자가 작성한 코드를 커널 내에서 실행하므로, 투기적 실행의 영향을 받는 패턴을 쉽게 만들 수 있었습니다.
/* Spectre v1 공격 패턴 (BPF 맵 배열 접근) *//* 공격자가 제어하는 인덱스 값 */
r0 = *(u32 *)(r1 + offsetof(ctx, data)) /* 외부 입력 *//* Verifier는 이 분기를 확인하여 r0 < array_size 보장 */if r0 >= array_size goto out
/* 그러나 CPU는 투기적으로 분기를 무시하고 실행 *//* r0가 array_size를 초과하는 값으로 투기적 로드 */
r1 = map_base + r0 * 8/* 투기적 OOB 포인터 계산 */
r2 = *(u64 *)(r1) /* 투기적 OOB 로드 — 커널 메모리 읽기 *//* 캐시 사이드 채널을 통해 r2 값 추출 */
r3 = probe_array + r2 * 64/* 캐시 라인 크기 단위 접근 */
r4 = *(u64 *)(r3) /* 캐시에 r2 값의 흔적을 남김 */
수정 패치에서는 Verifier가 투기적 실행에 취약한 패턴을 탐지하여 lfence 명령(직렬화(Serialization) 배리어)을 삽입합니다. 또한 포인터 산술에 대한 BPF_ALU64_REG sanitization을 도입하여, 투기적 실행 경로에서도 포인터가 허용된 범위를 벗어나지 않도록 마스킹합니다.
/* 수정 후: Verifier가 삽입하는 방어 코드 */if r0 >= array_size goto out
/* Verifier가 자동 삽입하는 sanitization */
r0 &= array_mask /* 투기적 실행에서도 범위 제한 *//* x86에서는 lfence 삽입으로 투기적 실행 직렬화 */
r1 = map_base + r0 * 8/* 이제 투기적 OOB 불가 */
r2 = *(u64 *)(r1)
Spectre 완화 현황: 커널 5.x 이후 BPF Verifier는 투기적 실행 분석 패스를 내장하고 있으며, 포인터 연산에 대한 마스킹(sanitization)을 자동으로 수행합니다. 그러나 새로운 투기적 실행 변종이 지속적으로 발견되므로, 최신 커널 업데이트와 CPU 마이크로코드 업데이트를 병행해야 합니다.
BPF 맵 경쟁 조건(Race Condition) 사례
데이터 무결성(Integrity) 위험: BPF 맵은 커널과 사용자 공간, 또는 여러 CPU 간에 공유되는 데이터 구조이므로 동시 접근 시 경쟁 조건(race condition)이 발생할 수 있습니다.
BPF_MAP_TYPE_ARRAY는 고정 크기 배열로, 개별 요소에 대한 읽기/쓰기가 원자적(Atomic)이지 않습니다. 멀티 코어 환경에서 동일 요소를 동시에 업데이트하면 torn read/write가 발생하여 일부만 업데이트된 값을 읽을 수 있습니다. BPF_MAP_TYPE_HASH에서는 해시 테이블 resize 과정에서 element가 손실되거나 중복되는 문제가 보고되었습니다.
/* 문제: torn read/write가 발생하는 맵 접근 */structstats {
__u64 packets;
__u64 bytes;
};
/* CPU 0과 CPU 1이 동시에 같은 키의 stats를 업데이트 */structstats *val = bpf_map_lookup_elem(&stats_map, &key);
if (val) {
val->packets += 1; /* 비원자적: read-modify-write 경쟁 */
val->bytes += pkt_len; /* packets과 bytes 사이에 다른 CPU가 끼어들 수 있음 *//* 결과: 카운터 손실 또는 packets/bytes 불일치 */
}
bpf_spin_lock을 사용하면 맵 요소에 대한 동기화된 접근이 가능합니다. 커널 5.1부터 지원되며, 맵 value 구조체에 struct bpf_spin_lock 필드를 포함시켜야 합니다.
/* 해결책 2: per-CPU 맵으로 락 없는 병렬 접근 */struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 256);
__type(key, __u32);
__type(value, structstats);
} stats_percpu SEC(".maps");
/* 각 CPU가 독립적인 복사본에 접근 — 락 불필요 */structstats *val = bpf_map_lookup_elem(&stats_percpu, &key);
if (val) {
val->packets += 1; /* 경쟁 없음: CPU별 독립 메모리 */
val->bytes += pkt_len;
}
/* 사용자 공간에서 모든 CPU 값 합산 *//* bpf_map_lookup_elem()은 ncpus개의 값을 반환 */__u64 total_packets = 0;
for (int i = 0; i < ncpus; i++)
total_packets += percpu_vals[i].packets;
맵 유형별 동기화 전략:
BPF_MAP_TYPE_ARRAY / HASH — 공유 맵. 다중 CPU에서 동시 업데이트 시 bpf_spin_lock 필요
BPF_MAP_TYPE_PERCPU_ARRAY / PERCPU_HASH — CPU별 독립 복사본. 락 없이 안전하지만 사용자 공간에서 합산 필요
__sync_fetch_and_add() — 단순 카운터에 대해 원자적 증가. 구조체 전체 동기화에는 부적합
BPF_MAP_TYPE_HASH resize — 해시 맵의 동적 확장 중 요소 접근은 RCU로 보호되지만, 대량 삽입 시 성능 저하 주의
eBPF Verifier 상세
BPF Verifier는 커널에서 가장 복잡한 정적 분석기 중 하나입니다. 사용자 공간에서 제출한 BPF 바이트코드가 커널 내에서 안전하게 실행될 수 있는지 로드 시점에 검증합니다. 검증을 통과하지 못한 프로그램은 절대로 커널에서 실행되지 않습니다.
Verifier 아키텍처
Verifier는 추상 해석(abstract interpretation) 기법을 사용합니다. 프로그램의 모든 가능한 실행 경로를 탐색하면서 각 명령어 실행 후의 레지스터 상태를 추적합니다. 핵심 구조체는 struct bpf_verifier_env이며, 검증 과정은 크게 두 단계로 나뉩니다.
BPF Verifier: CFG 검증 → 상태 시뮬레이션 → Spectre 완화 → JIT 컴파일 순서로 진행
BPF 레지스터 규약
eBPF 가상 머신은 11개의 64비트 레지스터(R0~R10)와 512바이트 스택, 프로그램 카운터(PC)를 제공합니다. 이 규약은 호출 규약(calling convention)을 정의하며 BPF 프로그램 작성과 Verifier 이해의 기본이 됩니다.
레지스터
용도
특성
R0
반환값 / BPF 프로그램 종료 코드
헬퍼 함수 반환값, XDP 결과 코드(XDP_PASS 등)
R1
첫 번째 인자 / 컨텍스트 포인터
프로그램 진입 시 ctx (예: xdp_md *)
R2
두 번째 인자
헬퍼 함수 호출 시 인자
R3
세 번째 인자
헬퍼 함수 호출 시 인자
R4
네 번째 인자
헬퍼 함수 호출 시 인자
R5
다섯 번째 인자
헬퍼 함수 호출 시 인자
R6
callee-saved
함수 호출 후 값 보존
R7
callee-saved
함수 호출 후 값 보존
R8
callee-saved
함수 호출 후 값 보존
R9
callee-saved
함수 호출 후 값 보존
R10
스택 프레임(Stack Frame) 포인터 (FP)
읽기 전용(Read-Only) — 수정 불가
호출 규약 핵심:
R1~R5는 scratch 레지스터입니다. 헬퍼 함수 호출 후 값이 파괴됩니다.
R6~R9는 callee-saved입니다. 헬퍼 호출 전후 값이 보존됩니다.
BPF 프로그램 진입 시 R1 = ctx, R10 = FP이며 나머지는 미초기화 상태입니다.
스택은 R10을 기준으로 음수 오프셋(Offset)(R10 - 8, R10 - 16 …)으로 접근하며 최대 512바이트입니다.
# bpftool로 BPF 프로그램의 명령어 덤프 (JIT 전)
$ sudo bpftool prog dump xlated id 42
0: (b7) r0 = 2 # MOV r0, 2 (XDP_PASS)
1: (95) exit # EXIT# JIT 컴파일된 네이티브 코드 확인
$ sudo bpftool prog dump jited id 42
0: mov $0x2,%eax
5: retq
레지스터 상태 추적
Verifier는 R0~R10 총 11개 레지스터의 상태를 명령어마다 추적합니다. 각 레지스터는 타입(enum bpf_reg_type)과 값 범위(struct tnum — tracked number)를 가지며, 분기 조건에 따라 상태가 정제(refine)됩니다.
/* kernel/bpf/verifier.c — 레지스터 상태 구조체 간소화 */structbpf_reg_state {
enumbpf_reg_type type; /* PTR_TO_MAP_VALUE, SCALAR_VALUE 등 */structtnum var_off; /* 비트 단위 알려진 값 추적 */s64 smin_value; /* 부호 있는 최소값 */s64 smax_value; /* 부호 있는 최대값 */u64 umin_value; /* 부호 없는 최소값 */u64 umax_value; /* 부호 없는 최대값 */s32 s32_min_value; /* 32비트 부호 있는 최소 */s32 s32_max_value; /* 32비트 부호 있는 최대 */u32 u32_min_value; /* 32비트 부호 없는 최소 */u32 u32_max_value; /* 32비트 부호 없는 최대 */
};
/* tnum: 비트 단위 추적으로 정밀한 범위 분석 */structtnum {
u64 value; /* 확실히 1인 비트 */u64 mask; /* 알 수 없는 비트 (1=unknown) */
};
/* 예: value=0x10, mask=0xFF → 값은 0x10~0x1F 또는 0x10|0x00~0xFF 범위 */
코드 설명
3행bpf_reg_type은 레지스터가 가리키는 대상의 종류를 나타냅니다. SCALAR_VALUE(일반 정수), PTR_TO_MAP_VALUE(맵 값 포인터), PTR_TO_CTX(프로그램 컨텍스트) 등 약 30가지 타입이 있으며, Verifier는 각 명령어 실행 후 타입 전이 규칙을 적용합니다.
4행struct tnum의 var_off는 비트 단위로 알려진 값을 추적합니다. AND/OR/SHIFT 같은 비트 연산 후에도 가능한 값 범위를 정밀하게 유지할 수 있어, 단순 min/max 범위 추적보다 훨씬 정확합니다.
5-10행64비트와 32비트의 부호 있는/없는 범위를 각각 별도로 추적합니다. CVE-2020-8835는 32비트와 64비트 범위의 불일치를 악용한 사례로, 이후 커널에서 32비트 범위 필드(s32_min/max, u32_min/max)가 추가되었습니다.
16-19행tnum의 value는 확실히 1인 비트, mask는 아직 결정되지 않은 비트입니다. 두 tnum의 AND 연산 시 mask 비트끼리의 조합으로 가능한 범위를 계산하며, 이 추적 덕분에 val & 0xF 후 배열 인덱스로 사용하는 것이 안전함을 증명할 수 있습니다.
루프 검증 (Bounded Loops)
커널 5.3부터 Verifier는 bounded loop를 허용합니다. 루프가 허용되려면 다음 조건을 모두 만족해야 합니다:
루프 변수가 매 반복마다 단조 증가 또는 단조 감소해야 합니다
종료 조건이 정적으로 결정 가능하거나 Verifier가 반복 횟수의 상한을 추론할 수 있어야 합니다
루프 본문이 Verifier 복잡도 예산 내에서 완전히 펼쳐(unroll)질 수 있어야 합니다
/* OK: Verifier가 허용하는 bounded loop */for (int i = 0; i < 256; i++) {
if (data + i >= data_end)
break;
/* 패킷 데이터 처리 */
}
/* OK: bpf_loop() 헬퍼 (커널 5.17+) — 콜백 기반 루프 */staticlongloop_callback(u32 index, void *ctx)
{
/* 루프 본문 */return0; /* 0=계속, 1=중단 */
}
bpf_loop(1000, loop_callback, &ctx, 0);
/* FAIL: Verifier가 거부하는 패턴 */while (ptr != NULL) { /* ← 상한을 알 수 없음 */
ptr = ptr->next;
}
맵 접근 검증
bpf_map_lookup_elem() 반환값은 PTR_TO_MAP_VALUE_OR_NULL 타입입니다. Verifier는 이 포인터를 사용하기 전에 반드시 NULL 체크가 있는지 확인하며, 체크 후에는 PTR_TO_MAP_VALUE로 타입을 정제합니다. 맵 값에 대한 접근은 value_size 범위 내로 제한됩니다.
/* Verifier의 맵 접근 검증 과정 */__u32 key = 0;
structdata *val = bpf_map_lookup_elem(&my_map, &key);
/* 여기서 val의 타입: PTR_TO_MAP_VALUE_OR_NULL *//* ❌ Verifier 거부: NULL일 수 있는 포인터 역참조 */// val->count++; // R0 invalid mem access 'map_value_or_null' */if (!val)
return0;
/* ✅ 여기서 val의 타입: PTR_TO_MAP_VALUE (NULL 아님 확정) */
val->count++; /* offset 0, value_size 내 → OK *//* ❌ Verifier 거부: value_size 초과 접근 */// *((char *)val + 4096) = 0; // invalid access to map value */
코드 설명
3행bpf_map_lookup_elem()은 커널 내부에서 맵 타입별 map->ops->map_lookup_elem()을 호출합니다. 반환값의 Verifier 타입은 PTR_TO_MAP_VALUE_OR_NULL로 설정되며, 이는 사용 전에 반드시 NULL 검사를 요구합니다.
4행Verifier는 NULL 가능 포인터에 대한 역참조를 감지하면 즉시 R0 invalid mem access 'map_value_or_null' 에러를 발생시킵니다. 이는 커널 메모리 안전성의 핵심 보장으로, 맵에 키가 없을 때 커널 패닉을 방지합니다.
8-9행NULL 체크 분기 이후 Verifier는 mark_ptr_not_null_reg()를 호출하여 타입을 PTR_TO_MAP_VALUE로 정제합니다. 이 시점부터 off 필드와 value_size를 비교하여 범위 내 접근만 허용합니다.
12행맵 값 접근 시 Verifier는 check_mem_access()에서 off + size <= map->value_size를 검증합니다. 이 범위를 초과하면 인접 메모리를 읽을 수 있으므로, 커널은 이를 엄격히 차단합니다.
Verifier 로그 읽기
프로그램이 검증에 실패하면 Verifier는 상세한 로그를 출력합니다. 이 로그를 읽는 능력은 BPF 개발에서 매우 중요합니다.
# Verifier 로그 확인 방법# 1. bpftool로 상세 로그 출력
bpftool prog load prog.o /sys/fs/bpf/prog -d 2>&1 | head -100
# 2. libbpf 로그 레벨 조정# libbpf_set_print(my_print_fn) 또는 LIBBPF_LOG_LEVEL=debug# Verifier 로그 출력 예시:# func#0 @0# 0: (b7) r1 = 0 ; R1_w=0# 1: (63) *(u32 *)(r10 -4) = r1 ; R1_w=0 R10=fp0# 2: (bf) r2 = r10 ; R2_w=fp0 R10=fp0# 3: (07) r2 += -4 ; R2_w=fp-4# 4: (18) r1 = 0xffff888012345678 ; R1_w=map_ptr(id=0,...)# 6: (85) call bpf_map_lookup_elem#1# 7: (15) if r0 == 0x0 goto pc+3 ; R0_w=map_value_or_null# 8: (61) r1 = *(u32 *)(r0 +0) ; R0_w=map_value R1_w=scalar# ...# 로그에서 R#_w= 은 레지스터의 현재 타입과 추적 범위를 표시
Verifier 로그 핵심 패턴:
R0=scalar(id=X,umax=Y) — 레지스터가 스칼라 값이고 최대값이 Y
R1=map_value(off=0,ks=4,vs=16) — 맵 값 포인터, key 4바이트, value 16바이트
invalid mem access 'map_value_or_null' — NULL 체크 누락
back-edge from insn X to Y — 루프 탐지, bounded 여부 확인
processed N insns ... complexity limit — 복잡도 초과, 프로그램 단순화 필요
BPF 맵 타입별 상세
BPF 맵은 커널과 유저스페이스, 또는 BPF 프로그램 간에 데이터를 공유하는 핵심 메커니즘입니다. 용도에 따라 적절한 맵 타입을 선택하는 것이 성능과 정확성에 직접적인 영향을 줍니다.
맵 타입
자료구조
키 조회
동시성
주요 용도
성능 특성
HASH
해시 테이블
O(1) 평균
RCU + spin_lock
연결 추적, 세션 테이블
동적 크기, 충돌 시 체이닝
ARRAY
고정 배열
O(1)
원자적 아님
설정, 전역 카운터
사전 할당, 삭제 불가
PERCPU_HASH
CPU별 해시
O(1) 평균
CPU별 독립
고속 통계, 플로우 카운터
락 없음, CPU수 x value 메모리
PERCPU_ARRAY
CPU별 배열
O(1)
CPU별 독립
per-CPU 카운터, 히스토그램
락 없음, 사용자 공간에서 합산
LRU_HASH
LRU 해시
O(1) 평균
내부 LRU 락
캐시(Cache), 연결 테이블 (자동 퇴거)
max_entries 초과 시 LRU 제거
LPM_TRIE
Longest Prefix Match
O(key bits)
spin_lock
IP 주소 매칭, 라우팅
CIDR 기반 검색
RINGBUF
링 버퍼
FIFO
lock-free MPSC
이벤트 스트리밍
perf_buffer 대비 메모리 효율
STACK_TRACE
스택 트레이스
ID 기반
해시 기반
프로파일링(Profiling), 콜 그래프
bpf_get_stackid()로 저장
QUEUE / STACK
큐/스택
FIFO/LIFO
spin_lock
작업 큐(Workqueue), 이벤트 버퍼링
키 없음, push/pop/peek
CGROUP_STORAGE
cgroup별 저장소
자동 키
cgroup별 독립
cgroup별 정책, 제한
cgroup attach 시 자동 할당
STRUCT_OPS
구조체 콜백(Callback)
—
등록 기반
TCP 혼잡 제어(Congestion Control), 스케줄러(Scheduler)
커널 ops 테이블 교체
/* 주요 맵 타입 선언 예시 *//* 1. LRU_HASH: 자동 퇴거되는 연결 추적 테이블 */struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH);
__uint(max_entries, 100000);
__type(key, structflow_key); /* src_ip, dst_ip, proto, ports */__type(value, structflow_stats);
} conn_table SEC(".maps");
/* 2. LPM_TRIE: CIDR 기반 IP 매칭 */struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__uint(max_entries, 10000);
__uint(map_flags, BPF_F_NO_PREALLOC);
__type(key, structbpf_lpm_trie_key_u8); /* prefixlen + data */__type(value, __u32);
} ip_blocklist SEC(".maps");
/* 3. STACK_TRACE: 프로파일링용 스택 저장 */struct {
__uint(type, BPF_MAP_TYPE_STACK_TRACE);
__uint(max_entries, 8192);
__uint(key_size, sizeof(__u32));
__uint(value_size, 100 * sizeof(__u64));
} stackmap SEC(".maps");
/* 4. QUEUE: 키 없는 FIFO */struct {
__uint(type, BPF_MAP_TYPE_QUEUE);
__uint(max_entries, 1024);
__type(value, structevent);
} event_queue SEC(".maps");
/* QUEUE 사용법 */structevent e = { .pid = pid, .ts = ts };
bpf_map_push_elem(&event_queue, &e, BPF_ANY); /* enqueue *//* 사용자 공간에서: bpf_map_lookup_and_delete_elem() → dequeue */
코드 설명
3-9행BPF_MAP_TYPE_LRU_HASH는 커널 내부에서 per-CPU 로컬 freelist와 글로벌 LRU 리스트를 유지합니다. max_entries 초과 시 LRU 리스트 끝의 엔트리를 자동 퇴거하며, per-CPU 캐싱 덕분에 대부분의 경우 글로벌 락 없이 삽입/조회가 가능합니다.
11-18행BPF_MAP_TYPE_LPM_TRIE는 커널에서 비트 단위 trie 구조로 구현됩니다. BPF_F_NO_PREALLOC 플래그는 동적 메모리 할당을 사용하라는 의미로, CIDR 블록 수가 가변적인 IP 블랙리스트에 적합합니다. 다만 동적 할당은 데이터 경로에서 지연 스파이크를 유발할 수 있습니다.
28-37행BPF_MAP_TYPE_QUEUE는 커널에서 순환 버퍼로 구현된 FIFO로, 키가 없이 순서대로 삽입/추출합니다. bpf_map_push_elem()은 프로듀서, 사용자 공간의 bpf_map_lookup_and_delete_elem()은 컨슈머 역할을 하며, BPF→유저스페이스 이벤트 전달에 RINGBUF의 대안으로 사용됩니다.
맵 타입 선택 가이드:
고속 카운터 → PERCPU_ARRAY (락 없이 CPU별 독립 증가, 사용자 공간에서 합산)
연결 추적 → LRU_HASH (메모리 한도 초과 시 자동 퇴거, prealloc 주의)
IP 블랙리스트 → LPM_TRIE (CIDR 서브넷 단위 매칭)
이벤트 전송 → RINGBUF (가변 크기 이벤트, 공유 버퍼로 메모리 효율)
TCP 혼잡 제어 → STRUCT_OPS (커널의 tcp_congestion_ops 교체)
CO-RE (Compile Once, Run Everywhere) 상세
CO-RE는 BPF 프로그램의 이식성 문제를 해결하는 핵심 기술입니다. 커널 구조체의 레이아웃은 버전마다 달라질 수 있는데, CO-RE는 BTF(BPF Type Format) 정보를 활용하여 컴파일 시점의 구조체 오프셋을 실행 시점에 자동으로 재조정(relocate)합니다.
BTF (BPF Type Format)
BTF는 BPF 프로그램과 커널의 타입 정보를 컴팩트하게 인코딩하는 형식입니다. DWARF 디버그 정보를 축소한 것으로, 커널 이미지에 포함되어 /sys/kernel/btf/vmlinux에서 접근할 수 있습니다.
# BTF 지원 확인
ls -la /sys/kernel/btf/vmlinux
# 파일이 있으면 BTF 지원 커널# vmlinux.h 생성 — 모든 커널 타입 정의를 담은 헤더
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 약 10만 줄 이상의 C 헤더 생성# 이 헤더만 include하면 커널 헤더 의존성 제거# 특정 구조체의 BTF 정보 확인
bpftool btf dump file /sys/kernel/btf/vmlinux | grep -A 20 "struct task_struct"
# 커널 모듈의 BTF 확인
bpftool btf dump file /sys/kernel/btf/nf_conntrack
bpf_core_read 매크로(Macro)와 재배치(Relocation)
BPF_CORE_READ() 매크로는 컴파일 시점에 CO-RE 재배치(relocation) 정보를 생성합니다. libbpf가 프로그램을 로드할 때, 컴파일 시점의 오프셋을 실행 중인 커널의 BTF와 비교하여 자동으로 수정합니다.
#include"vmlinux.h"#include<bpf/bpf_helpers.h>#include<bpf/bpf_core_read.h>/* CO-RE 방식: 필드 오프셋이 자동 재배치됨 */SEC("tp/sched/sched_process_exec")
inttrace_exec(void *ctx)
{
structtask_struct *task = (structtask_struct *)bpf_get_current_task();
/* BPF_CORE_READ: 중첩 포인터를 안전하게 따라감 */pid_t pid = BPF_CORE_READ(task, tgid);
pid_t ppid = BPF_CORE_READ(task, real_parent, tgid);
/* 구조체 필드 존재 여부 런타임 확인 */if (bpf_core_field_exists(task->loginuid)) {
uid_t loginuid = BPF_CORE_READ(task, loginuid.val);
}
/* 필드 크기 확인 (커널 버전별 다를 수 있음) */if (bpf_core_field_size(task->comm) == 16) {
/* TASK_COMM_LEN == 16인 커널 */
}
return0;
}
/* struct flavor: 커널 버전별로 다른 구조체 레이아웃 대응 */structtask_struct___old { /* ___old 접미사 = flavor */int prio;
int static_prio;
/* 이전 커널에서 다른 위치에 있던 필드 */
};
/* bpf_core_type_matches()로 어떤 flavor인지 런타임 판별 */if (bpf_core_type_matches(structtask_struct___old)) {
/* 이전 구조체 레이아웃 사용 */
}
CO-RE 워크플로
# CO-RE 개발 워크플로# 1. vmlinux.h 생성 (개발 머신에서 한 번)
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 2. BPF 프로그램 컴파일 (CO-RE 활성화)
clang -g -O2 -target bpf \
-D__TARGET_ARCH_x86 \
-c prog.bpf.c -o prog.bpf.o
# 3. 스켈레톤 생성
bpftool gen skeleton prog.bpf.o > prog.skel.h
# 4. 사용자 프로그램 컴파일
gcc -g -O2 -o prog prog.c -lbpf -lelf -lz
# 이 바이너리를 다른 커널 버전 시스템에 복사해도 동작!# libbpf가 로드 시 BTF 재배치를 자동 수행
libbpf 프로그래밍 상세
libbpf는 BPF 프로그램의 로드, 검증, 어태치를 관리하는 공식 사용자 공간 라이브러리입니다. 스켈레톤(skeleton) API는 BPF 프로그램의 맵, 프로그램, 전역 변수에 대한 타입 안전한 접근을 제공합니다.
Skeleton API
/* === BPF 측 (prog.bpf.c) === */#include"vmlinux.h"#include<bpf/bpf_helpers.h>/* 전역 변수: 사용자 공간에서 직접 읽기/쓰기 가능 */const volatilepid_t target_pid = 0; /* .rodata → 로드 시 설정 */__u64 event_count = 0; /* .bss → 런타임 업데이트 */struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} events SEC(".maps");
SEC("tp/sched/sched_process_exec")
inthandle_exec(void *ctx)
{
pid_t pid = bpf_get_current_pid_tgid() >> 32;
if (target_pid && pid != target_pid)
return0;
__sync_fetch_and_add(&event_count, 1);
/* ... 이벤트 전송 ... */return0;
}
/* === 사용자 측 (prog.c) === */#include"prog.skel.h"/* bpftool gen skeleton으로 생성 */intmain(int argc, char **argv)
{
structprog_bpf *skel;
/* 1단계: open — ELF 파싱, 맵/프로그램 탐색 */
skel = prog_bpf__open();
if (!skel) return1;
/* 2단계: 로드 전 설정 — 전역 변수, 맵 크기 조정 */
skel->rodata->target_pid = atoi(argv[1]);
bpf_map__set_max_entries(skel->maps.events, 512 * 1024);
/* 3단계: load — verifier 검증, JIT 컴파일, 맵 생성 */if (prog_bpf__load(skel)) {
fprintf(stderr, "BPF load failed\n");
goto cleanup;
}
/* 4단계: attach — hook point에 연결 */if (prog_bpf__attach(skel)) {
fprintf(stderr, "BPF attach failed\n");
goto cleanup;
}
/* 5단계: 이벤트 수신 (ring buffer) */structring_buffer *rb = ring_buffer__new(
bpf_map__fd(skel->maps.events),
handle_event, NULL, NULL);
while (!exiting) {
ring_buffer__poll(rb, 100);
/* 전역 변수 읽기 (mmap 기반) */printf("events: %llu\n", skel->bss->event_count);
}
cleanup:
ring_buffer__free(rb);
prog_bpf__destroy(skel);
return0;
}
프로그램 타입별 Attach
/* libbpf 수동 attach 예시 *//* kprobe — 커널 함수 진입점 */structbpf_link *link = bpf_program__attach_kprobe(
skel->progs.my_kprobe, false, "tcp_v4_connect");
/* tracepoint — 정적 트레이스포인트 */
link = bpf_program__attach_tracepoint(
skel->progs.my_tp, "net", "net_dev_xmit");
/* perf event — 하드웨어/소프트웨어 이벤트 */
link = bpf_program__attach_perf_event(
skel->progs.my_perf, pmu_fd);
/* XDP — 네트워크 인터페이스 */
link = bpf_program__attach_xdp(
skel->progs.my_xdp, ifindex);
/* cgroup — cgroup 경로 기반 */
link = bpf_program__attach_cgroup(
skel->progs.my_cgroup, cgroup_fd);
/* LSM — 보안 모듈 hook *//* SEC("lsm/bprm_check_security") → 자동 attach *//* struct_ops — 커널 ops 테이블 교체 */
link = bpf_map__attach_struct_ops(skel->maps.my_tcp_cc);
Perf Buffer vs Ring Buffer
특성
Perf Buffer
Ring Buffer
메모리 모델
CPU별 독립 버퍼
전체 공유 버퍼
메모리 효율
낮음 (CPU수 x 크기)
높음 (단일 공유)
이벤트 순서
CPU별 순서만 보장
전역 순서 보장(Ordering)
가변 크기
제한적
네이티브 지원
API
bpf_perf_event_output()
bpf_ringbuf_reserve/submit()
오버헤드
데이터 복사 1회
제로카피 가능 (reserve/submit)
커널 버전
4.4+
5.8+
BPF 프로그램 타입
BPF 프로그램은 타입에 따라 커널의 특정 hook point에 부착됩니다. 각 타입은 컨텍스트 구조체, 사용 가능한 헬퍼 함수, 반환값의 의미가 다릅니다.
프로그램 타입
SEC() 이름
컨텍스트
부착 위치
주요 용도
BPF_PROG_TYPE_XDP
xdp
xdp_md
NIC 드라이버
고속 패킷 필터링/포워딩
BPF_PROG_TYPE_SCHED_CLS
tc
__sk_buff
TC ingress/egress
패킷 분류, 수정, 폴리싱
BPF_PROG_TYPE_KPROBE
kprobe/func
pt_regs
커널 함수 진입/반환
동적 트레이싱
BPF_PROG_TYPE_TRACEPOINT
tp/cat/name
트레이스포인트별
정적 트레이스포인트
안정적 커널 이벤트 관찰
BPF_PROG_TYPE_RAW_TRACEPOINT
raw_tp/name
bpf_raw_tracepoint_args
원시 트레이스포인트
오버헤드 최소화 트레이싱
BPF_PROG_TYPE_PERF_EVENT
perf_event
bpf_perf_event_data
PMU 이벤트
CPU 프로파일링, 샘플링
BPF_PROG_TYPE_CGROUP_SKB
cgroup_skb/ingress
__sk_buff
cgroup ingress/egress
cgroup별 네트워크 정책
BPF_PROG_TYPE_SOCKET_FILTER
socket
__sk_buff
소켓
패킷 캡처 필터링
BPF_PROG_TYPE_STRUCT_OPS
struct_ops/name
ops별
커널 ops 테이블
TCP CC, 스케줄러 교체
BPF_PROG_TYPE_LSM
lsm/hook
hook별
LSM hook
동적 보안 정책
BPF_PROG_TYPE_TRACING
fentry/func
함수 인자 직접
커널 함수 (BTF 기반)
kprobe 대체 (오버헤드 낮음)
BPF_PROG_TYPE_SK_LOOKUP
sk_lookup
bpf_sk_lookup
소켓 검색
커스텀 소켓 바인딩
/* 주요 프로그램 타입 예시 *//* fentry/fexit — kprobe보다 오버헤드가 낮은 BTF 기반 트레이싱 */SEC("fentry/tcp_v4_connect")
intBPF_PROG(trace_tcp_connect, structsock *sk,
structsockaddr *uaddr, int addr_len)
{
/* BTF 기반: 인자를 직접 타입으로 받음 (pt_regs 불필요) */__u16 dport = sk->__sk_common.skc_dport;
bpf_printk("connecting to port %d\n", ntohs(dport));
return0;
}
/* struct_ops — TCP 혼잡 제어 알고리즘 BPF로 구현 */SEC("struct_ops/my_cong_init")
voidBPF_PROG(my_cong_init, structsock *sk)
{
structtcp_sock *tp = tcp_sk(sk);
tp->snd_cwnd = 10; /* 초기 혼잡 윈도우 */
}
SEC(".struct_ops")
structtcp_congestion_ops my_bpf_cc = {
.init = (void *)my_cong_init,
.ssthresh = (void *)my_ssthresh,
.cong_avoid = (void *)my_cong_avoid,
.name = "bpf_my_cc",
};
/* LSM — 동적 보안 정책 */SEC("lsm/bprm_check_security")
intBPF_PROG(restrict_exec, structlinux_binprm *bprm)
{
/* 특정 바이너리의 실행을 차단 */const char *filename = BPF_CORE_READ(bprm, filename);
/* 정책에 따라 -EPERM 반환으로 차단 */return0; /* 0=허용, 음수=차단 */
}
BPF 헬퍼 함수 상세
BPF 헬퍼 함수는 BPF 프로그램이 커널 기능에 접근하는 유일한 인터페이스입니다. 각 프로그램 타입마다 사용 가능한 헬퍼가 다르며, Verifier가 컴파일 시점에 이를 강제합니다.
맵 조작 헬퍼
/* 맵 조작 — 모든 프로그램 타입에서 사용 가능 *//* 조회: 키에 해당하는 값의 포인터 반환 (NULL 가능) */void *bpf_map_lookup_elem(structbpf_map *map, const void *key);
/* 갱신: flags로 동작 제어 */longbpf_map_update_elem(structbpf_map *map,
const void *key, const void *value, __u64 flags);
/* flags: BPF_ANY(0)=삽입 또는 갱신 BPF_NOEXIST(1)=없을 때만 삽입 BPF_EXIST(2)=있을 때만 갱신 *//* 삭제: 해시 맵 전용 (배열은 삭제 불가) */longbpf_map_delete_elem(structbpf_map *map, const void *key);
/* 원자적 갱신: per-CPU가 아닌 맵에서 카운터 증가 */__u64 *val = bpf_map_lookup_elem(&counter_map, &key);
if (val)
__sync_fetch_and_add(val, 1); /* 원자적 증가 */
메모리 읽기와 트레이싱 헬퍼
/* 커널 메모리 안전 읽기 (kprobe, tracepoint, fentry) */longbpf_probe_read_kernel(void *dst, __u32 size, const void *unsafe_ptr);
longbpf_probe_read_kernel_str(void *dst, __u32 size, const void *unsafe_ptr);
/* 사용자 공간 메모리 읽기 */longbpf_probe_read_user(void *dst, __u32 size, const void *unsafe_ptr);
longbpf_probe_read_user_str(void *dst, __u32 size, const void *unsafe_ptr);
/* 프로세스 정보 */__u64bpf_get_current_pid_tgid(void); /* 상위 32비트=tgid, 하위=pid */__u64bpf_get_current_uid_gid(void); /* 상위 32비트=gid, 하위=uid */longbpf_get_current_comm(void *buf, __u32 size);
structtask_struct *bpf_get_current_task(void);
/* 타임스탬프 */__u64bpf_ktime_get_ns(void); /* 부팅 후 나노초 */__u64bpf_ktime_get_boot_ns(void); /* 슬립 포함 나노초 *//* 디버그 출력 — /sys/kernel/debug/tracing/trace_pipe에서 확인 */longbpf_trace_printk(const char *fmt, __u32 fmt_size, ...);
/* 최대 3개 인자, %d/%u/%x/%lld/%llu/%llx/%p/%s 지원 *//* 프로덕션에서는 사용 금지 — 성능 오버헤드 큼 */
네트워크 헬퍼
/* XDP/TC 전용 네트워크 헬퍼 *//* 패킷 리다이렉트 */longbpf_redirect(__u32 ifindex, __u64 flags);
longbpf_redirect_map(structbpf_map *map, __u32 key, __u64 flags);
/* 패킷 데이터 수정 (TC) */longbpf_skb_store_bytes(struct__sk_buff *skb,
__u32 offset, const void *from, __u32 len, __u64 flags);
/* L3/L4 체크섬 재계산 */longbpf_l3_csum_replace(struct__sk_buff *skb,
__u32 offset, __u64 from, __u64 to, __u64 flags);
longbpf_l4_csum_replace(struct__sk_buff *skb,
__u32 offset, __u64 from, __u64 to, __u64 flags);
/* 패킷 크기 조정 */longbpf_skb_change_head(struct__sk_buff *skb, __u32 len, __u64 flags);
longbpf_skb_change_tail(struct__sk_buff *skb, __u32 len, __u64 flags);
/* XDP 패킷 크기 조정 */longbpf_xdp_adjust_head(structxdp_md *xdp, int delta);
longbpf_xdp_adjust_tail(structxdp_md *xdp, int delta);
/* FIB (라우팅 테이블) 조회 */longbpf_fib_lookup(void *ctx, structbpf_fib_lookup *params,
int plen, __u32 flags);
/* XDP/TC에서 커널 라우팅 테이블을 직접 조회하여 L2 헤더 완성 */
BPF 개발 도구
BPF 프로그램 개발, 디버깅, 관리를 위한 도구 생태계는 빠르게 성장하고 있습니다. 기본 도구인 bpftool부터 고수준 프레임워크까지 용도에 따라 적절한 도구를 선택합니다.
bpftool
bpftool은 BPF 프로그램과 맵을 관리하는 공식 CLI 도구입니다. 커널 소스 트리의 tools/bpf/bpftool/에서 빌드됩니다.
# 프로그램 관리
bpftool prog list # 로드된 BPF 프로그램 목록
bpftool prog show id 42 # 특정 프로그램 상세 정보
bpftool prog dump xlated id 42 # BPF 바이트코드 덤프
bpftool prog dump jited id 42 # JIT 네이티브 코드 덤프
bpftool prog dump xlated id 42 visual # DOT 그래프 (graphviz용)# 맵 관리
bpftool map list # BPF 맵 목록
bpftool map dump id 5 # 맵 전체 내용 덤프
bpftool map lookup id 5 key 0x01 0x00 0x00 0x00 # 특정 키 조회
bpftool map update id 5 key 1 0 0 0 value 100 0 0 0 # 값 갱신# BTF 관리
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
bpftool btf dump id 1 # BTF 정보 덤프# 스켈레톤 생성
bpftool gen skeleton prog.bpf.o > prog.skel.h
# 프로그램 pin/unpin (BPF 파일시스템)
bpftool prog pin id 42 /sys/fs/bpf/my_prog
bpftool prog load prog.o /sys/fs/bpf/my_prog type xdp
# 네트워크 관련
bpftool net list # XDP/TC에 부착된 프로그램 목록# 기능 지원 확인
bpftool feature probe # 현재 커널의 BPF 기능 점검
bpftrace
bpftrace는 AWK와 유사한 고수준 트레이싱 언어로, 한 줄짜리 스크립트로 강력한 커널 분석이 가능합니다.
// cilium/ebpf (Go) 사용 예시package main
import (
"github.com/cilium/ebpf""github.com/cilium/ebpf/link"
)
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go counter counter.bpf.cfuncmain() {
objs := counterObjects{}
if err := loadCounterObjects(&objs, nil); err != nil {
log.Fatal(err)
}
defer objs.Close()
// kprobe에 BPF 프로그램 부착
kp, _ := link.Kprobe("tcp_v4_connect", objs.TraceTcpConnect, nil)
defer kp.Close()
// 맵에서 값 읽기var count uint64
objs.CounterMap.Lookup(uint32(0), &count)
}
// Aya (Rust) 사용 예시use aya::{Bpf, programs::Xdp, maps::HashMap};
use aya::programs::XdpFlags;
fnmain() -> Result<(), Box<dyn std::error::Error>> {
letmut bpf = Bpf::load_file("prog.o")?;
// XDP 프로그램 로드 및 부착let program: &mutXdp = bpf.program_mut("xdp_prog")?
.try_into()?;
program.load()?;
program.attach("eth0", XdpFlags::default())?;
// 맵 접근let blocklist: HashMap<_, u32, u32> =
HashMap::try_from(bpf.map_mut("blocklist")?)?;
blocklist.insert(167772161, 1, 0)?; // 10.0.0.1 차단Ok(())
}
도구 선택 가이드:
빠른 분석/디버깅 → bpftrace (한 줄 스크립트)
프로덕션 C 프로그램 → libbpf + CO-RE (최고의 이식성과 성능)
Kubernetes/클라우드 → cilium/ebpf (Go, Cilium 생태계)
시스템 도구 (Rust) → Aya (순수 Rust, 안전성)
프로토타이핑/교육 → bcc (Python, 풍부한 예제)
BPF 관리/검사 → bpftool (공식 CLI)
BPF 검증기 — 상태 탐색과 Pruning
BPF Verifier는 프로그램의 모든 실행 경로를 추상 해석(abstract interpretation)으로 탐색합니다. 각 분기점에서 레지스터의 타입(type)과 값 범위(range)를 추적하며, 이미 검증된 상태와 동일한 상태에 도달하면 가지치기(pruning)로 중복 탐색을 제거합니다. 이 메커니즘이 없으면 명령어 100만 개 제한 내에서 실제 프로그램 대부분을 검증할 수 없습니다.
BPF Verifier가 분기점마다 레지스터 상태를 추적하고, pruning으로 중복 경로를 제거하는 과정
상태 추적 구조체
Verifier는 각 명령어 실행 후의 레지스터 상태를 struct bpf_reg_state로 추적합니다. 핵심 필드를 살펴보면:
/* kernel/bpf/verifier.c — Verifier 레지스터/프레임 상태 간소화 */structbpf_reg_state {
enumbpf_reg_typetype; /* PTR_TO_MAP_VALUE, SCALAR_VALUE 등 */structtnum var_off; /* 비트 단위 known/unknown 마스크 */s64 smin_value, smax_value; /* 부호 있는 최솟값/최댓값 */u64 umin_value, umax_value; /* 부호 없는 최솟값/최댓값 */s32 s32_min_value, s32_max_value;
u32 u32_min_value, u32_max_value;
s64 off; /* 포인터 오프셋 */u32 id; /* 조건부 NULL 검사 연결 ID */
};
/* 프레임 상태: 레지스터 + 스택 슬롯 */structbpf_func_state {
structbpf_reg_state regs[MAX_BPF_REG]; /* R0-R10 */structbpf_stack_state *stack;
int allocated_stack;
int callsite;
};
코드 설명
1-11행bpf_reg_state는 Verifier가 각 명령어마다 R0~R10 레지스터별로 유지하는 추상 상태입니다. type은 포인터 종류를, var_off+min/max 쌍은 가능한 값 범위를 이중으로 추적합니다. 이 이중 추적은 비트 연산(tnum)과 산술 연산(min/max) 모두에서 정밀한 범위 분석을 가능하게 합니다.
8행off 필드는 포인터 타입 레지스터의 베이스 주소로부터의 오프셋입니다. 예를 들어 PTR_TO_MAP_VALUE + off=16이면 맵 값 시작으로부터 16바이트 위치를 가리킵니다. Verifier는 off + access_size <= value_size를 검증하여 범위 초과 접근을 방지합니다.
9행id는 조건부 NULL 검사를 연결하는 식별자입니다. bpf_map_lookup_elem() 반환값을 여러 레지스터에 복사해도, 하나에 대해 NULL 검사를 수행하면 동일 id를 가진 모든 레지스터의 타입이 함께 정제됩니다.
13-17행bpf_func_state는 하나의 BPF 함수 프레임을 나타냅니다. regs[11]은 R0~R10의 상태, stack은 512바이트 스택의 각 슬롯 상태(STACK_MISC, STACK_SPILL 등)를 추적합니다. BPF-to-BPF 호출 시 새 프레임이 푸시되며, callsite은 호출 명령어 위치를 기록하여 리턴 시 복원에 사용됩니다.
Pruning 알고리즘
상태 가지치기(state pruning)는 Verifier 성능의 핵심입니다. 매 분기점(prune point)에서 이전에 검증된 상태 목록과 현재 상태를 비교합니다:
/* kernel/bpf/verifier.c — states_equal() 핵심 로직 */staticboolstates_equal(structbpf_verifier_env *env,
structbpf_verifier_state *old,
structbpf_verifier_state *cur)
{
/* 모든 프레임의 모든 레지스터를 비교 */for (i = 0; i <= cur->curframe; i++) {
for (j = 0; j < MAX_BPF_REG; j++) {
/* cur 상태가 old 상태의 부분집합인지 확인 */if (!regsafe(env, &old_regs[j], &cur_regs[j], idmap))
returnfalse;
}
}
returntrue;
}
코드 설명
2-4행states_equal()은 Verifier의 상태 가지치기(state pruning) 핵심 함수입니다. 분기점에서 이전에 검증을 통과한 상태(old)와 현재 상태(cur)를 비교하여, 현재 상태가 이전 상태의 부분집합이면 나머지 경로 탐색을 건너뜁니다. 이 최적화 없이는 복잡한 프로그램의 검증 시간이 지수적으로 증가합니다.
6-7행모든 프레임(curframe)의 모든 레지스터를 순회합니다. BPF-to-BPF 호출이 있으면 여러 프레임이 스택에 존재하며, 각 프레임의 R0~R10을 모두 비교해야 합니다.
8-9행regsafe()는 현재 레지스터 상태가 이전 상태보다 제한적(more restricted)인지 판단합니다. 예를 들어 이전에 umax=100이었고 현재 umax=50이면, 현재 상태는 더 좁은 범위이므로 안전합니다. idmap은 포인터 ID 간의 대응 관계를 추적합니다.
전체가지치기의 정확성은 BPF 보안의 토대입니다. 부분집합 판정이 틀리면(더 넓은 범위를 허용) 검증을 우회하는 취약점이 됩니다. CVE-2021-3490 등 다수의 Verifier 취약점이 이 비교 로직의 결함에서 비롯되었습니다.
실무 팁: Verifier 거부 시 bpftool prog load ... verbose로 상세 로그를 확인하세요. 특히 R0 invalid mem access 'scalar' 같은 메시지는 NULL 체크 누락을 의미하고, back-edge from insn N to insn M은 bounded loop 검증 실패를 나타냅니다.
Bounded Loop 검증
커널 5.3 이후 BPF 프로그램에서 제한된 루프가 허용됩니다. Verifier는 루프 카운터의 범위를 추적하여 반복 횟수가 유한함을 정적으로 증명합니다:
BPF/XDP 개발에서 자주 발생하는 실수와 안티패턴을 정리합니다. 이 패턴들을 미리 파악하면 디버깅 시간을 크게 줄일 수 있습니다.
Verifier 에러 패턴
에러 메시지
원인
해결 방법
R1 invalid mem access 'scalar'
포인터 범위 검사 누락
데이터 접근 전 반드시 if (ptr + size > data_end) 검사
back-edge from insn X to Y
무한 루프 감지
bpf_loop()(5.17+) 또는 bpf_for()(6.4+) 사용
combined stack size exceeds 512 bytes
지역 변수 + BPF-to-BPF 호출 스택 초과
큰 구조체는 BPF_MAP_TYPE_PERCPU_ARRAY에 저장
program is too large (X insns)
명령어 수 1M 초과 또는 탐색 복잡도 초과
tail call로 분할, 불필요한 분기 제거
R0 !read_ok
맵 lookup 후 NULL 체크 누락
bpf_map_lookup_elem() 반환값 반드시 NULL 검사
unreachable insn
도달 불가능한 코드 존재
dead code 제거 또는 컴파일러 최적화(Compiler Optimization) 확인
tail_call abusing MAX_TAIL_CALL_CNT
tail call 깊이 33회 초과
체인 구조 재설계, 불필요한 tail call 제거
Map 크기 설계 실수
/* 안티패턴: 단일 CPU에서 전역 해시 맵으로 고 PPS 카운터 */
struct {
__uint(type, BPF_MAP_TYPE_HASH); /* ✗ 잠금 경합 발생 */
__type(key, __u32);
__type(value, __u64);
__uint(max_entries, 1000000);
} bad_counter SEC(".maps");
/* 올바른 패턴: per-CPU 맵 사용 */
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH); /* ✓ CPU별 독립 */
__type(key, __u32);
__type(value, __u64);
__uint(max_entries, 1000000);
} good_counter SEC(".maps");
/* 안티패턴: 고정 크기 해시 맵으로 동적 엔트리 관리 */
struct {
__uint(type, BPF_MAP_TYPE_HASH); /* ✗ 가득 차면 새 엔트리 삽입 실패 */
__type(key, __u32);
__type(value, __u64);
__uint(max_entries, 1024);
} bad_flow_table SEC(".maps");
/* 올바른 패턴: LRU 해시로 자동 퇴거 */
struct {
__uint(type, BPF_MAP_TYPE_LRU_HASH); /* ✓ 오래된 엔트리 자동 제거 */
__type(key, __u32);
__type(value, __u64);
__uint(max_entries, 1024);
} good_flow_table SEC(".maps");
XDP MTU/프래그먼트 처리 누락
주의: XDP 프로그램에서 가장 흔한 실전 버그는 점보 프레임과 멀티버퍼 패킷 처리 누락입니다. MTU가 1500을 초과하거나 GRO/LRO가 활성화된 환경에서는 반드시 xdp_buff의 프래그먼트를 확인해야 합니다.
/* 안티패턴: 단일 선형 버퍼만 가정 */
SEC("xdp")
int bad_xdp(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
/* ✗ 멀티버퍼 패킷이면 data~data_end가 전체가 아님 */
int pkt_len = data_end - data; /* 선형 부분만 */
...
}
/* 올바른 패턴: 멀티버퍼 인식 (6.x+) */
SEC("xdp.frags")
int good_xdp(struct xdp_md *ctx)
{
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
/* 선형 부분 + 프래그먼트 = 전체 패킷 */
int linear_len = data_end - data;
/* bpf_xdp_get_buff_len()으로 전체 길이 확인 (6.5+) */
__u64 total_len = bpf_xdp_get_buff_len(ctx);
/* 프래그먼트 접근은 bpf_xdp_load_bytes() 사용 */
char buf[64];
if (bpf_xdp_load_bytes(ctx, linear_len, buf, sizeof(buf)) < 0)
return XDP_PASS; /* 프래그먼트 없으면 정상 처리 */
...
}
CO-RE 미사용으로 인한 호환성 문제
/* 안티패턴: 커널 헤더 직접 포함 (빌드 환경 커널에 종속) */
#include <linux/sched.h>
SEC("tp_btf/sched_switch")
int bad_trace(u64 *ctx)
{
struct task_struct *prev = (void *)ctx[0];
/* ✗ task_struct 오프셋이 커널 버전마다 다름 */
int pid = prev->pid;
...
}
/* 올바른 패턴: CO-RE + vmlinux.h + BPF_CORE_READ */
#include "vmlinux.h"
#include <bpf/bpf_core_read.h>
SEC("tp_btf/sched_switch")
int good_trace(u64 *ctx)
{
struct task_struct *prev = (void *)ctx[0];
/* ✓ BTF 기반으로 런타임에 오프셋 재배치 */
int pid = BPF_CORE_READ(prev, pid);
...
}
/* CO-RE 필드 존재 확인 (조건부 접근) */
SEC("tp_btf/sched_switch")
int safe_trace(u64 *ctx)
{
struct task_struct *prev = (void *)ctx[0];
/* 필드가 존재하는 커널에서만 접근 */
if (bpf_core_field_exists(prev->se.cfs_rq)) {
void *cfs_rq = BPF_CORE_READ(prev, se.cfs_rq);
/* ... */
}
...
}
CO-RE 체크리스트:
vmlinux.h 사용 (bpftool btf dump file /sys/kernel/btf/vmlinux format c로 생성)
BPF_CORE_READ() / BPF_CORE_READ_STR_INTO() 매크로 사용
bpf_core_field_exists()로 선택적 필드 접근
bpf_core_type_exists()로 타입 호환성 확인
커널 5.5+ BTF 지원 필수 (CONFIG_DEBUG_INFO_BTF=y)
실습 가이드: bpftool/bpftrace 디버깅
BPF 프로그램의 디버깅은 일반 프로그램과 다릅니다. 커널 내 실행이므로 GDB를 직접 사용할 수 없지만 bpftool, bpftrace, tracepoint을 활용하면 효과적으로 디버깅할 수 있습니다.
bpftool 디버깅 기법
# 로드된 BPF 프로그램 목록 (상세)
sudo bpftool prog list -j | python3 -m json.tool
# 프로그램 상세 정보 (JIT, 맵 바인딩)
sudo bpftool prog show id 42 --pretty
# BPF 바이트코드 덤프 (verifier가 본 명령어)
sudo bpftool prog dump xlated id 42
# JIT 어셈블리 덤프 (실제 실행 코드)
sudo bpftool prog dump jited id 42
# 프로그램 실행 통계 (활성화 필요)
sudo sysctl kernel.bpf_stats_enabled=1
sudo bpftool prog show id 42
# run_cnt: 15234 run_time_ns: 892340
# → 평균 실행 시간: 892340/15234 ≈ 58.6ns
# 맵 조회/수정
sudo bpftool map show
sudo bpftool map dump id 5
sudo bpftool map update id 5 key 0x01 0x00 0x00 0x00 value 0xFF 0x00 0x00 0x00
# BTF 정보 확인
sudo bpftool btf show
sudo bpftool btf dump id 1 format c | head -50
# 프로그램 Pin (파일시스템에 고정)
sudo bpftool prog pin id 42 /sys/fs/bpf/my_xdp_prog
ls -la /sys/fs/bpf/my_xdp_prog
bpf_printk() — 간단한 디버그 출력 (cat /sys/kernel/debug/tracing/trace_pipe)
BPF_MAP_TYPE_RINGBUF — 구조화된 이벤트 전달
Verifier 로그: bpf_object__load() 실패 시 stderr 확인
BPF 성능 최적화
BPF 프로그램의 성능을 극대화하기 위한 JIT 최적화, batch 연산, 효율적 맵 선택 등의 기법을 다룹니다.
JIT 컴파일 최적화
# JIT 활성화 확인 및 설정
cat /proc/sys/net/core/bpf_jit_enable
# 0: 비활성, 1: 활성, 2: 디버그 모드 (프로덕션에서 사용 금지)
sudo sysctl net.core.bpf_jit_enable=1
# JIT kallsyms 활성화 (perf에서 BPF 함수명 표시)
sudo sysctl net.core.bpf_jit_kallsyms=1
# JIT 하드닝 (보안 강화, 약간의 성능 비용)
cat /proc/sys/net/core/bpf_jit_harden
# 0: 비활성, 1: unprivileged만, 2: 모든 프로그램
sudo sysctl net.core.bpf_jit_harden=1
Batch 연산
/* 맵 Batch 연산 (5.6+) — 대량 읽기/쓰기 성능 향상 */
#include <bpf/bpf.h>
/* 일반 방식: O(n) 시스템 콜 */
for (int i = 0; i < 10000; i++)
bpf_map_lookup_elem(fd, &keys[i], &values[i]); /* 10,000번 syscall */
/* Batch 방식: O(1) 시스템 콜 */
__u32 count = 10000;
DECLARE_LIBBPF_OPTS(bpf_map_batch_opts, opts, .elem_flags = 0, .flags = 0);
/* 한 번의 syscall로 10,000개 항목 조회 */
bpf_map_lookup_batch(fd, NULL, &next_key,
keys, values, &count, &opts);
/* Batch 업데이트 */
bpf_map_update_batch(fd, keys, values, &count, &opts);
/* Batch 삭제 */
bpf_map_delete_batch(fd, keys, &count, &opts);
/* Lookup-and-delete Batch (통계 수집에 이상적) */
bpf_map_lookup_and_delete_batch(fd, NULL, &next_key,
keys, values, &count, &opts);
Bloom Filter Map (5.16+)
/* BPF_MAP_TYPE_BLOOM_FILTER — 빠른 존재 확인 (false positive 허용) */
struct {
__uint(type, BPF_MAP_TYPE_BLOOM_FILTER);
__type(value, __u32);
__uint(max_entries, 1000000);
__uint(map_extra, 3); /* hash 함수 개수 (2-5 권장) */
} bloom SEC(".maps");
SEC("xdp")
int xdp_with_bloom(struct xdp_md *ctx)
{
/* ... 패킷 파싱 ... */
__u32 saddr = ip->saddr;
/* 1단계: Bloom Filter로 빠른 검사 (O(k), k=hash 함수 수) */
if (bpf_map_peek_elem(&bloom, &saddr) != 0)
return XDP_PASS; /* 확실히 블랙리스트 아님 */
/* 2단계: 정확한 해시 맵 조회 (Bloom 양성일 때만) */
if (bpf_map_lookup_elem(&blacklist, &saddr))
return XDP_DROP;
return XDP_PASS;
}
/* → Bloom Filter가 대부분의 정상 트래픽을 O(1)로 통과시켜 성능 향상 */
성능 최적화 우선순위(Priority):
맵 타입 선택 — per-CPU, LRU, Bloom Filter로 경합(Contention) 최소화
JIT 활성화 — 인터프리터 대비 2-5배 성능 향상
Batch 연산 — 사용자 공간 ↔ 맵 상호작용 최적화
tail call / BPF-to-BPF — 코드 크기 분할로 verifier 부하 감소
인라인 최적화 — __always_inline으로 함수 호출 오버헤드 제거
struct_ops 기반 커널 확장
BPF_PROG_TYPE_STRUCT_OPS(5.6+)는 커널 내부의 함수 포인터 테이블을 BPF 프로그램으로 대체하는 메커니즘입니다. TCP 혼잡 제어 알고리즘, 스케줄러(sched_ext) 등을 커널 모듈(Kernel Module) 없이 BPF로 구현할 수 있습니다.
# struct_ops 등록
sudo bpftool struct_ops register bpf_cc.bpf.o
# 등록된 struct_ops 확인
sudo bpftool struct_ops list
# 소켓에 혼잡 제어 알고리즘 적용
sysctl net.ipv4.tcp_congestion_control=bpf_aimd
# 또는 소켓 옵션으로 개별 적용
# setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "bpf_aimd", 9);
# 해제
sudo bpftool struct_ops unregister name bpf_aimd
코드 설명
SEC("struct_ops")커널은 bpf(BPF_MAP_CREATE) 시 BPF_MAP_TYPE_STRUCT_OPS 맵을 생성하고, BTF 정보를 사용하여 tcp_congestion_ops의 각 콜백 슬롯에 BPF 프로그램을 바인딩합니다. BPF_PROG 매크로는 커널 함수 시그니처를 BPF 컨텍스트로 변환하는 트램폴린(Trampoline) 코드를 생성합니다.
init 콜백bpf_aimd_init()은 새 TCP 연결 생성 시 호출됩니다. 커널의 tcp_init_congestion_control()이 icsk->icsk_ca_ops->init()을 통해 이 BPF 프로그램을 실행합니다. snd_cwnd = 10은 초기 혼잡 윈도우를 10 MSS로 설정합니다.
cong_avoid 콜백ACK 수신 시마다 호출되는 핵심 경로입니다. snd_cwnd < snd_ssthresh이면 Slow Start(지수 증가), 아니면 Congestion Avoidance(선형 증가)를 수행합니다. 이 콜백은 매 패킷마다 실행되므로 BPF JIT 컴파일이 성능에 필수적입니다.
ssthresh 콜백패킷 손실 감지 시 호출되어 새로운 slow start threshold를 반환합니다. cwnd >> 1(절반)은 전통적인 AIMD의 Multiplicative Decrease입니다. BBR 같은 알고리즘은 이 콜백에서 전혀 다른 로직을 구현합니다.
SEC(".struct_ops") 맵 선언이 섹션은 BPF 로더가 struct tcp_congestion_ops의 레이아웃을 BTF에서 조회하여 각 함수 포인터 슬롯을 대응하는 BPF 프로그램으로 채웁니다. .name = "bpf_aimd"는 sysctl tcp_congestion_control에서 이 알고리즘을 선택하는 데 사용되는 식별자입니다.
BPF 토큰과 보안 모델 (6.9+)
BPF 토큰은 컨테이너(Container)/샌드박스(Sandbox) 환경에서 BPF 기능에 대한 세밀한 권한 위임을 가능하게 하는 보안 메커니즘입니다. 기존의 CAP_BPF/CAP_SYS_ADMIN 바이너리 권한 모델을 대체합니다.
/* BPF 토큰 생성 (호스트에서) */
#include <linux/bpf.h>
#include <bpf/bpf.h>
/* 토큰이 허용할 BPF 기능 정의 */
LIBBPF_OPTS(bpf_token_create_opts, token_opts,
.flags = 0,
);
/* BPF 파일시스템에 토큰 고정 */
int bpffs_fd = open("/sys/fs/bpf/container_a", O_DIRECTORY);
int token_fd = bpf_token_create(bpffs_fd, &token_opts);
/* 토큰을 컨테이너에 전달 (fd 또는 UDS 경유) */
/* 컨테이너 내부에서 토큰 사용 */
LIBBPF_OPTS(bpf_prog_load_opts, prog_opts,
.token_fd = token_fd,
);
/* 토큰 범위 내의 프로그램만 로드 가능 */