チュートリアル2026年2月23日Soyeon Lee1 閲覧

eBPF 완전 입문 — 커널 코드 없이 커널을 수정하는 마법

eBPF는 커널을 재컴파일하지 않고도 커널 공간에서 코드를 실행할 수 있는 혁신적인 기술입니다. 이 가이드에서는 eBPF의 기본 개념부터 실제 구현까지 단계별로 안내하며, 보안 모니터링과 성능 분석에 활용하는 방법을 설명합니다.

#eBPF#커널 프로그래밍#보안 모니터링#성능 분석#Linux#BCC#libbpf#시스템 추적#kprobes#tracepoints
eBPF 완전 입문 — 커널 코드 없이 커널을 수정하는 마법
Soyeon Lee

Soyeon Lee

2026年2月23日

도입부: eBPF가 필요한 이유

전통적인 커널 프로그래밍은 높은 진입장벽을 가지고 있습니다. 커널 기능을 추가하거나 수정하려면 C 언어로 커널 모듈을 작성하고 컴파일한 후 재부팅해야 합니다. 이 과정은 시스템 안정성을 위협하고 배포 시간을 크게 늘립니다. 더욱이 버그가 발생하면 전체 시스템이 다운될 수 있습니다.

eBPF(extended Berkeley Packet Filter)는 이러한 문제를 근본적으로 해결합니다. eBPF를 사용하면 커널을 재시작하지 않고도 커널 공간에서 안전하게 프로그램을 실행할 수 있습니다. 보안 모니터링, 성능 분석, 네트워크 최적화, 시스템 추적 등 다양한 분야에서 eBPF의 활용도가 빠르게 증가하고 있습니다. Linux 커널 5.8 이상이라면 eBPF의 모든 주요 기능을 활용할 수 있습니다.

eBPF의 배경과 기술 개념

BPF는 원래 1992년 패킷 필터링을 위해 개발되었습니다. tcpdump 같은 도구가 이를 사용하여 효율적으로 네트워크 패킷을 처리했습니다. 그러나 기존 BPF는 32비트 레지스터만 지원하고 동적 메모리 할당이 불가능하며 함수 호출이 제한적이었습니다.

Linux 커널 3.15(2014년)에서 소개된 eBPF는 이러한 제약을 극복했습니다. 64비트 연산, 제한된 메모리 맵 접근, 커널 함수 호출이 추가되었고, Linux 커널 4.1부터는 kprobes(동적 추적 포인트)와 tracepoints(정적 추적 포인트)에 eBPF를 연결할 수 있게 되었습니다. 현재 eBPF는 시스템 보안, 성능 모니터링, 네트워크 기능 프로그래밍 등에 광범위하게 활용되고 있습니다.

eBPF의 핵심은 안전성과 유연성의 균형입니다. 커널 내 검증기(verifier)가 모든 eBPF 프로그램을 실행 전에 검증하여 무한 루프, 메모리 접근 위반, 권한 없는 연산을 방지합니다. 이를 통해 사용자 공간에서 작성한 코드가 커널 공간에서 안전하게 실행될 수 있습니다.

전제 조건 및 필요 환경

eBPF를 학습하기 위해서는 다음 조건이 필요합니다. Linux 커널은 5.8 이상을 권장하며, uname -r 명령으로 현재 커널 버전을 확인할 수 있습니다. 커널 구성에서 CONFIG_BPF, CONFIG_BPF_SYSCALL, CONFIG_DEBUG_INFO_BTF가 활성화되어 있어야 합니다.

개발 환경으로는 LLVM과 Clang 컴파일러(버전 10 이상 권장), Linux 헤더 파일, libbpf 라이브러리가 필요합니다. Python 3.6 이상이 설치되어 있으면 bcc 프레임워크를 사용할 수 있습니다. C 언어의 기본 문법과 포인터 개념, 리눅스 시스템 호출에 대한 이해가 도움이 됩니다.

환경 설정 및 도구 설치

eBPF 개발을 위한 환경을 구성하는 첫 번째 단계는 필수 패키지를 설치하는 것입니다. Ubuntu 또는 Debian 기반 시스템의 경우 다음 명령을 실행합니다.

