Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 

(시리즈 글이 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




eBPF / bpf2go - (BPF_MAP_TYPE_HASH) Map을 이용한 전역 변수 구현

자, 이제 본격적으로 Map에 대한 이야기를 해볼까요? ^^

eBPF에서 Map이란 용어는 Dictionary의 의미가 아니라 "Memory Map"에서의 Map이라고 보시면 됩니다. 즉, Ring 0 레벨의 커널 측에서도 접근할 수 있고 Ring 3 레벨의 사용자 코드에서도 접근할 수 있는 메모리 영역을 의미합니다.

그렇다면, 1개의 항목만 담을 수 있는 Map을 정의한다면 그것이 곧 "전역 변수"와 다를 바가 없는데요,

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, uint32_t);
    __type(value, uint32_t);
    __uint(max_entries, 1);
} my_hash_map SEC(".maps");

위와 같이 max_entries == 1인 Map 정의로 전역 변수를 흉내 낼 수 있는 것입니다. 자, 그럼 정의한 구문을 천천히 뜯어볼까요? 우선, type == BPF_MAP_TYPE_HASH로 설정한 것은, 다양한 용도의 Map 구조가 정의돼 있지만 여기서는 "HASH" 성격을 갖는 Map을 정의하겠다는 것입니다.

현재 eBPF가 지원하는 모든 Map 타입은 아래의 문서에서 확인할 수 있습니다.

Map types (Linux)
; https://docs.ebpf.io/linux/map-type/

그다음, key와 value는 각각 uint32_t 타입으로 준 것이고, 이렇게 구성한 Map의 이름을 "my_hash_map"으로 ".maps" 섹션에 정의했습니다. (Map은 반드시 ".maps" 섹션에 있어야 합니다.)

이렇게 정의했으면, CRUD 연산을 할 수 있어야죠? ^^

eBPF에서 Map에 대해 지원하는 (연산) 함수는 아래의 문서에서 확인할 수 있습니다.

Map helpers
; https://docs.ebpf.io/linux/helper-function/

예를 들어, 위의 목록 중에 bpf_map_lookup_elem을 선택해 볼까요?

Helper function bpf_map_lookup_elem
; https://docs.ebpf.io/linux/helper-function/bpf_map_lookup_elem/

위의 문서에서 확인할 수 있듯이, 함수에 따라 사용할 수 있는 eBPF 프로그램 타입이 정의된 것을 알 수 있는데요, 아래는 bpf_map_lookup_elem을 사용할 수 있는 프로그램 유형입니다.

Program types 
This helper call can be used in the following program types:

    BPF_PROG_TYPE_CGROUP_DEVICE
    BPF_PROG_TYPE_CGROUP_SKB
    BPF_PROG_TYPE_CGROUP_SOCK
    BPF_PROG_TYPE_CGROUP_SOCKOPT
    BPF_PROG_TYPE_CGROUP_SOCK_ADDR
    BPF_PROG_TYPE_CGROUP_SYSCTL
    BPF_PROG_TYPE_FLOW_DISSECTOR
    BPF_PROG_TYPE_KPROBE
    BPF_PROG_TYPE_LIRC_MODE2
    BPF_PROG_TYPE_LSM
    BPF_PROG_TYPE_LWT_IN
    BPF_PROG_TYPE_LWT_OUT
    BPF_PROG_TYPE_LWT_SEG6LOCAL
    BPF_PROG_TYPE_LWT_XMIT
    BPF_PROG_TYPE_NETFILTER
    BPF_PROG_TYPE_PERF_EVENT
    BPF_PROG_TYPE_RAW_TRACEPOINT
    BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE
    BPF_PROG_TYPE_SCHED_ACT
    BPF_PROG_TYPE_SCHED_CLS
    BPF_PROG_TYPE_SK_LOOKUP
    BPF_PROG_TYPE_SK_MSG
    BPF_PROG_TYPE_SK_REUSEPORT
    BPF_PROG_TYPE_SK_SKB
    BPF_PROG_TYPE_SOCKET_FILTER
    BPF_PROG_TYPE_SOCK_OPS
    BPF_PROG_TYPE_STRUCT_OPS
    BPF_PROG_TYPE_SYSCALL
    BPF_PROG_TYPE_TRACEPOINT
    BPF_PROG_TYPE_TRACING
    BPF_PROG_TYPE_XDP

우리가 정의한 eBPF 코드는 (BPF_PROG_TYPE_TRACEPOINT에 해당하는) tracepoint이므로,

SEC("tracepoint/syscalls/sys_enter_close")
int sys_enter_close(struct trace_event_raw_sys_enter *ctx) {
    // ...[생략]...
}

