eBPF (bpf2go) - __attribute__((preserve_access_index)) 사용법
아래의 글에 보면,
eBPF application development: Beyond the basics
; https://developers.redhat.com/articles/2023/10/19/ebpf-application-development-beyond-basics
The BPF program attaches to the net_dev_queue kernel tracepoint to intercept all packets sent or received by any program running on the host. The tracepoint context includes a pointer to the struct sk_buff that holds the packet data. We are only interested in accessing thesk_buff->data and sk_buff->len fields so we can use CO-RE to access them. The BPF program defines its own private version of struct sk_buff that contains only the fields we need. The struct is annotated with the preserve_access_index attribute so that the CO-RE relocation can happen when the BPF program gets loaded.
"Accessing kernel data structures" 절에서 "__attribute__((preserve_access_index)"에 대한 설명이 나옵니다. 정리해 보면, 필요한 커널 구조체가 있을 때 원래의 그 타입이 소유한 모든 필드를 정의할 필요 없이 관심 있는 필드만 정의할 수 있게 해주는 attribute입니다. (어차피 이름이 같은 field의 offset을 런타임 시에 eBPF 로더가 계산해 주므로.)
예를 들어, task_struct를 사용한다고 가정해 볼까요? ^^
전에도 설명했지만 task_struct는 너무 크고 빌드 시 정의한 전처리 상수에 따라 유동적이므로 그런 구조체의 정의를 BPF 내에서 직접 정의해 사용하는 것은 좀 애매합니다. 바로 그럴 때, __attribute__((preserve_access_index))를 사용해 관심 있는 필드만 정의할 수 있습니다.
struct task_struct {
unsigned int flags;
const struct cred *cred;
} __attribute__((preserve_access_index));
한 가지 유의할 점이라면, 저렇게 정의할 수 있는 구조체는
"vmlinux.h"에 정의되지 않은 구조체여야 한다는 점입니다. 왜냐하면 같은 이름의 구조체를 재정의할 수 없기 때문입니다.
즉, 약식 정의한 구조체의 이름을 task_struct라고 정의하면, vmlinux.h에 이미 동일한 이름으로 정의돼 있으므로 컴파일 오류가 발생하는 것입니다. (사실, 굳이 vmlinux.h에 있는 구조체를 재정의할 필요가 없긴 합니다.) 그렇다고 해서, 대신 my_task_struct 같은 이름을 사용하면 어떻게 될까요?
struct my_task_struct {
unsigned int flags;
const struct cred *cred;
} __attribute__((preserve_access_index));
SEC("tracepoint/syscalls/sys_enter_execve")
int sys_enter_execve(struct pt_regs *ctx) {
struct my_task_struct *current_task = (struct my_task_struct *)bpf_get_current_task();
unsigned int task_flags = BPF_CORE_READ(current_task, flags);
bpf_printk("task_flags == %d\n", task_flags);
return 0;
}
저렇게 하면 일단 (
bpf2go 등의) 컴파일 단계는 통과하지만, 이후의 실행 단계에서 bpf 프로그램을 로드할 때 다음과 같은 오류가 발생합니다.
program sys_enter_execve: load program: bad CO-RE relocation: invalid func unknown#195896080 (6 line(s) omitted)
왜냐하면 CO-RE 재배치(relocation)를 위해서는 구조체의 이름 정보까지 동일해야 하기 때문입니다.
그렇다면, vmlinux.h 헤더를 포함시키지 않으면 되지 않을까요? 그러기에는 ^^; vmlinux 헤더 파일에 포함된 수많은 정의를 사용할 수 없어 더 큰 불편이 발생합니다.
그래도 아주 불가능한 것은 아닌데요, 이에 대한 좋은 예제를 cilium에서 찾아볼 수 있습니다.
examples/headers
; https://github.com/cilium/ebpf/tree/main/examples/headers
examples/tracepoint_in_c/tracepoint.c
; https://github.com/cilium/ebpf/blob/main/examples/tracepoint_in_c/tracepoint.c
tracepoint.c 예제를 보면, "examples/headers" 경로에 있는 common.h 헤더만 포함시키고 있는데요, 그 파일(및 그것이 포함하는 헤더 파일)은 vmlinux.h의 약식 버전에 해당합니다. 즉, task_struct 같은 커널 구조체 정의는 포함하고 있지 않으므로 다음과 같이 코딩할 수 있습니다.
#include "common.h"
#include <bpf/bpf_core_read.h>
#define TASK_COMM_LEN 16
struct task_struct {
unsigned int flags;
const struct cred *cred;
char comm[TASK_COMM_LEN];
} __attribute__((preserve_access_index));
SEC("tracepoint/syscalls/sys_enter_execve")
int sys_enter_execve(struct pt_regs *ctx) {
struct task_struct *current_task = (struct task_struct *)bpf_get_current_task();
unsigned int task_flags = BPF_CORE_READ(current_task, flags);
bpf_printk("task_flags == %d, %s\n", task_flags, current_task->comm);
return 0;
}
당연하겠지만, 저 attribute가 적용된 구조체는
BTF가 활성화된 OS에서만 사용 가능합니다. 만약 BTF가 비활성화된 OS에서 저 BPF 프로그램을 로딩하면
다음과 같은 오류가 발생합니다.
program sys_enter_execve: apply CO-RE relocations: load kernel spec: btf: not found
그런데... 굳이 저 옵션이 (vmlinux.h 포함 여부에 상관없이) task_struct와 같은 커널 구조체에 대해 필요한가...입니다. 왜냐하면 vmlinux.h 헤더에 정의된 task_struct도 preserve_access_index 옵션 없이 정의돼 있기 때문입니다. 실제로, (vmlinux.h를 없애고 common.h로 실습한) 위의 예제에서도 task_struct를 preserve_access_index 없이 정의해도 잘 동작합니다. 그러니까, 어차피 BTF 정보가 있는 타입이라면 굳이 저 속성을 붙이지 않아도 된다는 뜻입니다.
그런 의미에서 "
eBPF application development: Beyond the basics" 글에서 설명한 저 속성의 용도는 딱히 와닿지 않습니다.
그렇다고 그냥 사용자 코드에서 정의한 구조체에 저 속성을 붙이는 것도,
[BPF] Add preserve_access_index attribute for record definition
; https://reviews.llvm.org/D69759
의미 있어 보이진 않습니다. 혹시 저 옵션에 대한 이력이나 관련 설명을 해주실 분이 계실까요? ^^
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]