Зачем CO-RE
Раньше [[ebpf-basics|eBPF]] программы привязывались к конкретному
kernel - structure layout менялся между версиями. Если writeable
(task_struct->pid) перенесли на другой offset - программа сломалась.
Решений было два:
- kernel-headers attached - программа собирается под целевой kernel (BCC: компиляция в runtime через LLVM в каждом deploy)
- CO-RE (Compile Once - Run Everywhere) - программа собирается один раз, на runtime libbpf делает relocations через BTF
Сегодня CO-RE - стандарт. BCC объявлен legacy, новые tracing-инструменты пишут на libbpf+CO-RE.
BTF - BPF Type Format
BTF - компактный формат метаданных о типах (как DWARF, но специально для BPF). Описывает structs, unions, enums, function signatures.
Kernel экспортирует свой BTF в /sys/kernel/btf/vmlinux (с
CONFIG_DEBUG_INFO_BTF=y, в большинстве дистрибутивов с 5.4+).
Размер - около 5-7 МБ против 200+ МБ для DWARF debug-info. Это позволяет держать его постоянно в kernel.
Каждый загруженный module тоже имеет свой BTF в /sys/kernel/btf/<module>.
Userspace проверка:
ls /sys/kernel/btf/
bpftool btf dump file /sys/kernel/btf/vmlinux | head -20
vmlinux.h - заголовок всего kernel
Из BTF собирается vmlinux.h - один большой header со всеми структурами kernel:
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
Эта строка - вся включенная: вместо #include <linux/sched.h> ты
пишешь #include "vmlinux.h" и получаешь все типы. Не надо ставить
kernel-headers, не надо думать про CONFIG_*.
Минусы: 50K+ строк, дольше компиляция. Зато не нужен kernel-source.
CO-RE relocations
Идея: clang при компиляции eBPF не "запекает" offset поля в bytecode, а оставляет специальный CO-RE relocation record. На runtime libbpf читает target-kernel BTF, находит соответствующее поле, переписывает offset в загруженной программе.
Помечаешь access через BPF_CORE_READ или __builtin_preserve_access_index:
#include <bpf/bpf_core_read.h>
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
pid_t pid = BPF_CORE_READ(task, pid);
pid_t parent_pid = BPF_CORE_READ(task, parent, pid);
В bytecode: вместо "load байты по offset 0x4F8" - "load по
offset(struct task_struct, pid)" с relocation record.
На runtime libbpf проверяет в target BTF где pid лежит в
task_struct и подставляет правильный offset.
Если поле переименовано - можно использовать BPF_CORE_READ_INTO с
fallback'ами, или __attribute__((preserve_access_index)) структуры.
Field-existence checks
CO-RE умеет работать с отсутствующими в target-kernel полями:
if (bpf_core_field_exists(task->some_new_field)) {val = BPF_CORE_READ(task, some_new_field);
} else {val = -1; // graceful fallback
}
То же для enum значений:
if (bpf_core_enum_value_exists(enum cpu_state, CPU_STATE_NEW)) {...
}
Это даёт настоящую portability - один бинарник работает от kernel 5.4 до 6.10, gracefully degraded'ит features.
libbpf-bootstrap - стартовый шаблон
github.com/libbpf/libbpf-bootstrap - канонический шаблон проекта на libbpf+CO-RE. Структура:
myproject/
src/
myprog.bpf.c # eBPF-программа на C
myprog.c # userspace-loader
Makefile # генерирует skeleton, собирает оба
libbpf/ # submodule с библиотекой
bpftool/ # submodule
Workflow:
clang -O2 -target bpf -c myprog.bpf.c -o myprog.bpf.obpftool gen skeleton myprog.bpf.o > myprog.skel.h- Userspace
myprog.cinclud'ит skeleton, вызываетmyprog__open(),myprog__load(),myprog__attach() - Финальный binary - один статически слинкованный исполняемый файл с embedded BPF-bytecode
Деплой: один файл, нет dependency на kernel-headers, BCC, LLVM.
CO-RE vs BCC - сравнение
| Свойство | BCC | libbpf+CO-RE |
|---|---|---|
| Что нужно на target | LLVM (~600 МБ), kernel-headers | ничего (только kernel с BTF) |
| Размер артефакта | Python + C source | один statically-linked binary |
| Время старта | секунды (LLVM compile) | миллисекунды |
| Portability | привязан к target kernel | один бинарник на много kernel |
| Сложность написания | проще (high-level Python) | сложнее (C, skeleton) |
| Языки userspace | Python | C, Go (libbpfgo), Rust (libbpf-rs) |
Современные tools (parca-agent, tetragon, hubble, beyla) - все на libbpf+CO-RE.
BTF на старых ядрах - BTFGen
CO-RE требует BTF в kernel. Старые ядра (RHEL 7, Ubuntu 18.04) -
CONFIG_DEBUG_INFO_BTF не было. Решение - BTFGen (от Aqua Security):
генерится "минимальный" BTF под конкретное приложение, доставляется в
его deploy. На target libbpf использует этот мини-BTF вместо vmlinux.
Packaged в libbpfgo/btfgen или через bpftool gen min_core_btf. Для
observability на legacy kernels - спасение.
Миграция с BCC на libbpf+CO-RE
Steps:
- Скачать
vmlinux.hот target kernel (или generate из своего):bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
- Заменить
#include <linux/...>на#include "vmlinux.h" - Заменить direct field access (
task->pid) наBPF_CORE_READ(task, pid) - Userspace - переписать с Python BCC на C libbpf
- Сгенерить skeleton, использовать
myprog__attach()вместо BCC'sattach_kprobe - Опубликовать как один static binary
Утилита bcc-to-libbpf (community) делает базовое преобразование но
не идеально - правки руками всё равно нужны.
Когда CO-RE ломается
field 'foo' not found in target kernel- поле было переименовано/ удалено. Используйbpf_core_field_exists+ fallback.type 'struct foo' not found- struct переименована. Старые кейсы:request_queue→ структура изменена в 6.x. Альтернатива -BPF_CORE_READ_BITFIELD_PROBEDили прямой kprobe.- kernel без BTF (
/sys/kernel/btf/vmlinuxнет) - используй BTFGen или собирай custom kernel сCONFIG_DEBUG_INFO_BTF=y. - Verifier rejected - тот же verifier что и в ebpf-basics, CO-RE сам по себе не помогает с safety checks.
- Crash после kernel-update - вероятно тип, который ты читаешь, кардинально изменился (struct rebuilt). Re-test on new kernel, добавь field-existence check.
Где почитать
- https://nakryiko.com/posts/bpf-portability-and-co-re/ - оригинальная статья от Andrii Nakryiko (автора libbpf)
- https://github.com/libbpf/libbpf-bootstrap - стартовый шаблон
- https://docs.kernel.org/bpf/btf.html - спецификация BTF
- https://ebpf.io/applications/ - real-world проекты на CO-RE