bpf_map_lookup_elem을 사용하는데 문제가 없습니다. 하지만, 또 하나의 제약이 있는데요, 즉 bpf_map_lookup_elem을 사용할 수 있는 Map의 타입도 정해져 있다는 점입니다. 역시 문서에 나오듯이,

Map types
This helper call can be used with the following map types:

    BPF_MAP_TYPE_ARRAY
    BPF_MAP_TYPE_ARRAY_OF_MAPS
    BPF_MAP_TYPE_HASH
    BPF_MAP_TYPE_HASH_OF_MAPS
    BPF_MAP_TYPE_LPM_TRIE
    BPF_MAP_TYPE_LRU_HASH
    BPF_MAP_TYPE_LRU_PERCPU_HASH
    BPF_MAP_TYPE_PERCPU_ARRAY
    BPF_MAP_TYPE_PERCPU_HASH
    BPF_MAP_TYPE_SOCKHASH
    BPF_MAP_TYPE_SOCKMAP
    BPF_MAP_TYPE_XSKMAP

bpf_map_lookup_elem은 우리가 만든 BPF_MAP_TYPE_HASH를 지원하므로 코드에서 사용이 가능합니다. 그렇게 확인이 끝났으면, 이제 문서에 따라 다음과 같은 식으로 코드를 작성하면 됩니다.

SEC("tracepoint/syscalls/sys_enter_close")
int sys_enter_close(struct trace_event_raw_sys_enter *ctx) {

    uint32_t key    = 0;
    uint32_t *arg_from_userspace = bpf_map_lookup_elem(&my_hash_map, &key);

    uint32_t arg_value = 0;

    if (arg_from_userspace) {
        arg_value = *arg_from_userspace;
    }

    bpf_printk("sys_enter_close called: %d", arg_value);

    return 0;
}

위의 코드는, 나중에 go 측에서 my_hash_map에 값을 쓰게 되면 lookup이 성공해 그 값을 화면에 로그로 찍어주는 역할을 합니다.




go 측에서도 연동을 해야겠죠? ^^ bpf2go 덕분에, 해당 Map도 자동 생성된 코드 안에 포함돼 다음과 같이 아주 간단하게 처리할 수 있습니다.

// ... [생략]...

var bpfObj ebpf_basicObjects
err = spec.LoadAndAssign(&bpfObj, &opts)

// ... [생략]...

go UpdateMap(bpfObj)

func UpdateMap(bpfObj ebpf_basicObjects) {

    value := uint32(0)

    for {
        _ = bpfObj.MyHashMap.Update(uint32(0), &value, ebpf.UpdateAny)
        value++

        time.Sleep(1 * time.Second)
    }
}

테스트를 위해 1초마다 증가시키도록 했으므로, 이제 실행해 보면 화면에 이런 식의 출력을 볼 수 있습니다.

$ sudo cat /sys/kernel/debug/tracing/trace_pipe
...[생략]...
systemd-oomd-782     [004] ...21 239675.678467: bpf_trace_printk: sys_enter_close called: 0
systemd-oomd-782     [004] ...21 239675.928435: bpf_trace_printk: sys_enter_close called: 1
...[생략]...

참고로, bpfObj.MyHashMap.Update는 eBPF에서는 bpf_map_update_elem에 해당하는 함수입니다.

결국 BPF_MAP_TYPE_HASH 타입에 대한 CRUD 연산을 bpf_map_lookup_elem, bpf_map_update_elem, bpf_map_delete_elem 등의 함수를 통해 수행할 수 있고, 위의 예제에서는 user space에서 커널로 데이터를 전달했지만 반대로 커널에서 user space로 데이터를 전달할 때도 사용할 수 있습니다.




cilium github 코드를 보면,

examples/kprobe/kprobe.c
; https://github.com/cilium/ebpf/blob/main/examples/kprobe/kprobe.c

아래와 같은 사용 예제가 있는데요,

//go:build ignore

#include "common.h"

char __license[] SEC("license") = "Dual MIT/GPL";

struct bpf_map_def SEC("maps") kprobe_map = {
	.type        = BPF_MAP_TYPE_ARRAY,
	.key_size    = sizeof(u32),
	.value_size  = sizeof(u64),
	.max_entries = 1,
};

SEC("kprobe/sys_execve")
int kprobe_execve() {
	u32 key     = 0;
	u64 initval = 1, *valp;

	valp = bpf_map_lookup_elem(&kprobe_map, &key);
	if (!valp) {
		bpf_map_update_elem(&kprobe_map, &key, &initval, BPF_ANY);
		return 0;
	}
	__sync_fetch_and_add(valp, 1);

	return 0;
}