#!/bin/bash
# eBPF 개발 환경 설정 스크립트
# 필수 패키지 설치
sudo apt-get update
sudo apt-get install -y \
    clang \
    llvm \
    libelf-dev \
    libpcap-dev \
    linux-headers-$(uname -r) \
    libbpf-dev \
    python3-pip \
    git
# bcc(BPF Compiler Collection) 설치
sudo apt-get install -y bpfcc-tools libbpfcc
# Python 바인딩을 통한 bcc 라이브러리 설치
pip3 install bcc
# 설치 확인
echo "=== 설치 확인 ==="
clang --version
llvm-config --version
pkg-config --modversion libbpf

설치가 완료되면 커널 설정을 확인합니다. /boot/config-$(uname -r) 파일에서 필요한 옵션이 활성화되어 있는지 검증합니다.

#!/bin/bash
# 커널 설정 확인
echo "=== eBPF 관련 커널 설정 확인 ==="
for config in BPF BPF_SYSCALL BPF_JIT DEBUG_INFO_BTF; do
    if grep -q "CONFIG_${config}=y" /boot/config-$(uname -r); then
        echo "✓ CONFIG_${config} 활성화됨"
    else
        echo "✗ CONFIG_${config} 비활성화됨"
    fi
done
# BPF 파일시스템 마운트 확인
if mountpoint -q /sys/kernel/debug/tracing; then
    echo "✓ debugfs 마운트됨"
else
    echo "⚠ debugfs 마운트 필요"
    sudo mount -t debugfs none /sys/kernel/debug
fi

환경 설정이 완료되었습니다. 이제 eBPF 프로그램을 작성하고 실행할 준비가 되었습니다.

단계별 가이드: 첫 eBPF 프로그램 작성 및 실행

1단계: 간단한 시스템 콜 추적 프로그램 작성

eBPF 프로그램의 가장 기초적인 예제는 시스템 콜을 추적하는 것입니다. openat 시스템 콜이 호출될 때마다 프로세스 ID와 파일명을 기록합니다.

// trace_openat.c - eBPF 프로그램
#include 
#include 
// BPF 맵: 추적 데이터를 저장할 링 버퍼
BPF_RINGBUF_OUTPUT(events, 256);
struct data_t {
    u32 pid;
    u64 ts;
    char filename[256];
};
// openat 시스템 콜 진입점 추적
TRACEPOINT_PROBE(syscalls, sys_enter_openat) {
    struct data_t data = {};
    // 현재 프로세스 ID 획득
    data.pid = bpf_get_current_pid_tgid() >> 32;
    // 타임스탬프 획득
    data.ts = bpf_ktime_get_ns();
    // 3번째 인자(파일명 포인터)에서 문자열 읽기
    bpf_probe_read_kernel_str(&data.filename, sizeof(data.filename),
                             (void *)ctx->args[1]);
    // 링 버퍼에 이벤트 전송
    bpf_ringbuf_output(&events, &data, sizeof(data), 0);
    return 0;
}

2단계: 사용자 공간 프로그램 작성

eBPF 프로그램을 로드하고 결과를 수집하는 Python 사용자 공간 프로그램을 작성합니다.

#!/usr/bin/env python3
# trace_openat.py - 사용자 공간 프로그램
import ctypes as ct
import struct
from bcc import BPF
import sys
from datetime import datetime
# eBPF 프로그램 로드
bpf_code = open('trace_openat.c').read()
b = BPF(text=bpf_code)
# 링 버퍼 콜백 함수 정의
def print_event(cpu, data, size):
    # 데이터 구조체 정의
    class Data(ct.Structure):
        _fields_ = [
            ("pid", ct.c_uint32),
            ("ts", ct.c_uint64),
            ("filename", ct.c_char * 256)
        ]
    event = ct.cast(data, ct.POINTER(Data)).contents
    # 타임스탬프를 읽기 쉬운 형식으로 변환
    ts_sec = event.ts / 1_000_000_000
    ts_readable = datetime.fromtimestamp(ts_sec).strftime('%H:%M:%S.%f')[:-3]
    # 파일명을 문자열로 변환
    filename = event.filename.decode('utf-8', 'ignore').rstrip('\x00')
    print(f"[{ts_readable}] PID {event.pid}: openat('{filename}')")  
