Microsoft MVP성태의 닷넷 이야기
Linux: 86. Golang + bpf2go를 사용한 eBPF 기본 예제 [링크 복사], [링크+제목 복사],
조회: 7706
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  128  129  130  131  132  133  [134]  135  ...
NoWriterDateCnt.TitleFile(s)
1739정성태8/24/201427737.NET Framework: 457. 교착상태(Dead-lock) 해결 방법 - Lock Leveling [2]파일 다운로드1
1738정성태8/23/201423441.NET Framework: 456. C# - CAS를 이용한 Lock 래퍼 클래스파일 다운로드1
1737정성태8/20/201420921VS.NET IDE: 93. Visual Studio 2013 동기화 문제
1736정성태8/19/201426911VC++: 79. [부연] CAS Lock 알고리즘은 과연 빠른가? [2]파일 다운로드1
1735정성태8/19/201419394.NET Framework: 455. 닷넷 사용자 정의 예외 클래스의 최소 구현 코드 - 두 번째 이야기
1734정성태8/13/201421147오류 유형: 237. Windows Media Player cannot access the file. The file might be in use, you might not have access to the computer where the file is stored, or your proxy settings might not be correct.
1733정성태8/13/201427493.NET Framework: 454. EmptyWorkingSet Win32 API를 사용하는 C# 예제파일 다운로드1
1732정성태8/13/201435810Windows: 99. INetCache 폴더가 다르게 보이는 이유
1731정성태8/11/201428283개발 환경 구성: 235. 점(.)으로 시작하는 파일명을 탐색기에서 만드는 방법
1730정성태8/11/201423468개발 환경 구성: 234. Royal TS의 터미널(Terminal) 연결에서 한글이 깨지는 현상 해결 방법
1729정성태8/11/201419462오류 유형: 236. SqlConnection - The requested Performance Counter is not a custom counter, it has to be initialized as ReadOnly.
1728정성태8/8/201431699.NET Framework: 453. C# - 오피스 파워포인트(Powerpoint) 파일을 WinForm에서 보는 방법파일 다운로드1
1727정성태8/6/201421900오류 유형: 235. SignalR 오류 메시지 - Counter 'Messages Bus Messages Published Total' does not exist in the specified Category. [2]
1726정성태8/6/201420705오류 유형: 234. IIS Express에서 COM+ 사용 시 SecurityException - "Requested registry access is not allowed" 발생
1725정성태8/6/201422652오류 유형: 233. Visual Studio 2013 Update3 적용 후 Microsoft.VisualStudio.Web.PageInspector.Runtime 모듈에 대한 FileNotFoundException 예외 발생
1724정성태8/5/201427450.NET Framework: 452. .NET System.Threading.Thread 개체에서 Native Thread Id를 구하는 방법 - 두 번째 이야기 [1]파일 다운로드1
1723정성태7/29/201459841개발 환경 구성: 233. DirectX 9 예제 프로젝트 빌드하는 방법 [3]파일 다운로드1
1722정성태7/25/201422182오류 유형: 232. IIS 500 Internal Server Error - NTFS 암호화된 폴더에 웹 애플리케이션이 위치한 경우
1721정성태7/24/201425482.NET Framework: 451. 함수형 프로그래밍 개념 - 리스트 해석(List Comprehension)과 순수 함수 [2]
1720정성태7/23/201423455개발 환경 구성: 232. C:\WINDOWS\system32\LogFiles\HTTPERR 폴더에 로그 파일을 남기지 않는 설정
1719정성태7/22/201427337Math: 13. 동전을 여러 더미로 나누는 경우의 수 세기(Partition Number) - 두 번째 이야기파일 다운로드1
1718정성태7/19/201436779Math: 12. HTML에서 수학 관련 기호/수식을 표현하기 위한 방법 - MathJax.js [4]
1716정성태7/17/201436484개발 환경 구성: 231. PC 용 무료 안드로이드 에뮬레이터 - genymotion
1715정성태7/13/201431573기타: 47. 운영체제 종료 후에도 USB 외장 하드의 전원이 꺼지지 않는 경우 [3]
1714정성태7/11/201421571VS.NET IDE: 92. Visual Studio 2013을 지원하는 IL Support 확장 도구
1713정성태7/11/201445343Windows: 98. 윈도우 시스템 디스크 용량 확보를 위한 "Package Cache" 폴더 이동 [1]
... 121  122  123  124  125  126  127  128  129  130  131  132  133  [134]  135  ...