값을 atomic하게 증가시켜 주는 __sync_fetch_and_add 함수 사용법을 익혀 두면 좋겠습니다. ^^




(이하, 오류 상황에 따른 해결 방법입니다.)

아래의 예제에 따라,

ebpf/testdata/loader.c
; https://github.com/cilium/ebpf/blob/main/testdata/loader.c#L8

SEC 매크로가 아닌 __section을 직접 지정해 보면,

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, uint32_t);
    __type(value, uint32_t);
    __uint(max_entries, 1);
} my_hash_map __section(".maps");

go generate로 자동 생성 코드를 만들 때 이런 오류가 발생합니다.

$ go generate
/mnt/c/temp/ebpf_sample/basic.c:22:2: error: expected ';' after struct
   22 | } my_hash_map __section(".maps");
      |  ^
/mnt/c/temp/ebpf_sample/basic.c:22:3: error: unknown type name 'my_hash_map'
   22 | } my_hash_map __section(".maps");
      |   ^
/mnt/c/temp/ebpf_sample/basic.c:22:25: error: expected parameter declarator
   22 | } my_hash_map __section(".maps");
      |                         ^
/mnt/c/temp/ebpf_sample/basic.c:22:25: error: expected ')'
/mnt/c/temp/ebpf_sample/basic.c:22:24: note: to match this '('
   22 | } my_hash_map __section(".maps");
      |                        ^
4 errors generated.
Error: compile: exit status 1
exit status 1
main.go:15: running "go": exit status 1

검색해 보면, SEC 매크로는 "__attribute__((section("name"), used))"로 펼쳐진다고 합니다. 실제로 아래와 같이 정의하면 아무 문제 없이 빌드가 잘됩니다.

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, uint32_t);
    __type(value, uint32_t);
    __uint(max_entries, 1);
} my_hash_map __attribute__((section(".maps"), used));




본문에서 정의한 맵을 go 측에서 bpf_map_lookup_elem으로 이런 식으로 값을 받아오려고 하면,

for {
    valueOut := 0
    err := bpfObj.MyHashMap.Lookup(0, valueOut)
    if err != nil {
        fmt.Printf("MyHashMap: %v\n", err)
    } else {
        fmt.Printf("MyHashMap: %v\n", valueOut)
    }

    time.Sleep(1 * time.Second)
}

오류가 발생합니다.

can't marshal key: binary.Write: some values are not fixed-sized in type int

왜냐하면, key로 넘긴 '0' 리터럴 값이 uint32_t 타입이 아니기 때문입니다. 따라서 정확히 uint32 타입으로 지정해 전달해야만 해당 오류가 없어집니다.

err := bpfObj.MyHashMap.Lookup(uint32(0), valueOut)

하지만, 그래도 다음과 같은 오류가 이어서 발생합니다.

binary.Read: invalid type int

이번엔 valueOut의 타입이 문제인 건데요, 이것도 (map의 정의에서 uint32_t라고 지정했으므로) uint32로 명시해야 하지만,

valueOut := uint32(0)
err := bpfObj.MyHashMap.Lookup(uint32(0), valueOut)

// 에러: binary.Read: invalid type uint32

그래도 오류가 발생합니다. 비록 Lookup이 value의 타입을 interface{}로 받긴 해도 eBPF의 스타일(?)에 따라 valueOut은 포인터로 전달해야 합니다.

valueOut := uint32(0)
err := bpfObj.MyHashMap.Lookup(uint32(0), &valueOut)




아래와 같은 식으로 eBPF 코드를 작성하면,

uint32_t key    = 0;
uint32_t *arg_from_userspace = bpf_map_lookup_elem(&my_hash_map, &key);

bpf_printk("sys_enter_close called: %d", *arg_from_userspace);

go 측에서 로딩 시 이런 오류가 발생합니다.

program sys_enter_close: load program: permission denied: 36: (61) r4 = *(u32 *)(r0 +0): R0 invalid mem access 'map_value_or_null' (43 line(s) omitted)

오류 메시지("map_value_or_null")에 나오듯이 arg_from_userspace가 null인 경우가 발생할 수 있으므로 이를 체크하는 코드를 넣어야만 합니다.

uint32_t *arg_from_userspace = bpf_map_lookup_elem(&my_hash_map, &key);

uint32_t arg_value = 0;

if (arg_from_userspace) {
    arg_value = *arg_from_userspace;
}

bpf_printk("sys_enter_close called:  %d", arg_value);

