eBPF 함수의 인자를 다루는 방법
지난 글에 실습한 kprobe 예제는,
SEC("kprobe/sys_clone") int kprobe_sys_clone(void *ctx)
{
u64 pid_tgid = bpf_get_current_pid_tgid();
pid_t tgid = pid_tgid >> 32;
pid_t pid = pid_tgid;
bpf_printk("pid == %d, thread_id == %d, newsp == %x\n", tgid, pid, clone_flags);
return -1;
}
인자를 void* 타입으로 퉁쳤었는데요, 이제 그것을 수정해 보겠습니다. 이를 위해 우선 kprobe 대상 함수("sys_clone")의 인자를 확인해 보면,
// https://github.com/torvalds/linux/blob/ae90f6a6170d7a7a1aa4fddf664fbd093e3023bc/include/linux/syscalls.h#L786
#ifdef CONFIG_CLONE_BACKWARDS
asmlinkage long sys_clone(unsigned long, unsigned long, int __user *, unsigned long,
int __user *);
#else
#ifdef CONFIG_CLONE_BACKWARDS3
asmlinkage long sys_clone(unsigned long, unsigned long, int, int __user *,
int __user *, unsigned long);
#else
asmlinkage long sys_clone(unsigned long, unsigned long, int __user *,
int __user *, unsigned long);
#endif
5개의 인자가 나오는데요, 이것을 접근하는 방법은 eBPF 함수에 기본적으로 전달되는 pt_regs를 이용해 PT_REGS_PARAM 매크로를 사용하면 됩니다.
SEC("kprobe/sys_clone") int kprobe_sys_clone(struct pt_regs* ctx)
{
unsigned long clone_flags = PT_REGS_PARM1(ctx);
unsigned long newsp = PT_REGS_PARM2(ctx);
int* parent_tidptr = (int*)PT_REGS_PARM3(ctx);
unsigned long tls = PT_REGS_PARM4(ctx);
int* child_tidptr = (int*)PT_REGS_PARM5(ctx);
// ...[생략]...
}
여기서 pt_regs는 Win32 환경이라면
CONTEXT 구조체와 비슷한 역할을 합니다. 따라서 CPU 레지스터를 대표하기 때문에 운영체제가 정한 ABI에 따라 레지스터를 통해 들어오는 인자와 Stack Pointer로부터의 오프셋 계산으로 인자를 추출할 수 있습니다.
리눅스의 x86-64 ABI인 경우, 첫 6개의 매개변수를 rdi, rsi, rdx, rcx, r8, r9 레지스터에 전달한다고 합니다. 만약 부동 소수점 타입인 경우에는 xmm 레지스터로 전달하고, 그 이상의 매개변수인 경우에는 스택을 이용합니다.
위와 같은 접근법이 libbpf를 도입하면 약간 바뀌는데요, 이에 대해서는 설명한 적이 있습니다.
eBPF의 2가지 방식 - libbcc와 libbpf(CO-RE) - 8. Kprobes
; https://www.sysnet.pe.kr/2/0/13801#8
즉, BPF_KPROBE 매크로 함수를 사용해 pt_regs를 숨기고 인자를 직접 지정할 수 있는데,
#ifdef __BCC__
int kprobe__acct_collect(struct pt_regs *ctx, long exit_code, int group_dead)
#else
SEC("kprobe/acct_collect")
int BPF_KPROBE(kprobe__acct_collect, long exit_code, int group_dead)
#endif
{
/* BPF code accessing exit_code and group_dead here */
}
원한다면 일부 인자만 정의해도 됩니다. 가령 kprobe_sys_clone 함수를 첫 번째 인자(clone_flags)만 관심이 있다면 이렇게 정의할 수 있습니다.
SEC("kprobe/sys_clone") int BPF_KPROBE(kprobe_sys_clone, unsigned long clone_flags)
{
bpf_printk("clone_flags == %x\n", clone_flags);
return 0;
}
그런데 사실 pt_regs 인자가 없어진 것이 아닙니다. 이를 위해 BPF_KPROBE 매크로 정의를 보면,
#define BPF_KPROBE(name, args...) \
name(struct pt_regs *ctx); \
static __always_inline typeof(name(0)) \
____##name(struct pt_regs *ctx, ##args); \
typeof(name(0)) name(struct pt_regs *ctx) \
{ \
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \
return ____##name(___bpf_kprobe_args(args)); \
_Pragma("GCC diagnostic pop") \
}
pt_regs 인자를 ctx라는 숨겨진 변수로 처리하고 있습니다. 결국 다음과 같이 BPF_KPROBE의 인자 지정과 함께 혼용하는 것도 가능합니다.
// 이렇게 사용하는 것도 가능
SEC("kprobe/sys_clone") int BPF_KPROBE(kprobe_sys_clone, unsigned long clone_flags)
{
unsigned long newsp = PT_REGS_PARM2(ctx); // BPF_KPROBE에 지정하지 않은 2번째 인자를 매크로로 추출
bpf_printk("clone_flags == %x, newsp == %x\n", clone_flags, newsp);
return 0;
}
다시 말해, pt_regs* ctx가 없어진 것이 아니라 그냥 가볍게 감싼 것에 불과하기 때문에 PT_REGS_PARM 매크로를 이용한 방법은 여전히 유효합니다.
SEC("kprobe/sys_clone") int BPF_KPROBE(kprobe_sys_clone, unsigned long clone_flags, unsigned long newsp, int* parent_tidptr, unsigned long tls, int* child_tidptr)
{
unsigned long clone_flags2 = PT_REGS_PARM1(ctx);
bpf_printk("clone_flags == %x, clone_flags2 == %x\n", clone_flags, clone_flags2);
unsigned long newsp2 = PT_REGS_PARM2(ctx);
bpf_printk("newsp == %x, newsp2 == %x\n", newsp, newsp2);
int* parent_tidptr2 = (int*)PT_REGS_PARM3(ctx);
bpf_printk("parent_tidptr == %x, parent_tidptr2 == %x\n", parent_tidptr, parent_tidptr2);
unsigned long tls2 = PT_REGS_PARM4(ctx);
bpf_printk("tls == %x, tls2 == %x\n", tls, tls2);
int* child_tidptr2 = (int*)PT_REGS_PARM5(ctx);
bpf_printk("child_tidptr == %x, child_tidptr2 == %x\n", child_tidptr, child_tidptr2);
return 0;
}
그럼 커널 구조체도 접근해 볼까요? ^^ 예를 들기 위해 tcp_connect 함수를 대상으로 할 텐데요, 그 함수의 첫 번째 인자는 struct sock *sk입니다.
struct sock
; https://github.com/torvalds/linux/blob/master/include/net/sock.h#L342
struct sock 구조체의 내부에는 다시 struct sock_common 구조체가 있는데요,
struct sock_common
; https://github.com/torvalds/linux/blob/master/include/net/sock.h#L150
물론, 커널 헤더 파일의 정의대로 직접 eBPF 소스코드에 복사해 사용할 수도 있지만
vmlinux.h를 생성하면 저 구조체들이 포함돼 있으므로 그걸 활용하는 것이 더 깔끔합니다.
$ grep -A 5 "struct sock {" vmlinux.h
struct sock {
struct sock_common __sk_common;
socket_lock_t sk_lock;
atomic_t sk_drops;
int sk_rcvlowat;
struct sk_buff_head sk_error_queue;
$ grep -A 60 "struct sock_common {" vmlinux.h
struct sock_common {
union {
__addrpair skc_addrpair;
struct {
__be32 skc_daddr;
__be32 skc_rcv_saddr;
};
};
union {
unsigned int skc_hash;
__u16 skc_u16hashes[2];
};
union {
__portpair skc_portpair;
struct {
__be16 skc_dport;
__u16 skc_num;
};
};
short unsigned int skc_family;
volatile unsigned char skc_state;
unsigned char skc_reuse: 4;
unsigned char skc_reuseport: 1;
unsigned char skc_ipv6only: 1;
unsigned char skc_net_refcnt: 1;
int skc_bound_dev_if;
union {
struct hlist_node skc_bind_node;
struct hlist_node skc_portaddr_node;
};
struct proto *skc_prot;
possible_net_t skc_net;
struct in6_addr skc_v6_daddr;
struct in6_addr skc_v6_rcv_saddr;
atomic64_t skc_cookie;
union {
long unsigned int skc_flags;
struct sock *skc_listener;
struct inet_timewait_death_row *skc_tw_dr;
};
int skc_dontcopy_begin[0];
union {
struct hlist_node skc_node;
struct hlist_nulls_node skc_nulls_node;
};
short unsigned int skc_tx_queue_mapping;
short unsigned int skc_rx_queue_mapping;
union {
int skc_incoming_cpu;
u32 skc_rcv_wnd;
u32 skc_tw_rcv_nxt;
};
refcount_t skc_refcnt;
int skc_dontcopy_end[0];
union {
u32 skc_rxhash;
u32 skc_window_clamp;
u32 skc_tw_snd_nxt;
};
};
그럼, 이걸 이용해 eBPF 내부에서 INET/INET6 패밀리만을 추적하도록 코드를 수정할 수 있습니다.
SEC("fentry/tcp_connect")
int BPF_PROG(tcp_connect, struct sock *sk)
{
if (sk->__sk_common.skc_family != AF_INET &&
sk->__sk_common.skc_family != AF_INET6)
{
return 0;
}
pid_t tgid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("fentry-tcp_connect: pid = %d\n", tgid);
return 0;
}
물론 저렇게 코딩하는 것은 libbcc의 방법에서 가능한 것이고,
libbpf에서라면 bpf_probe_read로 다음과 같이 접근해야 합니다.
// BPF_CORE_READ() - #include <bpf/bpf_core_read.h>
// https://nakryiko.com/posts/bpf-core-reference-guide/#bpf-core-read-1
unsigned short sk_family = BPF_CORE_READ(sk, __sk_common.skc_family);
PT_REGS_PARM1 ~ PT_REGS_PARM5 매크로를 사용한 경우, 또는 BPF_KPROBE 매크로에 의해 내부적으로 PT_REGS_PARM을 사용하는 경우,
SEC("kprobe/sys_clone") int BPF_KPROBE(kprobe_sys_clone, unsigned long clone_flags)
{
bpf_printk("clone_flags == %x\n", clone_flags);
return 0;
}
기존 bpf2go 명령어로 컴파일하면,
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go ebpf_basic basic.c
이런 오류가 발생합니다.
$ go generate
/mnt/c/temp/ebpf_sample/basic.c:20:29: error: The eBPF is using target specific macros, please provide -target that is not bpf, bpfel or bpfeb
20 | SEC("kprobe/sys_clone") int BPF_KPROBE(kprobe_sys_clone, unsigned long clone_flags)
| ^
/usr/include/bpf/bpf_tracing.h:429:20: note: expanded from macro 'BPF_KPROBE'
429 | return ____##name(___bpf_kprobe_args(args)); \
| ^
/usr/include/bpf/bpf_tracing.h:409:2: note: expanded from macro '___bpf_kprobe_args'
409 | ___bpf_apply(___bpf_kprobe_args, ___bpf_narg(args))(args)
| ^
/usr/include/bpf/bpf_helpers.h:165:29: note: expanded from macro '___bpf_apply'
165 | #define ___bpf_apply(fn, n) ___bpf_concat(fn, n)
| ^
note: (skipping 2 expansions in backtrace; use -fmacro-backtrace-limit=0 to see all)
/usr/include/bpf/bpf_tracing.h:399:33: note: expanded from macro '___bpf_kprobe_args1'
399 | ___bpf_kprobe_args0(), (void *)PT_REGS_PARM1(ctx)
| ^
/usr/include/bpf/bpf_tracing.h:309:29: note: expanded from macro 'PT_REGS_PARM1'
309 | #define PT_REGS_PARM1(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; })
| ^
:21:6: note: expanded from here
21 | GCC error "The eBPF is using target specific macros, please provide -target that is not bpf, bpfel or bpfeb"
| ^
1 error generated.
Error: compile: exit status 1
exit status 1
main.go:3: running "go": exit status 1
왜냐하면, 해당 매크로를 이용하게 되면 더 이상 endian 중립적인 코드를 사용할 수 없기 때문에 명시적으로 대상 플랫폼을 지정해야 하는데요, 이를 위해 "-target" 인자를 추가할 수 있습니다.
The eBPF is using target specific macros, please provide -target #772
; https://github.com/cilium/ebpf/discussions/772
//go2:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 ebpf_basic basic.c
가능한 platform 타깃은 bpf, bpfel, bpfeb, 386, amd64, arm, arm64, loong64, mips, ppc64, ppc64le, riscv64, s390x입니다.
참고로, arm64 타깃으로 컴파일하는 경우,
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target arm64 ebpf_basic basic.c
이런 오류가 발생합니다.
$ go generate
Compiled /mnt/c/temp/ebpf_sample/ebpf_basic_x86_bpfel.o
Stripped /mnt/c/temp/ebpf_sample/ebpf_basic_x86_bpfel.o
Wrote /mnt/c/temp/ebpf_sample/ebpf_basic_x86_bpfel.go
/mnt/c/temp/ebpf_sample/basic.c:18:33: error: incomplete definition of type 'struct user_pt_regs'
18 | unsigned long clone_flags = PT_REGS_PARM1(ctx);
| ^~~~~~~~~~~~~~~~~~
/usr/include/bpf/bpf_tracing.h:195:49: note: expanded from macro 'PT_REGS_PARM1'
195 | #define PT_REGS_PARM1(x) (((PT_REGS_ARM64 *)(x))->regs[0])
| ~~~~~~~~~~~~~~~~~~~~~~^
/mnt/c/temp/ebpf_sample/basic.c:18:33: note: forward declaration of 'struct user_pt_regs'
/usr/include/bpf/bpf_tracing.h:195:29: note: expanded from macro 'PT_REGS_PARM1'
195 | #define PT_REGS_PARM1(x) (((PT_REGS_ARM64 *)(x))->regs[0])
| ^
/usr/include/bpf/bpf_tracing.h:194:45: note: expanded from macro 'PT_REGS_ARM64'
194 | #define PT_REGS_ARM64 const volatile struct user_pt_regs
| ^
1 error generated.
Error: compile: exit status 1
exit status 1
main.go:4: running "go": exit status 1
ARM의 경우 CPU 레지스터 구조가 바뀌는데 이를 표현하기 위한 구조체가 user_pt_regs로 정의돼 있습니다. 위의 코드는 PT_REGS_ARM64 매크로가 user_pt_regs를 사용해 펼치긴 했지만 정작 그 구조체가 정의돼 있지 않아 컴파일 오류가 발생한 것입니다. 그런데, 이 오류를 해결하는 방법이 애매합니다. 아마도 ARM64 환경에서 저 빌드를 해야 하는 것 같은데요, 일단 user_pt_regs 구조체의 정의는 다음과 같습니다.
// https://github.com/torvalds/linux/blob/master/arch/arm64/include/uapi/asm/ptrace.h
struct user_pt_regs {
__u64 regs[31];
__u64 sp;
__u64 pc;
__u64 pstate;
};
PT_REGS_PARM 매크로를 이용해 인자를 포인터로 받으면,
int* parent_tidptr = PT_REGS_PARM3(ctx);
int value = *parent_tidptr;
이런 컴파일 오류가 발생합니다.
$ go generate
/mnt/c/temp/ebpf_sample/basic.c:28:10: error: incompatible integer to pointer conversion initializing 'int *' with an expression of type 'unsigned long' [-Wint-conversion]
28 | int* parent_tidptr = PT_REGS_PARM3(ctx);
| ^ ~~~~~~~~~~~~~~~~~~
1 error generated.
Error: compile: exit status 1
exit status 1
main.go:4: running "go": exit status 1
당연히 형변환이 필요한데요,
int* parent_tidptr = (int*)PT_REGS_PARM3(ctx);
문제는, 이렇게 포인터 변수로부터 값을 참조해 사용하면,
unsigned long clone_flags = PT_REGS_PARM1(ctx);
int* parent_tidptr = (int*)PT_REGS_PARM3(ctx);
int ptid = *parent_tidptr;
bpf_printk("clone_flags == %x, ptid == %x\n", clone_flags, ptid);
런타임 시 이런 오류가 발생합니다.
field KprobeSysClone: program kprobe_sys_clone: load program: permission denied: 2: (61) r7 = *(u32 *)(r1 +0): R1 invalid mem access 'scalar' (7 line(s) omitted)
이에 대해서 검색해 보면,
How to correctly read socket->sk from pt_regs* in ebpf program?
; https://stackoverflow.com/questions/76960866/how-to-correctly-read-socket-sk-from-pt-regs-in-ebpf-program
eBPF Verifier 입장에서는 저 포인터의 타입은 물론, 그것이 포인터인지조차도 알 수 없다고 합니다. 따라서 그런 식으로 포인터의 값을 읽을 수는 없고 별도로
bpf_probe_read_kernel/
bpf_probe_read_user 함수를 이용해야 한다고 합니다.
int ptid = 0;
int* parent_tidptr = (int*)PT_REGS_PARM3(ctx);
if (parent_tidptr != NULL)
{
bpf_probe_read_kernel(&ptid, sizeof(__u32), parent_tidptr);
}
BPF 함수는 매개변수의 수가 5개를 넘을 수 없습니다. 따라서 그 이상의 매개변수가 정의된 경우가 있다면,
// https://github.com/torvalds/linux/blob/ae90f6a6170d7a7a1aa4fddf664fbd093e3023bc/include/linux/syscalls.h#L786
unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long pgoff);
SEC("kprobe/ksys_mmap_pgoff") int BPF_KPROBE(ksys_mmap_pgoff, unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long pgoff)
{
bpf_printk("addr == %x\n", addr);
bpf_printk("len == %x\n", len);
bpf_printk("prot == %x\n", prot);
bpf_printk("flags == %x\n", flags);
bpf_printk("fd == %x\n", fd);
bpf_printk("pgoff == %x\n", pgoff);
return 0;
}
빌드 시 이런 오류가 발생합니다.
/mnt/c/temp/ebpf_sample/basic.c:26:35: error: call to undeclared function '___bpf_kprobe_args6'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
26 | SEC("kprobe/ksys_mmap_pgoff") int BPF_KPROBE(ksys_mmap_pgoff, unsigned long addr, unsigned long len,
| ^
/usr/include/bpf/bpf_tracing.h:429:20: note: expanded from macro 'BPF_KPROBE'
429 | return ____##name(___bpf_kprobe_args(args)); \
| ^
/usr/include/bpf/bpf_tracing.h:409:2: note: expanded from macro '___bpf_kprobe_args'
409 | ___bpf_apply(___bpf_kprobe_args, ___bpf_narg(args))(args)
| ^
/usr/include/bpf/bpf_helpers.h:165:29: note: expanded from macro '___bpf_apply'
165 | #define ___bpf_apply(fn, n) ___bpf_concat(fn, n)
| ^
/usr/include/bpf/bpf_helpers.h:162:29: note: expanded from macro '___bpf_concat'
162 | #define ___bpf_concat(a, b) a ## b
| ^
<scratch space>:36:1: note: expanded from here
36 | ___bpf_kprobe_args6
| ^
/mnt/c/temp/ebpf_sample/basic.c:26:63: error: expected expression
26 | SEC("kprobe/ksys_mmap_pgoff") int BPF_KPROBE(ksys_mmap_pgoff, unsigned long addr, unsigned long len,
| ^
/mnt/c/temp/ebpf_sample/basic.c:26:83: error: expected expression
26 | SEC("kprobe/ksys_mmap_pgoff") int BPF_KPROBE(ksys_mmap_pgoff, unsigned long addr, unsigned long len,
| ^
/mnt/c/temp/ebpf_sample/basic.c:27:61: error: expected expression
27 | unsigned long prot, unsigned long flags,
| ^
/mnt/c/temp/ebpf_sample/basic.c:27:81: error: expected expression
27 | unsigned long prot, unsigned long flags,
| ^
/mnt/c/temp/ebpf_sample/basic.c:28:61: error: expected expression
28 | unsigned long fd, unsigned long pgoff)
| ^
/mnt/c/temp/ebpf_sample/basic.c:28:79: error: expected expression
28 | unsigned long fd, unsigned long pgoff)
| ^
7 errors generated.
Error: compile: exit status 1
exit status 1
main.go:4: running "go": exit status 1
___bpf_kprobe_args6 정의가 없다는 것인데, 실제로 제가 빌드하는 환경에 있는 "/usr/include/bpf/bpf_tracing.h" 파일에 그 정의를 찾아볼 수 없습니다. 반면 libbpf 소스코드를 보면,
// https://github.com/libbpf/libbpf/blob/master/src/bpf_tracing.h
// ...[생략]...
#define ___bpf_kprobe_args6(x, args...) ___bpf_kprobe_args5(args), (unsigned long long)PT_REGS_PARM6(ctx)
#define ___bpf_kprobe_args7(x, args...) ___bpf_kprobe_args6(args), (unsigned long long)PT_REGS_PARM7(ctx)
#define ___bpf_kprobe_args8(x, args...) ___bpf_kprobe_args7(args), (unsigned long long)PT_REGS_PARM8(ctx)
이렇게 6 ~ 8개의 인자를 처리할 수 있는 것처럼 나오는데, 이게 버전에 따라 지원을 한다는 것인지는 잘 모르겠습니다. 어쨌든, (특별한 방법이 있는 것인지는 알 수 없으나) 5개를 초과해서는 매개변수 정의가 안 됩니다. 반면, 아키텍처 구조를 잘 알고 있다면 추출하는 것이 가능한 것 같은데요, 이에 관해서는 다음의 글을 참고하세요. ^^
Extracting kprobe parameters in eBPF
; https://eyakubovich.github.io/2022-04-19-ebpf-kprobe-params/
결국 저런 방법까지 동원할 생각이 없다면 5개 이하로만 매개변수를 정의해야 합니다.
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]