# 링 버퍼 콜백 등록
b["events"].open_ring_buffer(print_event)
print("[*] openat 시스템 콜 추적 시작 (Ctrl-C로 종료)")
print("="*60)
try:
    while True:
        b.ring_buffer_poll()
except KeyboardInterrupt:
    print("\n[*] 추적 종료")
    sys.exit(0)

3단계: 프로그램 컴파일 및 실행

작성한 eBPF 프로그램을 실행하려면 다음과 같이 진행합니다.

#!/bin/bash
# 프로그램 실행
echo "[*] eBPF 프로그램 실행 중..."
# sudo가 필요 (커널 공간 접근)
sudo python3 trace_openat.py
# 또는 BCC 도구 직접 사용
# trace-openat는 BCC에 포함된 기본 도구
sudo trace-openat -h

이 프로그램을 실행하면 시스템의 모든 openat 시스템 콜이 추적되고 프로세스 ID와 함께 파일명이 출력됩니다.

4단계: 고급 예제 - 시스템 성능 분석

더 실용적인 예제로 파일 읽기/쓰기 성능을 분석하는 eBPF 프로그램을 작성합니다.

// vfs_read_write.c - 파일 I/O 성능 분석
#include 
#include 
// 이벤트 구조체
struct event_t {
    u64 ts;
    u32 pid;
    u32 uid;
    u64 latency_ns;
    u32 bytes;
    u8 op; // 0=read, 1=write
    char comm[16];
};
BPF_PERF_OUTPUT(events);
BPF_HASH(start_ts, u64, u64);
// vfs_read 진입점
int trace_vfs_read_entry(struct pt_regs *ctx, struct file *file,
                        char __user *buf, size_t count) {
    u64 pid_tgid = bpf_get_current_pid_tgid();
    u64 ts = bpf_ktime_get_ns();
    start_ts.update(&pid_tgid, &ts);
    return 0;
}
// vfs_read 종료점
int trace_vfs_read_return(struct pt_regs *ctx) {
    u64 pid_tgid = bpf_get_current_pid_tgid();
    u64 *start = start_ts.lookup(&pid_tgid);
    if (!start) return 0;
    u64 delta = bpf_ktime_get_ns() - *start;
    ssize_t bytes = PT_REGS_RC(ctx);
    if (bytes <= 0) return 0;
    struct event_t event = {
        .ts = bpf_ktime_get_ns(),
        .pid = pid_tgid >> 32,
        .uid = bpf_get_current_uid_gid() & 0xFFFFFFFF,
        .latency_ns = delta,
        .bytes = (u32)bytes,
        .op = 0,  // read
    };
    bpf_get_current_comm(&event.comm, sizeof(event.comm));
    events.perf_submit(ctx, &event, sizeof(event));
    start_ts.delete(&pid_tgid);
    return 0;
}

5단계: 실행 결과 분석

위 eBPF 프로그램들을 실행하면 실시간으로 커널 이벤트를 캡처합니다. openat 추적의 경우 다음과 같은 출력을 확인할 수 있습니다.

예상 결과 및 출력 검증

trace_openat.py를 실행하면 다음과 유사한 출력이 나타납니다.

[*] openat 시스템 콜 추적 시작 (Ctrl-C로 종료)
============================================================
[14:23:45.123] PID 1234: openat('/etc/passwd')
[14:23:46.456] PID 5678: openat('/tmp/test.log')
[14:23:47.789] PID 9012: openat('/home/user/.bashrc')
[14:23:48.012] PID 1234: openat('/var/log/syslog')
[14:23:49.345] PID 3456: openat('/proc/meminfo')

각 라인은 다음 정보를 포함합니다. 타임스탬프는 이벤트가 발생한 시간이고, PID는 해당 프로세스의 식별자입니다. openat 뒤의 경로는 시스템 콜이 접근한 파일입니다. 각 이벤트는 실시간으로 수집되므로 시스템의 파일 접근 패턴을 직관적으로 파악할 수 있습니다.

