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

비밀번호

댓글 작성자
 




... 136  137  138  139  140  141  142  143  144  145  146  147  148  149  [150]  ...
NoWriterDateCnt.TitleFile(s)
1303정성태6/26/201227403개발 환경 구성: 152. sysnet DB를 SQL Azure 데이터베이스로 마이그레이션
1302정성태6/25/201229445개발 환경 구성: 151. Azure 웹 사이트에 사용자 도메인 네임 연결하는 방법
1301정성태6/20/201225765오류 유형: 156. KB2667402 윈도우 업데이트 실패 및 마이크로소프트 Answers 웹 사이트 대응
1300정성태6/20/201231780.NET Framework: 329. C# - Rabin-Miller 소수 생성방법을 이용하여 RSACryptoServiceProvider의 개인키를 직접 채워보자 [1]파일 다운로드2
1299정성태6/18/201232893제니퍼 .NET: 21. 제니퍼 닷넷 - Ninject DI 프레임워크의 성능 분석 [2]파일 다운로드2
1298정성태6/14/201234408VS.NET IDE: 72. Visual Studio에서 pfx 파일로 서명한 경우, 암호는 어디에 저장될까? [2]
1297정성태6/12/201231055VC++: 63. 다른 프로세스에 환경 변수 설정하는 방법파일 다운로드1
1296정성태6/5/201227695.NET Framework: 328. 해당 DLL이 Managed인지 / Unmanaged인지 확인하는 방법 - 두 번째 이야기 [4]파일 다운로드1
1295정성태6/5/201225084.NET Framework: 327. RSAParameters와 System.Numerics.BigInteger 이야기파일 다운로드1
1294정성태5/27/201248541.NET Framework: 326. 유니코드와 한글 - 유니코드와 닷넷을 이용한 한글 처리 [7]파일 다운로드2
1293정성태5/24/201229776.NET Framework: 325. System.Drawing.Bitmap 데이터를 Parallel.For로 처리하는 방법 [2]파일 다운로드1
1292정성태5/24/201223755.NET Framework: 324. First-chance exception에 대해 조건에 따라 디버거가 멈추게 할 수는 없을까? [1]파일 다운로드1
1291정성태5/23/201230279VC++: 62. 배열 초기화를 위한 기계어 코드 확인 [2]
1290정성태5/18/201235082.NET Framework: 323. 관리자 권한이 필요한 작업을 COM+에 대행 [7]파일 다운로드1
1289정성태5/17/201239239.NET Framework: 322. regsvcs.exe로 어셈블리 등록 시 시스템 변경 사항 [5]파일 다운로드2
1288정성태5/17/201226465.NET Framework: 321. regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (3) - Type Library파일 다운로드1
1287정성태5/17/201229299.NET Framework: 320. regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (2) - .NET 4.0 + .NET 2.0 [2]
1286정성태5/17/201238229.NET Framework: 319. regasm.exe로 어셈블리 등록 시 시스템 변경 사항 (1) - .NET 2.0 + x86/x64/AnyCPU [5]
1285정성태5/16/201233265.NET Framework: 318. gacutil.exe로 어셈블리 등록 시 시스템 변경 사항파일 다운로드1
1284정성태5/15/201225700오류 유형: 155. Windows Phone 연결 상태에서 DRIVER POWER STATE FAILURE 블루 스크린 뜨는 현상
1283정성태5/12/201233311.NET Framework: 317. C# 관점에서의 Observer 패턴 구현 [1]파일 다운로드1
1282정성태5/12/201226110Phone: 6. Windows Phone 7 Silverlight에서 Google Map 사용하는 방법 [3]파일 다운로드1
1281정성태5/9/201233195.NET Framework: 316. WPF/Silverlight의 그래픽 단위와 Anti-aliasing 처리를 이해하자 [1]파일 다운로드1
1280정성태5/9/201226158오류 유형: 154. Could not load type 'System.ServiceModel.Activation.HttpModule' from assembly 'System.ServiceModel, ...'.
1279정성태5/9/201224918.NET Framework: 315. 해당 DLL이 Managed인지 / Unmanaged인지 확인하는 방법 [1]파일 다운로드1
1278정성태5/8/201226149오류 유형: 153. Visual Studio 디버깅 - Unable to break execution. This process is not currently executing the type of code that you selected to debug.
... 136  137  138  139  140  141  142  143  144  145  146  147  148  149  [150]  ...