Microsoft MVP성태의 닷넷 이야기
Linux: 116. eBPF / bpf2go - BTF Style Maps 정의 구문과 데이터 정렬 문제 [링크 복사], [링크+제목 복사],
조회: 2521
글쓴 사람
정성태 (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 - BTF Style Maps 정의 구문과 데이터 정렬 문제

libbpf에서는 BPF map 정의가 바뀌었다고 했는데요,

eBPF의 2가지 방식 - libbcc와 libbpf(CO-RE) 4. BPF maps
; https://www.sysnet.pe.kr/2/0/13801#4

아래의 공식 문서에서도 "legacy"와 "BTF style"로 나눠서 설명하고 있습니다.

eBPF Docs - Maps
; https://docs.ebpf.io/linux/concepts/maps/

legacy는 슬슬 지원하지 않는 듯하니, 이번 글에서는 BTF Style에 대해서만 다뤄볼 텐데요, 기본적인 정의는 다음과 같이 할 수 있습니다.

struct my_value { int x, y, z; };

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, int);
    __type(value, struct my_value);
    __uint(max_entries, 16);
} icmpcnt __attribute__((section(".maps"), used)); // == SEC(".maps");

낯설어 보이는 __uint, __type, __array, __ulong 등으로 인해 C 언어의 struct 구문에서 약간 빗나간 것처럼 보일 수도 있지만 문서에서 설명하듯이 이들은 매크로 정의에 불과합니다.

#define __uint(name, val) int (*name)[val]
#define __type(name, val) typeof(val) *name
#define __array(name, val) typeof(val) *name[]
#define __ulong(name, val) enum { ___bpf_concat(__unique_value, __COUNTER__) = val } name

그래서 결국, 위와 같은 코드를 컴파일하면 다음과 같이 구조체 정의로 바뀝니다.

/* BPF_MAP_TYPE_ARRAY 정의
$ cat /usr/include/linux/bpf.h | grep -B 3 BPF_MAP_TYPE_ARRAY,
enum bpf_map_type {
        BPF_MAP_TYPE_UNSPEC,
        BPF_MAP_TYPE_HASH,
        BPF_MAP_TYPE_ARRAY,
*/

/*
// gcc - 매크로를 확장한 코드를 출력
$ gcc -E test.c
*/

/*
// Visual C++ - 매크로를 확장한 코드 파일(test.i)을 산출
C:\temp> cl test.c /std:clatest /P
*/

struct my_value { int x, y, z; };

struct {
    int (*type)[BPF_MAP_TYPE_ARRAY];
    int *key;
    struct my_value *value;
    int (*max_entries)[16];
} icmpcnt __attribute__((section(".maps"), used));

C 코딩을 하면서 구조체를 "struct { ... } name;" 형태(anonymous struct)로 쓰는 경우는 잘 없는데요, 왜냐하면 이런 식으로 쓰면 type 이름이 없이 곧바로 변수를 정의하는 것과 같기 때문입니다.

그래서, 코드 내에서 저렇게 선언된 전역 변수를 사용할 수는 있지만,

bpf_map_update_elem(&icmpcnt, ..., ..., BPF_ANY);

임의로 저 타입의 변수를 (typeof가 추가된 C23 표준 전에는) 만들 수는 없습니다.




대충 정의가 눈에 들어왔으면, 이제 bpf2go와 연동해 볼까요?

우선, 간단하게 key/value가 모두 primitive type인 경우를 살펴보겠습니다.

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, __u32);
    __type(value, __u32);
    __uint(max_entries, 10000);
} my_u32_key_value_map SEC(".maps");

// ... eBPF 함수 ...
{
    __u32 key = 1000;
    __u32 value = 2000;

    bpf_map_update_elem(&my_u32_key_value_map, &key, &value, BPF_ANY);
}

위와 같이 정의한 map의 모든 데이터를 go 언어 측에서 조회하려면 map에 정의한 key, value 타입에 맞춰 주기만 하면 됩니다.

iter := bpfObj.MyU64KeyValueMap.Iterate()

var (
    key   uint32
    value uint32
)

for iter.Next(&key, &value) {
    log.Printf("key: %v, value: %v\n", key, value)
}

그렇다면, key의 타입을 다중 필드를 가진 타입으로 확장하면 어떨까요?

struct key128 {
    __u64 key1;
    __u64 key2;
};

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, struct key128);
    __type(value, __u32);
    __uint(max_entries, 10000);
} my_hash_key128_map SEC(".maps");

바뀐 key의 구조대로 go 언어 측에서도 타입을 맞춰주고 실행할 수 있습니다.

type Key128 struct {
    key1 uint64
    key2 uint64
}

var (
    key   Key128
    value uint32
)

