Microsoft MVP성태의 닷넷 이야기
Linux: 86. Golang + bpf2go를 사용한 eBPF 기본 예제 [링크 복사], [링크+제목 복사],
조회: 6112
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)
(시리즈 글이 14개 있습니다.)
Linux: 86. Golang + bpf2go를 사용한 eBPF 기본 예제
; https://www.sysnet.pe.kr/2/0/13769

Linux: 94. eBPF - vmlinux.h 헤더 포함하는 방법 (bpf2go에서 사용)
; https://www.sysnet.pe.kr/2/0/13783

Linux: 95. eBPF - kprobe를 이용한 트레이스
; https://www.sysnet.pe.kr/2/0/13784

Linux: 96. eBPF (bpf2go) - fentry, fexit를 이용한 트레이스
; https://www.sysnet.pe.kr/2/0/13788

Linux: 100.  eBPF의 2가지 방식 - libbcc와 libbpf(CO-RE)
; https://www.sysnet.pe.kr/2/0/13801

Linux: 103. eBPF (bpf2go) - Tracepoint를 이용한 트레이스 (BPF_PROG_TYPE_TRACEPOINT)
; https://www.sysnet.pe.kr/2/0/13810

Linux: 105. eBPF - bpf2go에서 전역 변수 설정 방법
; https://www.sysnet.pe.kr/2/0/13815

Linux: 106. eBPF / bpf2go - (BPF_MAP_TYPE_HASH) Map을 이용한 전역 변수 구현
; https://www.sysnet.pe.kr/2/0/13817

Linux: 107. eBPF - libbpf CO-RE의 CONFIG_DEBUG_INFO_BTF 빌드 여부에 대한 의존성
; https://www.sysnet.pe.kr/2/0/13819

Linux: 109. eBPF / bpf2go - BPF_PERF_OUTPUT / BPF_MAP_TYPE_PERF_EVENT_ARRAY 사용법
; https://www.sysnet.pe.kr/2/0/13824

Linux: 110. eBPF / bpf2go - BPF_RINGBUF_OUTPUT / BPF_MAP_TYPE_RINGBUF 사용법
; https://www.sysnet.pe.kr/2/0/13825

Linux: 115. eBPF (bpf2go) - ARRAY / HASH map 기본 사용법
; https://www.sysnet.pe.kr/2/0/13893

Linux: 116. eBPF / bpf2go - BTF Style Maps 정의 구문과 데이터 정렬 문제
; https://www.sysnet.pe.kr/2/0/13894

Linux: 117. eBPF / bpf2go - Map에 추가된 요소의 개수를 확인하는 방법
; https://www.sysnet.pe.kr/2/0/13895




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} {}}

(또한, SELinux가 설치된 환경이라면 데몬으로 실행하는 경우에는 별도 보안 설정이 필요합니다.)





이제 남은 작업은, 리눅스 커널에 해당 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




만약 이런 오류가 발생한다면?

$ go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 ebpf_basic basic.c
Error: missing package, you should either set the go-package flag or the GOPACKAGE env
exit status 1

오류 메시지 그대로 "GOPACKAGE" 환경 변수로 빌드 대상이 되는 Go 프로젝트의 패키지 이름을 지정하면 됩니다.

$ GOPACKAGE=ebpf_sample go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 ebpf_basic basic.c

// 또는 bpf2go의 -go-package 인자로 설정
$ go run github.com/cilium/ebpf/cmd/bpf2go -go-package ebpf_sample -target amd64 ebpf_basic basic.c

그래도 이런 오류가 발생한다면?

$ 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