vfs_read_write 프로그램의 경우 파일 읽기/쓰기 지연시간(latency)을 나노초 단위로 추적합니다. 이를 통해 느린 I/O 연산을 식별하고 성능 병목을 분석할 수 있습니다.

일반적인 문제 해결

문제 1: "Permission denied" 에러

eBPF 프로그램은 커널에 접근하므로 반드시 root 권한이 필요합니다. 다음과 같은 해결 방법이 있습니다.

  • sudo를 사용하여 프로그램 실행: sudo python3 trace_openat.py
  • 사용자를 bpf 그룹에 추가하고 재로그인: sudo usermod -aG bpf $USER

문제 2: "No such file or directory" (trace_openat.c 찾을 수 없음)

Python 스크립트가 같은 디렉토리에 있는 C 파일을 읽으려 합니다. 다음 확인 사항을 점검합니다.

  • trace_openat.c 파일이 같은 디렉토리에 있는지 확인
  • 파일 경로를 절대 경로로 지정: open('/full/path/to/trace_openat.c')
  • 현재 작업 디렉토리 확인: pwd

문제 3: "Verifier rejects" 에러

커널의 eBPF 검증기가 프로그램을 거부하는 경우입니다. 이는 보안상 제약이 있는 코드를 사용했거나 메모리 접근이 올바르지 않을 때 발생합니다.

  • 커널 로그에서 상세 정보 확인: sudo dmesg | tail -20
  • 메모리 바운드 체크 추가: 배열 접근 전 인덱스 범위 검증
  • 포인터 NULL 검사 확인: if (!ptr) return 0;

문제 4: 링 버퍼에서 데이터를 수신하지 못함

프로그램이 실행되지만 출력이 나타나지 않으면 다음을 확인합니다.

  • 시스템 콜이 실제로 발생하는지 확인: 다른 터미널에서 파일 접근
  • debugfs 마운트 확인: mount | grep debugfs
  • BCC/libbpf 버전 호환성 확인: 너무 오래된 버전일 수 있음

문제 5: 메모리 부족 에러

eBPF 프로그램의 스택 크기가 커널 제한(일반적으로 512바이트)을 초과하면 발생합니다.

  • 구조체 크기 최소화: 불필요한 필드 제거
  • BPF 맵 크기 조정: 너무 많은 데이터를 메모리에 저장하지 않기
  • 링 버퍼 활용: per-CPU 저장소 대신 효율적인 링 버퍼 사용

고급 활용 및 성능 최적화

eBPF의 성능은 설계 방식에 따라 크게 달라집니다. 프로덕션 환경에서는 다음 최적화 기법을 적용합니다.

1. Per-CPU 메모리 활용

여러 CPU 코어에서 동시에 실행되는 eBPF 프로그램을 최적화하려면 per-CPU 배열을 사용합니다. 이는 CPU 간 동기화 오버헤드를 줄입니다.

// per-CPU 배열 사용
BPF_ARRAY(cpu_stats, 256, "PerCpuArray");
int trace_function(struct pt_regs *ctx) {
    u32 cpu = bpf_get_smp_processor_id();
    struct stat_t *stat = cpu_stats.lookup(&cpu);
    if (stat) {
        stat->count++;
    }
    return 0;
}

2. 샘플링 (Sampling)

고빈도 이벤트를 모두 추적하면 성능 오버헤드가 커집니다. 통계적으로 유의미한 수준으로 샘플링합니다.

// 10%만 샘플링
int sample_rate = 10;
int trace_event(struct pt_regs *ctx) {
    if ((bpf_get_prandom_u32() % 100) >= sample_rate) {
        return 0;  // 스킵
    }
    // 이벤트 처리
    return 0;
}

3. 필터링 최적화

커널 공간에서 최대한 조기에 필터링하면 사용자 공간으로 전달되는 데이터량을 줄일 수 있습니다.

