TT Lab
블로그 시작하기
배우기 러닝패스 코스

리눅스 커널과 eBPF

필드의 위치가 아니라 이름을 기록한다

TT Lab 에서 이어서 보기

한 줄 요약

커널 구조체의 필드 위치는 판과 설정마다 다릅니다. CO-RE(Compile Once, Run Everywhere)는 컴파일할 때 "어느 구조체의 어느 필드를 읽는다" 는 사실만 기록하고, 올리는 순간 libbpf 가 그 커널의 BTF 를 보고 위치를 고칩니다. 그래서 바이너리 하나가 여러 커널에서 돕니다.

왜 필요했나

task->real_parent->tgid 를 읽는 BPF 프로그램을 생각해 봅시다. real_parent 가 task_struct 의 몇 번째 바이트에 있는지는 커널 판, 켜 둔 설정, 컴파일러에 따라 다릅니다. 예전 도구인 BCC 는 그래서 대상 서버에서 매번 그 서버의 커널 헤더로 다시 컴파일했습니다. 서버마다 clang 과 헤더 패키지를 깔아야 했고, 도구 하나 시작에 수십 초와 수백 MB 메모리가 들었고, 헤더 패키지가 없는 배포판에서는 아예 돌지 않았습니다. 수천 노드에 에이전트를 까는 Cilium 이나 Falco 에게 이것은 받아들일 수 없는 비용이었습니다.

어떻게 동작하나

출발점은 커널이 스스로를 설명하는 BTF(BPF Type Format)입니다. BTF 를 켜고 빌드한 커널은 /sys/kernel/btf/vmlinux 로 자기의 모든 타입 정보를 내놓습니다. bpftool btf dump file /sys/kernel/btf/vmlinux format c 는 그것을 C 머리 파일 vmlinux.h 로 풀어 주므로, 커널 헤더 패키지 없이 구조체를 쓸 수 있습니다.

BPF 프로그램에서 BPF_CORE_READ(task, real_parent, tgid) 로 읽으면 clang 은 필드 접근을 특별한 표시와 함께 컴파일하고, 오브젝트의 .BTF.ext 섹션에 재배치 기록을 남깁니다. "이 명령은 task_struct 의 real_parent 를 읽는다" 같은 기록입니다. 그냥 화살표로 쓰면 검증기가 커널 메모리 직접 접근으로 보고 거부하거나, 컴파일한 기계의 위치가 굳어 버립니다.

libbpf 는 오브젝트를 올리기 전에 대상 커널의 BTF 에서 같은 이름의 구조체와 필드를 찾아 실제 위치로 명령을 고칩니다. 필드가 없으면 올리기를 실패시키거나, 프로그램이 bpf_core_field_exists() 로 미리 물어 다른 길로 갈 수 있습니다.

사용자 공간 쪽은 스켈레톤이 단순하게 해 줍니다. bpftool gen skeleton 은 오브젝트를 통째로 머리 파일에 싣고, 맵과 프로그램에 이름으로 닿는 구조체와 __open_and_load()·__attach()·__destroy() 함수를 만들어 줍니다. 로더는 몇십 줄이면 됩니다. 로더가 끝난 뒤에도 프로그램을 남기려면 링크를 BPF 파일시스템에 핀합니다.

한 가지 오해를 풀어 둡니다. vmlinux.h 는 그것을 뽑은 커널의 타입이지만, 다른 판의 커널에서 뽑은 것을 써도 대개 괜찮습니다. 필드의 위치는 어차피 올릴 때 고쳐지고, 컴파일에 필요한 것은 이름과 타입뿐이기 때문입니다. 그래서 많은 프로젝트가 vmlinux.h 하나를 저장소에 넣어 두고 씁니다.

BTF 가 없는 옛 커널도 방법이 있습니다. bpftool gen min_core_btf 는 큰 BTF 에서 그 프로그램이 실제로 쓰는 타입과 필드만 남긴 작은 BTF 를 만들고, 그것을 도구와 함께 실어 보냅니다. 결과를 풀어 보면 프로그램이 커널의 어디에 기대는지가 그대로 보입니다.

현장에서 만나는 모습

"이 에이전트가 특정 배포판 커널에서만 안 뜬다" 는 문제의 상당수가 CO-RE 재배치 실패입니다. 로그에 failed to resolve CO-RE relocation 과 구조체 이름이 찍힙니다. 그 커널에서 필드 이름이 바뀌었거나 BTF 가 꺼져 있는 경우입니다. ls /sys/kernel/btf/vmlinux 한 줄로 BTF 유무부터 봅니다.

이 구조는 커널 기여에도 이어집니다. 구조체 필드 이름을 바꾸는 패치는 BTF 를 통해 바깥의 BPF 도구를 깨뜨릴 수 있어서, 리뷰에서 그 영향을 묻는 경우가 있습니다.

English corner

다음 실습에서 할 것

커널 BTF 에서 vmlinux.h 를 뽑고, exec 을 부모 프로세스별로 세는 BPF 프로그램을 BPF_CORE_READ 로 씁니다. 재배치 기록과 함께 컴파일해 스켈레톤을 만들고, libbpf 로더를 지어 링크와 맵을 핀합니다. 마지막으로 min_core_btf 로 이 프로그램이 기대는 필드만 눈으로 확인합니다.