사실 이런 문제들은 런타임에 발생할 수 있는 것들인데요, 단지 저 코드가 kernel에서 실행되기 때문에 실제로 발생한다면 시스템 crash가 발생하는 식의 문제로 이어지게 됩니다. 따라서, eBPF Verifier가 이런 문제를 미리 사전에 체크해 로딩 시점에 차단해 버리는 것입니다.




참고로, libbpf + C/C++로 Map이 어떻게 다뤄지는지 아래의 글을 보면 도움이 될 것입니다.

eBPF 맵
; https://wariua.github.io/man-pages-ko/bpf%282%29/#ebpf





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







[최초 등록일: ]
[최종 수정일: 2/26/2025]

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

비밀번호

댓글 작성자
 




... 31  32  33  34  35  [36]  37  38  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
13038정성태4/26/202213472오류 유형: 806. twine 실행 시 ConfigParser.ParsingError: File contains parsing errors: /root/.pypirc
13037정성태4/25/202214258.NET Framework: 1998. Azure Functions를 사용한 간단한 실습
13036정성태4/24/202215208.NET Framework: 1997. C# - nano 시간을 가져오는 방법 [2]
13035정성태4/22/202216468Windows: 204. Windows 10부터 바뀐 QueryPerformanceFrequency, QueryPerformanceCounter
13034정성태4/21/202214042.NET Framework: 1996. C# XingAPI - 주식 종목에 따른 PBR, PER, ROE, ROA 구하는 방법(t3320, t8430 예제)파일 다운로드1
13033정성태4/18/202215154.NET Framework: 1195. C# - Thread.Yield와 Thread.Sleep(0)의 차이점(?)
13032정성태4/17/202215357오류 유형: 805. Github의 50MB 파일 크기 제한 - warning: GH001: Large files detected. You may want to try Git Large File Storage
13031정성태4/15/202215037.NET Framework: 1194. C# - IdealProcessor와 ProcessorAffinity의 차이점
13030정성태4/15/202213576오류 유형: 804. 정규 표현식 오류 - Quantifier {x,y} following nothing.
13029정성태4/14/202214973Windows: 203. iisreset 후에도 이전에 설정한 전역 환경 변수가 w3wp.exe에 적용되는 문제
13028정성태4/13/202215097.NET Framework: 1193. (appsettings.json처럼) web.config의 Debug/Release에 따른 설정 적용
13027정성태4/12/202214970.NET Framework: 1192. C# - 환경 변수의 변화를 알리는 WM_SETTINGCHANGE Win32 메시지 사용법파일 다운로드1
13026정성태4/11/202216338.NET Framework: 1191. C 언어로 작성된 FFmpeg Examples의 C# 포팅 전체 소스 코드 [3]
13025정성태4/11/202215597.NET Framework: 1190. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 vaapi_encode.c, vaapi_transcode.c 예제 포팅
13024정성태4/7/202213855.NET Framework: 1189. C# - 런타임 환경에 따라 달라진 AppDomain.GetCurrentThreadId 메서드
13023정성태4/6/202214437.NET Framework: 1188. C# - ffmpeg(FFmpeg.AutoGen)를 이용한 transcoding.c 예제 포팅 [3]
13022정성태3/31/202214002Windows: 202. 윈도우 11 업그레이드 - "PC Health Check"를 통과했지만 여전히 업그레이드가 안 되는 경우 해결책
13021정성태3/31/202215821Windows: 201. Windows - INF 파일을 이용한 장치 제거 방법
13020정성태3/30/202214046.NET Framework: 1187. RDP 접속 시 WPF UserControl의 Unloaded 이벤트 발생파일 다운로드1
13019정성태3/30/202214507.NET Framework: 1186. Win32 Message를 Code로부터 메시지 이름 자체를 텍스트로 구하고 싶다면?파일 다운로드1
13018정성태3/29/202215253.NET Framework: 1185. C# - Unsafe.AsPointer가 반환한 포인터는 pinning 상태일까요? [5]
13017정성태3/28/202214235.NET Framework: 1184. C# - GC Heap에 위치한 참조 개체의 주소를 알아내는 방법 - 두 번째 이야기 [3]
13016정성태3/27/202215887.NET Framework: 1183. C# 11에 추가된 ref 필드의 (우회) 구현 방법파일 다운로드1
13015정성태3/26/202217592.NET Framework: 1182. C# 11 - ref struct에 ref 필드를 허용 [1]
13014정성태3/23/202215044VC++: 155. CComPtr/CComQIPtr과 Conformance mode 옵션의 충돌 [1]
13013정성태3/22/202212830개발 환경 구성: 641. WSL 우분투 인스턴스에 파이썬 2.7 개발 환경 구성하는 방법
... 31  32  33  34  35  [36]  37  38  39  40  41  42  43  44  45  ...