// 특정 프로세스만 추적
int trace_for_pid(struct pt_regs *ctx, u32 target_pid) {
    u64 pid_tgid = bpf_get_current_pid_tgid();
    u32 pid = pid_tgid >> 32;
    if (pid != target_pid) {
        return 0;  // 조기 반환
    }
    // 타겟 프로세스만 처리
    return 0;
}

4. 동적 추적 포인트 (kprobes) vs 정적 추적 포인트 (tracepoints)

kprobes는 모든 커널 함수에서 추적 가능하지만 오버헤드가 큽니다. 가능하면 커널에서 제공하는 정적 tracepoints를 사용합니다. 특정 함수만 필요한 경우 kprobes를 사용하되, 정확한 함수 시그니처를 파악하여 안정성을 높입니다.

5. 컨텍스트 메타데이터 최소화

각 이벤트마다 전송할 데이터를 최소화합니다. 전체 문자열 대신 해시를 저장하거나, 필요한 필드만 선택합니다.

보안 고려사항 및 모범 사례

eBPF는 강력한 도구이지만 잘못 사용하면 보안 위험을 초래할 수 있습니다. 다음 모범 사례를 준수합니다.

1. 입력 검증

사용자 공간에서 받은 데이터는 커널 공간에서 안전하게 검증해야 합니다. 포인터 값이 유효한 커널 메모리 범위에 있는지 확인합니다.

2. 메모리 접근 경계 확인

배열이나 구조체에 접근할 때 항상 경계를 확인합니다. 커널의 검증기는 명시적인 경계 검사를 요구합니다.

3. 권한 분리

eBPF 프로그램은 root 권한으로 실행되므로 신뢰할 수 있는 소스에서만 로드합니다. 사용자가 작성한 eBPF 코드를 그대로 실행하지 않습니다.

4. 감시 및 알림

eBPF로 수집한 보안 이벤트는 중앙 모니터링 시스템으로 전송하여 실시간으로 분석합니다. Seekurity SIEM은 eBPF 이벤트를 수집하고 위협 탐지 규칙을 적용하여 의심스러운 활동을 자동으로 식별합니다.

5. 정기적인 감시 및 리뷰

운영 중인 eBPF 프로그램의 성능 영향을 정기적으로 측정합니다. 시스템 부하가 높아지면 샘플링 비율을 낮추거나 필터 조건을 강화합니다.

결론 및 다음 단계

eBPF는 현대 Linux 시스템의 보안 및 성능 관찰 방식을 혁신하고 있습니다. 커널을 재컴파일하지 않고도 실시간으로 커널 동작을 모니터링하고 제어할 수 있다는 점에서 매우 강력합니다. 이 가이드에서는 eBPF의 기본 개념부터 실제 프로그래밍까지 단계별로 학습했습니다.

다음으로 학습할 주제는 Cilium을 이용한 eBPF 기반 네트워크 정책(Network Policy) 구현, Falco를 활용한 보안 이벤트 탐지, BCC 프레임워크를 통한 성능 프로파일링입니다. Kubernetes 환경에서는 eBPF 기반 서비스 메시 구현도 가능합니다.

특히 보안 모니터링 측면에서 eBPF의 가치는 매우 큽니다. Seekurity SIEM/SOAR와 FRIIM 제품군은 eBPF 기반 데이터 수집 기능을 통해 커널 수준의 보안 이벤트를 실시간으로 수집하고 분석합니다. Seekurity SIEM은 eBPF로 수집한 시스템 콜, 프로세스 실행, 파일 접근 정보를 중앙에서 수집하고 위협 탐지 규칙을 적용하여 의심스러운 활동을 즉각 식별합니다. FRIIM의 컨테이너 워크로드 보호(CWPP) 기능도 eBPF를 활용하여 컨테이너 런타임에서의 위협을 실시간으로 탐지합니다. 이처럼 eBPF 기술을 이해하면 현대적인 보안 인프라 구축과 고급 위협 탐지에 필수적인 토대를 확보할 수 있습니다.

タグ

#eBPF#커널 프로그래밍#보안 모니터링#성능 분석#Linux#BCC#libbpf#시스템 추적#kprobes#tracepoints