idx := 0
for iter.Next(&key, &value) {
    log.Printf("[%v], key: %v, value: %v\n", idx, key, value)
    idx++
}

어렵지 않죠? ^^




다중 필드를 가진 구조체의 경우 한 가지 유의해야 할 점이 있는데요, eBPF는 C 언어이기 때문에 구조체의 크기도 CPU word 단위로 정렬되는 것이 기본 동작입니다.

즉, key 구조체를 다음과 같이 정의했다면,

struct key96 {
    __u64 key1; // 8바이트
    __u32 key2; // 4바이트
};

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, struct key96);
    __type(value, __u32);
    __uint(max_entries, 10000);
} my_hash_key96_map SEC(".maps");

표면상으로는 12바이트를 점유하지만, 실제로는 16바이트로 다뤄집니다.

struct key96 e1 = { 50, 51 };
__u32 size1 = sizeof(e1);
bpf_printk("sizeof(key96) %d", size1); // 출력 결과: sizeof(key96) 16

이런 상태에서 Go 언어 측에서 다음과 같은 key 정의로 접근하면,

type Key96 struct {
    Key1 uint64
    Key2 uint32
}

var (
    key   Key96
    value uint32
)

iter := bpfObj.MyHashKey96Map.Iterate()

idx := 0
for iter.Next(&key, &value) {
    log.Printf("[%v], key: %v, value: %v\n", idx, key, value)
    idx++
}

err := iter.Err() 
if err != nil {
	log.Printf("failed to iterate: %v\n", err) // 출력 결과: unmarshaling *main.Key96 doesn't consume all data
}

저렇게 "unmarshaling *main.Key96 doesn't consume all data"라는 오류 메시지가 출력됩니다. 따라서 이런 경우에는 오히려 key 정의를 모두 64비트로 맞춰줘야 합니다.

type Key96 struct {
    Key1 uint64
    Key2 uint64
}

또는, eBPF 측의 구조체 정의를 아예 packed로 정의해 정렬 규칙을 무시하도록 만드는 것도 방법입니다.

struct packedKey96 {
    __u64 key1;
    __u32 key2;
} __attribute__((packed)); // Visual C++의 경우 pragma 지시자

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, struct packedKey96);
    __type(value, __u32);
    __uint(max_entries, 10000);
} my_hash_key96_map SEC(".maps");

이렇게 바꿔주면 Go 언어 측에서도 동일하게 (uint64, uint32)의 키로 접근할 수 있습니다.




참고로, map 정의 시 key, value가 아닌 key_size, value_size를 사용하는 것도 가능합니다. 이렇게 하는 경우에는 __type 매크로가 아닌 __uint 매크로를 사용해야 한다는 것에 유의해야 합니다.

즉, 다음의 2가지 map은 완전히 동일한 구조의 맵을 정의합니다.

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, struct key128);
    __type(value, __u32);
    __uint(max_entries, 10000);
} my_hash_key128_map SEC(".maps");

// __type ==> __uint
// 필드명은 key, value를 각각 key_size, value_size로 변경
// 두 번째 인자에는 타입명 대신 sizeof(...), 또는 직접 바이트 크기를 명시

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(key_size, sizeof(struct key128)); // 또는, __uint(key_size, 16);
    __uint(value_size, sizeof(__u32)); // 또는, __uint(value_size, 4);
    __uint(max_entries, 10000);
} my_hash_key128_map SEC(".maps");

만약 저걸 혼동해서 __type을 그대로 남겨두고 sizeof(...)를 사용한다거나 하면,

struct {
	__uint(type, BPF_MAP_TYPE_HASH);
	__type(key, sizeof(struct key128));
	__type(value, __u32);
	__uint(max_entries, 10000);
} my_hash_key128_map SEC(".maps");

어떻게 섞었느냐에 따라 eBPF 프로그램을 로드하는 시점에 이런 식의 오류가 발생하거나,

invalid argument: invalid indirect access to stack R2 off=-24 size=64 (60 line(s) omitted)

로딩은 되지만 iter.Next 시에 "unexpected EOF" 오류가 발생할 수 있습니다.




만약 iter.Next에서 이런 오류가 발생한다면?

iter.Next(&key, &value) 

// panic: reflect: reflect.Value.SetUint using value obtained using unexported field

goroutine 18 [running]:
reflect.flag.mustBeAssignableSlow(0x6bce3645f26bc00f?)
    /snap/go/10853/src/reflect/value.go:254 +0xb4
reflect.flag.mustBeAssignable(...)
    /snap/go/10853/src/reflect/value.go:244
reflect.Value.SetUint({0x57d0a0?, 0xc0002800a0?, 0x733040?}, 0x32)
    /snap/go/10853/src/reflect/value.go:2181 +0x45
