Golang + bpf2go를 사용한 eBPF 기본 예제
Golang의 경우 cilium repo에서 bpf2go라는 프로그램이 제공돼,
cilium/ebpf - ebpf-go is a pure-Go library to read, modify and load eBPF programs and attach them to various hooks in the Linux kernel
; https://github.com/cilium/ebpf
꽤나 간편하게 eBPF 프로그램을 작성할 수 있는데요, 이번 글에서는 위의 문서에 나온 "
Getting Started" 내용을 간단하게 실습해 보겠습니다.
Go 프로젝트를 하나 만들고, 다음의 eBPF C 코드 파일을 하나 추가해줍니다.
//go:build ignore
typedef unsigned long long u64;
typedef unsigned int u32;
#include <linux/bpf.h>
#include <linux/types.h>
#include <bpf/bpf_helpers.h>
SEC("kprobe/sys_clone")
int kprobe_sys_clone(void *ctx)
{
u64 bpf_id = bpf_get_current_pid_tgid();
u32 tgid = bpf_id >> 32;
u32 pid = bpf_id;
bpf_printk("pid == %d, thread_id == %d\n", tgid, pid);
return -1;
}
char __license[] SEC("license") = "GPL";
보는 바와 같이, 그냥 "Hello World" 성격의 eBPF 예제에 불과합니다. 위의 코드를 bpf2go를 이용하면 GoLang에서 바로 사용할 수 있는 object 파일과 소스코드가 생성되는데요, 문서에 따라 다음과 같이 실행해 볼 수 있습니다.
// Go 예제 패키지의 이름이 ebpf_sample이라고 가정
// 위에서 소개한 eBPF C 코드 파일명은 basic.c, 그것을 bpf2go로 컴파일해 생성할 Go 파일명을 ebpf_basic으로 지정
$ GOPACKAGE=ebpf_sample go run github.com/cilium/ebpf/cmd/bpf2go ebpf_basic basic.c
Compiled /mnt/c/temp/ebpf_sample/ebpf_basic_bpfel.o
Stripped /mnt/c/temp/ebpf_sample/ebpf_basic_bpfel.o
Wrote /mnt/c/temp/ebpf_sample/ebpf_basic_bpfel.go
Compiled /mnt/c/temp/ebpf_sample/ebpf_basic_bpfeb.o
Stripped /mnt/c/temp/ebpf_sample/ebpf_basic_bpfeb.o
Wrote /mnt/c/temp/ebpf_sample/ebpf_basic_bpfeb.go
...el, ...eb 파일이 있는데, 각각의 .go 파일을 열어보면 상단에 다음과 같은 내용이 나옵니다.
// Code generated by bpf2go; DO NOT EDIT.
//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64
...[생략]...
// Code generated by bpf2go; DO NOT EDIT.
//go:build mips || mips64 || ppc64 || s390x
...[생략]...
이에 대해서는 아래의 문서에 나오는데요,
Shipping Portable eBPF-powered Applications
; https://ebpf-go.dev/guides/portable-ebpf/
- _bpfel.o and *_bpfel.go for little-endian architectures like amd64, arm64, riscv64 and loong64
- _bpfeb.o and *_bpfeb.go for big-endian architectures like s390(x), mips and sparc
따라서 대개의 경우 (amd64, arm64를 모두 커버하는) ...el 파일만 있어도 충분할 것입니다. 자, 그럼 이것을 가지고 테스트를 해볼까요?
package main
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go ebpf_basic basic.c
import (
"fmt"
)
func main() {
var bpfObj ebpf_basicObjects // bpf2go로 자동 생성된 타입
err := loadEbpf_basicObjects(&bpfObj, nil) // bpf2go로 자동 생성된 함수
if err != nil {
fmt.Printf("objs == null, %v\n", err)
return
}
defer func(bpfObj *ebpf_basicObjects) {
_ = bpfObj.Close()
}(&bpfObj)
fmt.Printf("%v\n", bpfObj)
}
빌드 후 그냥 실행해 보면 이런 출력이 나옵니다.
objs == null, field ClsMain: program cls_main: load program: operation not permitted (MEMLOCK may be too low, consider rlimit.RemoveMemlock)
오류 메시지의 권한 문제뿐만 아니라, 함께 출력된 MEMLOCK 관련 제한을 해제하기 위해 다음과 같이 코드를 추가할 수 있는데요,
// import (
// ...[생략]...
// "github.com/cilium/ebpf/rlimit"
// )
if err := rlimit.RemoveMemlock(); err != nil {
log.Fatal("Removing memlock:", err)
}
// 실행 결과:
// Removing memlock:failed to set memlock rlimit: operation not permitted
저것 자체도 권한 문제로 실패합니다. eBPF는 나름 커널을 건드리는 만큼 권한이 필요하다는 것은 어쩔 수 없으니 sudo를 이용해 실행해야 합니다.
$ sudo ./ebpf_sample
loaded: {{Kprobe(kprobe_sys_clone)#7} {}}
이제 남은 작업은, 리눅스 커널에 해당 eBPF 코드를 연결하는 것인데요, 이것은 link ("github.com/cilium/ebpf/link") 패키지를 이용하면 됩니다.
이번 글에서 만든 eBPF 코드는 kprobe를 사용하므로, 이렇게 완성할 수 있습니다.
const (
kprobeFunc = "sys_clone"
)
// ...[생략]...
// kprobe 연결
kp, err := link.Kprobe(kprobeFunc, bpfObj.KprobeSysClone, nil)
if err != nil {
fmt.Printf("kp == null, %v\n", err)
}
defer func(kp link.Link) {
_ = kp.Close()
}(kp)
fmt.Printf("link.Kprobe: %v\n", kp)
// 종료하지 못하도록 대기
fmt.Println("Press any key to exit...")
input := bufio.NewScanner(os.Stdin)
input.Scan()
위의 코드를 실행하면 이전에 작성했던 basic.c 파일의 eBPF 코드가 동작하게 됩니다.
$ sudo ./ebpf_sample
loaded: {{Kprobe(kprobe_sys_clone)#7} {}}
link.Kprobe: &{{0xc0001860a0 } 0xc000014030}
Press any key to exit...
따라서, 이제 해당 리눅스 시스템에서 sys_clone 커널 함수가 호출될 때마다 kprobe로 연결한 eBPF 코드가 실행될 텐데요, 이에 대한 확인을 bpf_printk 코드로 심었던 출력으로 알 수 있습니다.
bpf_printk가 보낸 출력은 /sys/kernel/debug/tracing/trace_pipe 파일에 전달되므로 다른 터미널을 하나 띄워 cat 명령어를 수행하면,
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
bash-2178834 [010] ....1 351198.765874: bpf_trace_printk: pid == 2178834, thread_id == 2178834
bash-2178834 [010] ....1 351198.766143: bpf_trace_printk: pid == 2178834, thread_id == 2178834
...clone이 수행될 때마다 메시지 출력...
위와 같은 식의 메시지를 확인할 수 있습니다. 실제로 pid, tid가 정상적으로 출력되는지 테스트를 해볼까요? ^^
이를 위해 우선 bash shell을 하나 더 띄우고, 그것의 pid를 알아낸 다음,
// 터미널 A: bash
$ echo $$
596613
다른 터미널에서 저 pid로 trace_pipe 파일을 확인하는 명령어를 수행해 둡니다.
// 터미널 B: trace_pipe 확인
$ sudo cat /sys/kernel/debug/tracing/trace_pipe | grep 596613
이 상태에서 "터미널 A"로 돌아가 아무 명령어나 실행하면,
$ ls
"터미널 B"에서는 pid == 596613에 속한 메시지가 뜨는 것을 확인할 수 있습니다.
bash-596613 [003] ...21 411689.876366: bpf_trace_printk: pid == 596613, thread_id == 596613
bash-596613 [003] ...21 411689.878053: bpf_trace_printk: pid == 596613, thread_id == 596613
(위의 실습을
WSL 환경에서 해보면 pid가 존재하지 않습니다.)
참고로, 자주 eBPF 코드를 수정하는 경우에는 이런 식으로 명령어를 만들어 두면 편리할 것입니다.
$ go generate && go build && sudo ./ebpf_sample
go generate는 현재 go 패키지에 포함된 소스코드 내에 명시한 "//go:generate ..."를 모두 실행해 주는 역할을 합니다.
What is go:generate and how to use it ?
; https://www.practical-go-lessons.com/post/what-is-go-generate-and-how-to-use-it-ccava8v5toqc70ipi1jg
제가 작성한 go 소스코드를 보면,
package main
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go ebpf_basic basic.c
import (
"fmt"
)
func main() {
// ...[생략]...
}
"go run github.com/cilium/ebpf/cmd/bpf2go ebpf_basic basic.c" 명령을 수행하라고 지시하는데요, 결국 명령행에서 다음과 같은 식으로 수행하는 것과 다를 바가 없습니다.
// bpf2go 유틸리티 사용법
// 첫 번째 인자(출력): bpf2go가 생성할 파일명(임의의 파일명)
// 두 번째 인자(입력): eBPF 소스코드 파일명
$ bpf2go ebpf_basic basic.c
그다음 수행하는 "go build"의 경우 "basic.c" 파일이 패키지에 포함된 상태에서는 자칫 빌드를 할 수 없다는 오류가 발생할 수 있습니다.
package ebpf_sample: C source files not allowed when not using cgo or SWIG: basic.c
위와 같은 오류를 없애기 위해, basic.c 파일은 go 빌드에서 제외하라고 해당 파일 내에 다음과 같은 지시자를 포함하는 것입니다.
//go:build ignore
typedef unsigned long long u64;
typedef unsigned int u32;
#include <linux/bpf.h>
// ...[생략]...
대충 여기까지 잘 이해하셨다면 이제 다음의 글을 쉽게 따라할 수 있을 것입니다. ^^
Getting Started with eBPF in Go
; https://ebpf-go.dev/guides/getting-started/
An Applied Introduction to eBPF with Go
; https://edgedelta.com/company/blog/applied-introduction-ebpf-go
이런 오류가 발생한다면?
$ GOPACKAGE=ebpf_sample go run github.com/cilium/ebpf/cmd/bpf2go ebpf_basic basic.c
In file included from /home/testusr/ebpf_sample/basic.c:4:
In file included from /usr/include/linux/bpf.h:11:
/usr/include/linux/types.h:5:10: fatal error: 'asm/types.h' file not found
#include
^~~~~~~~~~~~~
1 error generated.
Error: compile: exit status 1
exit status 1
main.go:3: running "go": exit status 1
다음의 글을 보시면 됩니다.
WSL / Ubuntu - /usr/include/linux/types.h:5:10: fatal error: 'asm/types.h' file not found
; https://www.sysnet.pe.kr/2/0/13761
이런 오류가 발생한다면?
$ go generate
/home/testusr/ebpf_sample/basic.c:5:10: fatal error: 'bpf/bpf_helpers.h' file not found
#include
^~~~~~~~~~~~~~~~~~~
1 error generated.
Error: compile: exit status 1
exit status 1
main.go:3: running "go": exit status 1
"libbpf-dev" 패키지를 설치합니다.
$ sudo apt install libbpf-dev
$ ll /usr/include/bpf/bpf_helpers.h
-rw-r--r-- 1 root root 7679 Aug 19 2022 /usr/include/bpf/bpf_helpers.h
역시나 아래와 같은 오류도,
$ go generate
Error: exec: "llvm-strip": executable file not found in $PATH
exit status 1
main.go:3: running "go": exit status 1
관련된 패키지를 설치해 줍니다.
$ sudo apt install clang llvm
$ apt list -a llvm
Listing... Done
llvm/jammy,now 1:14.0-55~exp2 amd64 [installed]
제가 아직 리알못이라 이해가 안 되는 점이 하나 있는데요, bpf_printk로 출력한 메시지를 확인할 떄 tail -f를 썼더니 메시지가 아무 것도 안 나옵니다.
// 아래의 명령으로는 bpf_printk 출력이 안 뜸
$ sudo tail -f /sys/kernel/debug/tracing/trace_pipe
// 아래의 명령으로는 확인 가능
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
혹시 이유를 아시는 분이 계실까요? ^^
참고로, 관련 traace 경로는 각각 tracefs와 debugfs가 마운트된 경로라고 합니다.
$ mount -t tracefs
tracefs on /sys/kernel/tracing type tracefs (rw,nosuid,nodev,noexec,relatime)
tracefs on /sys/kernel/debug/tracing type tracefs (rw,nosuid,nodev,noexec,relatime)
$ mount -t debugfs
debugfs on /sys/kernel/debug type debugfs (rw,nosuid,nodev,noexec,relatime)
따라서 해당 경로에 대한 마운트가 돼 있지 않다면,
$ mount -t tracefs
$ mount -t debugfs
$
이런 식으로 직접 마운트를 할 수 있습니다.
$ sudo mount -t tracefs tracefs /sys/kernel/tracing
$ sudo mount -t debugfs debugfs /sys/kernel/debug
$ sudo mount -t tracefs tracefs /sys/kernel/debug/tracing
$ mount -t tracefs
tracefs on /sys/kernel/tracing type tracefs (rw,relatime)
tracefs on /sys/kernel/debug/tracing type tracefs (rw,relatime)
$ mount -t debugfs
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
하지만, 저 설정은 재부팅후 사라지므로, fstab 파일에 영구적으로 추가해 두는 것이 좋습니다.
# cat /etc/fstab
LABEL=cloudimg-rootfs / ext4 defaults 0 1
debugfs /sys/kernel/debug debugfs defaults
tracefs /sys/kernel/tracing tracefs defaults
tracefs /sys/kernel/debug/tracing tracefs defaults
(잘 동작합니다.
저렇게 해서 잘 동작하는지는 확인하지 못했습니다. ^^)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]