(잘 동작합니다. 저렇게 해서 잘 동작하는지는 확인하지 못했습니다. ^^)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/15/2025]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 16  17  18  19  20  21  22  23  24  25  26  27  28  [29]  30  ...
NoWriterDateCnt.TitleFile(s)
13212정성태1/8/202313542기타: 85. 단정도/배정도 부동 소수점의 정밀도(Precision)에 따른 형변환 손실
13211정성태1/6/202313299웹: 42. (https가 아닌) http 다운로드를 막는 웹 브라우저
13210정성태1/5/202312068Windows: 219. 윈도우 x64의 경우 0x00000000`7ffe0000 아래의 주소는 왜 사용하지 않을까요?
13209정성태1/4/202311504Windows: 218. 왜 윈도우에서 가상 메모리 공간은 64KB 정렬이 된 걸까요?
13208정성태1/3/202312353.NET Framework: 2086. C# - Windows 운영체제의 2MB Large 페이지 크기 할당 방법파일 다운로드1
13207정성태12/26/202212813.NET Framework: 2085. C# - gpedit.msc의 "User Rights Assignment" 특권을 코드로 설정/해제하는 방법 [1]파일 다운로드1
13206정성태12/24/202212994.NET Framework: 2084. C# - GetTokenInformation으로 사용자 SID(Security identifiers) 구하는 방법 [4]파일 다운로드1
13205정성태12/24/202212180.NET Framework: 2083. C# - C++과의 연동을 위한 구조체의 fixed 배열 필드 사용 (2)파일 다운로드1
13204정성태12/22/202211347.NET Framework: 2082. C# - (LSA_UNICODE_STRING 예제로) CustomMarshaler 사용법파일 다운로드1
13203정성태12/22/202211776.NET Framework: 2081. C# Interop 예제 - (LSA_UNICODE_STRING 예제로) 구조체를 C++에 전달하는 방법파일 다운로드1
13202정성태12/21/202213123기타: 84. 직렬화로 설명하는 Little/Big Endian파일 다운로드1
13201정성태12/20/202214235오류 유형: 835. PyCharm 사용 시 C 드라이브 용량 부족
13200정성태12/19/202212226오류 유형: 834. 이벤트 로그 - SSL Certificate Settings created by an admin process for endpoint
13199정성태12/19/202212279개발 환경 구성: 656. Internal Network 유형의 스위치로 공유한 Hyper-V의 VM과 호스트가 통신이 안 되는 경우
13198정성태12/18/202212517.NET Framework: 2080. C# - Microsoft.XmlSerializer.Generator 처리 없이 XmlSerializer 생성자를 예외 없이 사용하고 싶다면?파일 다운로드1
13197정성태12/17/202212049.NET Framework: 2079. .NET Core/5+ 환경에서 XmlSerializer 사용 시 System.IO.FileNotFoundException 예외 발생하는 경우파일 다운로드1
13196정성태12/16/202212655.NET Framework: 2078. .NET Core/5+를 위한 SGen(Microsoft.XmlSerializer.Generator) 사용법
13195정성태12/15/202213007개발 환경 구성: 655. docker - bridge 네트워크 모드에서 컨테이너 간 통신 시 --link 옵션 권장 이유
13194정성태12/14/202213281오류 유형: 833. warning C4747: Calling managed 'DllMain': Managed code may not be run under loader lock파일 다운로드1
13193정성태12/14/202213415오류 유형: 832. error C7681: two-phase name lookup is not supported for C++/CLI or C++/CX; use /Zc:twoPhase-
13192정성태12/13/202213340Linux: 55. 리눅스 - bash shell에서 실수 연산
13191정성태12/11/202215256.NET Framework: 2077. C# - 직접 만들어 보는 SynchronizationContext파일 다운로드1
13190정성태12/9/202216735.NET Framework: 2076. C# - SynchronizationContext 기본 사용법파일 다운로드1
13189정성태12/9/202216873오류 유형: 831. Visual Studio - Windows Forms 디자이너의 도구 상자에 컨트롤이 보이지 않는 문제
13188정성태12/9/202215289.NET Framework: 2075. C# - 직접 만들어 보는 TaskScheduler 실습 (SingleThreadTaskScheduler)파일 다운로드1
13187정성태12/8/202215261개발 환경 구성: 654. openssl - CA로부터 인증받은 새로운 인증서를 생성하는 방법 (2)
... 16  17  18  19  20  21  22  23  24  25  26  27  28  [29]  30  ...