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

비밀번호

댓글 작성자
 




... 106  107  [108]  109  110  111  112  113  114  115  116  117  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11224정성태6/13/201718155.NET Framework: 661. Json.NET의 DeserializeObject 수행 시 속성 이름을 동적으로 바꾸는 방법파일 다운로드1
11223정성태6/12/201716831개발 환경 구성: 318. WCF Service Application과 WCFTestClient.exe
11222정성태6/10/201720553오류 유형: 399. WCF - A property with the name 'UriTemplateMatchResults' already exists.파일 다운로드1
11221정성태6/10/201717522오류 유형: 398. Fakes - Assembly 'Jennifer5.Fakes' with identity '[...].Fakes, [...]' uses '[...]' which has a higher version than referenced assembly '[...]' with identity '[...]'
11220정성태6/10/201722911.NET Framework: 660. Shallow Copy와 Deep Copy [1]파일 다운로드2
11219정성태6/7/201718224.NET Framework: 659. 닷넷 - TypeForwardedFrom / TypeForwardedTo 특성의 사용법
11218정성태6/1/201721024개발 환경 구성: 317. Hyper-V 내의 VM에서 다시 Hyper-V를 설치: Nested Virtualization
11217정성태6/1/201716916오류 유형: 397. initerrlog: Could not open error log file 'C:\...\MSSQL12.MSSQLSERVER\MSSQL\Log\ERRORLOG'
11216정성태6/1/201719029오류 유형: 396. Activation context generation failed
11215정성태6/1/201719967오류 유형: 395. 관리 콘솔을 실행하면 "This app has been blocked for your protection" 오류 발생 [1]
11214정성태6/1/201717704오류 유형: 394. MSDTC 서비스 시작 시 -1073737712(0xC0001010) 오류와 함께 종료되는 문제 [1]
11213정성태5/26/201722485오류 유형: 393. TFS - The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel.
11212정성태5/26/201721822오류 유형: 392. Windows Server 2016에 KB4019472 업데이트가 실패하는 경우
11211정성태5/26/201720861오류 유형: 391. BeginInvoke에 전달한 람다 함수에 CS1660 에러가 발생하는 경우
11210정성태5/25/201721308기타: 65. ActiveX 없는 전자 메일에 사용된 "개인정보 보호를 위해 암호화된 보안메일"의 암호화 방법
11209정성태5/25/201768229Windows: 143. Windows 10의 Recovery 파티션을 삭제 및 새로 생성하는 방법 [16]
11208정성태5/25/201727955오류 유형: 390. diskpart의 set id 명령어에서 "The specified type is not in the correct format." 오류 발생
11207정성태5/24/201728274Windows: 142. Windows 10의 복구 콘솔로 부팅하는 방법
11206정성태5/24/201721558오류 유형: 389. DISM.exe - The specified image in the specified wim is already mounted for read/write access.
11205정성태5/24/201721271.NET Framework: 658. C#의 tail call 구현은? [1]
11204정성태5/22/201730808개발 환경 구성: 316. 간단하게 살펴보는 Docker for Windows [7]
11203정성태5/19/201718740오류 유형: 388. docker - Host does not exist: "default"
11202정성태5/19/201719809오류 유형: 387. WPF - There is no registered CultureInfo with the IetfLanguageTag 'ug'.
11201정성태5/16/201722558오류 유형: 386. WPF - .NET 3.5 이하에서 TextBox에 한글 입력 시 TextChanged 이벤트의 비정상 종료 문제 [1]파일 다운로드1
11200정성태5/16/201719333오류 유형: 385. WPF - 폰트가 없어 System.IO.FileNotFoundException 예외가 발생하는 경우
11199정성태5/16/201721160.NET Framework: 657. CultureInfo.GetCultures가 반환하는 값
... 106  107  [108]  109  110  111  112  113  114  115  116  117  118  119  120  ...