encoding/binary.(*decoder).value(0xc00015fcf8, {0x57d0a0?, 0xc0002800a0?, 0x4?})
    /snap/go/10853/src/encoding/binary/binary.go:894 +0x32c
encoding/binary.(*decoder).value(0xc00015fcf8, {0x595b60?, 0xc0002800a0?, 0xc?})
    /snap/go/10853/src/encoding/binary/binary.go:863 +0x7e5
encoding/binary.Read({0x5fcbc0, 0xc00028e060}, {0x5fe310, 0x732140}, {0x575560, 0xc0002800a0})
    /snap/go/10853/src/encoding/binary/binary.go:289 +0x385
github.com/cilium/ebpf/internal/sysenc.Unmarshal({0x575560, 0xc0002800a0}, {0xc0002800c0, 0x10, 0x10})
    /home/testusr/go/pkg/mod/github.com/cilium/ebpf@v0.16.0/internal/sysenc/marshal.go:108 +0x2e5
github.com/cilium/ebpf.(*MapIterator).Next(0xc00015fed0, {0x575560, 0xc0002800a0}, {0x577920, 0xc0002800b0})
    /home/testusr/go/pkg/mod/github.com/cilium/ebpf@v0.16.0/map.go:1621 +0x2dd
main.IterMyHashMap({{0xc0001223c0, 0xc000122400}, {0xc000144460, 0xc0001444b0, 0xc000144500, 0xc000144410, 0xc000144550, 0xc0001445a0, 0xc0001445f0, 0xc000144640, ...}})
    /home/testusr/ebpf_sample/main.go:149 +0x311
created by main.main in goroutine 1
    /home/testusr/ebpf_sample/main.go:132 +0x698

혹시 key나 value가 구조체가 아닌지 확인하고, 만약 구조체라면 필드를 소문자로 한 것은 아닌지 확인해 보세요. ^^;

type Key128 struct {
    key1 uint64
    key2 uint64
}

iter.Next는 내부적으로 reflect 패키지로 역직렬화를 하는데, 이 과정에서 구조체의 변수가 소문자로 시작한다면 직렬화 과정에서 제외(unexported field)시키므로 iter.Next에서 오류가 발생합니다.




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







[최초 등록일: ]
[최종 수정일: 2/28/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/201229443개발 환경 구성: 151. Azure 웹 사이트에 사용자 도메인 네임 연결하는 방법
1301정성태6/20/201225765오류 유형: 156. KB2667402 윈도우 업데이트 실패 및 마이크로소프트 Answers 웹 사이트 대응
1300정성태6/20/201231775.NET Framework: 329. C# - Rabin-Miller 소수 생성방법을 이용하여 RSACryptoServiceProvider의 개인키를 직접 채워보자 [1]파일 다운로드2
1299정성태6/18/201232892제니퍼 .NET: 21. 제니퍼 닷넷 - Ninject DI 프레임워크의 성능 분석 [2]파일 다운로드2
1298정성태6/14/201234406VS.NET IDE: 72. Visual Studio에서 pfx 파일로 서명한 경우, 암호는 어디에 저장될까? [2]
1297정성태6/12/201231054VC++: 63. 다른 프로세스에 환경 변수 설정하는 방법파일 다운로드1
1296정성태6/5/201227695.NET Framework: 328. 해당 DLL이 Managed인지 / Unmanaged인지 확인하는 방법 - 두 번째 이야기 [4]파일 다운로드1
1295정성태6/5/201225083.NET Framework: 327. RSAParameters와 System.Numerics.BigInteger 이야기파일 다운로드1
1294정성태5/27/201248531.NET Framework: 326. 유니코드와 한글 - 유니코드와 닷넷을 이용한 한글 처리 [7]파일 다운로드2
1293정성태5/24/201229774.NET Framework: 325. System.Drawing.Bitmap 데이터를 Parallel.For로 처리하는 방법 [2]파일 다운로드1
1292정성태5/24/201223754.NET Framework: 324. First-chance exception에 대해 조건에 따라 디버거가 멈추게 할 수는 없을까? [1]파일 다운로드1
1291정성태5/23/201230279VC++: 62. 배열 초기화를 위한 기계어 코드 확인 [2]
1290정성태5/18/201235081.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/201238228.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/201225699오류 유형: 155. Windows Phone 연결 상태에서 DRIVER POWER STATE FAILURE 블루 스크린 뜨는 현상
1283정성태5/12/201233310.NET Framework: 317. C# 관점에서의 Observer 패턴 구현 [1]파일 다운로드1
1282정성태5/12/201226107Phone: 6. Windows Phone 7 Silverlight에서 Google Map 사용하는 방법 [3]파일 다운로드1
1281정성태5/9/201233194.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]  ...