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

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  [127]  128  129  130  131  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
2880정성태3/7/201521677개발 환경 구성: 260. C# Code Coverage 도구 - Semantic Designs 소개
2879정성태3/3/201526680개발 환경 구성: 259. Visual Studio 없이 Visual C++ 컴파일하는 방법
2878정성태2/28/201527547.NET Framework: 503. == 연산자보다는 Equals 메서드의 호출이 더 권장됩니다. [3]파일 다운로드1
2877정성태2/28/201521748.NET Framework: 502. 연산자 재정의(operator overloading)와 메서드 재정의(method overriding)의 다른 점 - 가상 함수 호출 여부 [3]파일 다운로드1
2876정성태2/27/201524216VS.NET IDE: 98. IntegraStudio - Visual Studio에서 Java 프로그램 개발
2875정성태2/26/201522823디버깅 기술: 72. Visual Studio 2013에서의 sos.dll 사용 제한
2874정성태2/26/201519574디버깅 기술: 71. windbg + 닷넷 디버깅 (2) - null 체크 패턴
2873정성태2/25/201537048.NET Framework: 501. FtpWebRequest 타입을 이용해 FTP 파일 업로드 [4]파일 다운로드1
2872정성태2/25/201521231디버깅 기술: 70. windbg + 닷넷 디버깅 (1) - 배열 인덱스 사용 패턴
2871정성태2/24/201525203개발 환경 구성: 258. 윈도우 8.1에서 방화벽과 함께 FTP 서버 여는 (하지만, 권장하지 않는) 방법 [1]
2870정성태2/24/201526340개발 환경 구성: 257. 윈도우 8.1에서 방화벽과 함께 FTP 서버 여는 방법
2869정성태2/23/201520312.NET Framework: 500. struct로 정의한 값 형식(Value Type)의 경우 Equals 재정의를 권장합니다.파일 다운로드1
2868정성태2/23/201524822VS.NET IDE: 97. Visual C++ 프로젝트 디버깅 시에 Step-Into(F11) 동작이 원치 않는 함수로 진입하는 것을 막는 방법 [2]
2867정성태2/23/201518521오류 유형: 273. File History - Failed to initiate user data backup (error 80070005)
2866정성태2/23/201520363오류 유형: 272. WAT080 : Failed to locate the Windows Azure SDK. Please make sure the Windows Azure SDK v2.1 is installed.
1868정성태2/20/201517648오류 유형: 271. The type '...' cannot be used as type parameter 'TContext' in the generic type or method 'System.ServiceModel.DomainServices.EntityFramework.LinqToEntitiesDomainService<T>
1866정성태2/20/201518559오류 유형: 270. "aspnet_regiis -i" 실행 시 0x00000006 오류 해결 방법
1865정성태2/20/201519872.NET Framework: 499. 특정 닷넷 프레임워크 버전 이후부터 제공되는 타입을 사용해야 한다면?
1864정성태2/18/201524855.NET Framework: 498. C#으로 간단하게 만들어 본 ASCII Art 프로그램 [2]파일 다운로드1
1862정성태2/18/201528697.NET Framework: 497. .NET Garbage Collection에 대한 정리 [6]
1861정성태2/18/201524050.NET Framework: 496. 마우스 커서가 놓인 지점의 문자열 얻는 방법 [1]파일 다운로드1
1860정성태2/18/201523875.NET Framework: 495. CorElementType의 요소 값 설명파일 다운로드1
1859정성태2/17/201524280Windows: 106. 컴퓨터를 재부팅하면 절전(Power Saver) 전원 모드로 돌아가는 경우
1858정성태2/16/201534266Windows: 105. 자동으로 로그아웃/잠김 화면 상태로 전환된다면? [2]
1857정성태2/16/201522277.NET Framework: 494. 값(struct) 형식의 제네릭(Generic) 타입이 박싱되는 경우의 메타데이터 토큰 값파일 다운로드1
1856정성태2/15/201521254.NET Framework: 493. TypeRef 메타테이블에 등록되는 타입의 조건파일 다운로드1
... 121  122  123  124  125  126  [127]  128  129  130  131  132  133  134  135  ...