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

비밀번호

댓글 작성자
 




1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13818정성태11/15/20245233Windows: 272. Windows 11 24H2 - sudo 추가
13817정성태11/14/20244908Linux: 106. eBPF / bpf2go - (BPF_MAP_TYPE_HASH) Map을 이용한 전역 변수 구현
13816정성태11/14/20245368닷넷: 2312. C#, C++ - Windows / Linux 환경의 Thread Name 설정파일 다운로드1
13815정성태11/13/20244793Linux: 105. eBPF - bpf2go에서 전역 변수 설정 방법
13814정성태11/13/20245262닷넷: 2311. C# - Windows / Linux 환경에서 Native Thread ID 가져오기파일 다운로드1
13813정성태11/12/20244998닷넷: 2310. .NET의 Rune 타입과 emoji 표현파일 다운로드1
13812정성태11/11/20245227오류 유형: 933. Active Directory - The forest functional level is not supported.
13811정성태11/11/20244834Linux: 104. Linux - COLUMNS 환경변수가 언제나 80으로 설정되는 환경
13810정성태11/10/20245357Linux: 103. eBPF (bpf2go) - Tracepoint를 이용한 트레이스 (BPF_PROG_TYPE_TRACEPOINT)
13809정성태11/10/20245233Windows: 271. 윈도우 서버 2025 마이그레이션
13808정성태11/9/20245239오류 유형: 932. Linux - 커널 업그레이드 후 "error: bad shim signature" 오류 발생
13807정성태11/9/20244966Linux: 102. Linux - 커널 이미지 파일 서명 (Ubuntu 환경)
13806정성태11/8/20244884Windows: 270. 어댑터 상세 정보(Network Connection Details) 창의 내용이 비어 있는 경우
13805정성태11/8/20244721오류 유형: 931. Active Directory의 adprep 또는 복제가 안 되는 경우
13804정성태11/7/20245351Linux: 101. eBPF 함수의 인자를 다루는 방법
13803정성태11/7/20245303닷넷: 2309. C# - .NET Core에서 바뀐 DateTime.Ticks의 정밀도
13802정성태11/6/20245676Windows: 269. GetSystemTimeAsFileTime과 GetSystemTimePreciseAsFileTime의 차이점파일 다운로드1
13801정성태11/5/20245463Linux: 100. eBPF의 2가지 방식 - libbcc와 libbpf(CO-RE)
13800정성태11/3/20246305닷넷: 2308. C# - ICU 라이브러리를 활용한 문자열의 대소문자 변환 [2]파일 다운로드1
13799정성태11/2/20244878개발 환경 구성: 732. 모바일 웹 브라우저에서 유니코드 문자가 표시되지 않는 경우
13798정성태11/2/20245478개발 환경 구성: 731. 유니코드 - 출력 예시 및 폰트 찾기
13797정성태11/1/20245476C/C++: 185. C++ - 문자열의 대소문자를 변환하는 transform + std::tolower/toupper 방식의 문제점파일 다운로드1
13796정성태10/31/20245361C/C++: 184. C++ - ICU dll을 이용하는 예제 코드 (Windows)파일 다운로드1
13795정성태10/31/20245146Windows: 268. Windows - 리눅스 환경처럼 공백으로 끝나는 프롬프트 만들기
13794정성태10/30/20245255닷넷: 2307. C# - 윈도우에서 한글(및 유니코드)을 포함한 콘솔 프로그램을 컴파일 및 실행하는 방법
13793정성태10/28/20245118C/C++: 183. C++ - 윈도우에서 한글(및 유니코드)을 포함한 콘솔 프로그램을 컴파일 및 실행하는 방법
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...