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 - libbpf CO-RE의 CONFIG_DEBUG_INFO_BTF 빌드 여부에 대한 의존성

libbpf가 지원하는 CO-RE(Compile Once, Run Everywhere)의 핵심은 BTF(Binary Type Format) 정보를 이용하는 것입니다. 바로 그런 특성으로 인해 커널의 CONFIG_DEBUG_INFO_BTF 빌드 또는 BTF 정보를 별도로 설치하는 작업이 필요한 것인데요.

물론 libbpf를 사용해도 BTF에 의존하지 않게 만들 수도 있습니다. 즉, CO-RE를 사용하지 않는 방향으로 코드를 작성하면 되는데, 어떤 차이점을 갖는지 한번 볼까요? ^^

예를 들어 아래의 eBPF 코드는,

//go:build ignore

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, uint32_t);
    __type(value, uint32_t);
    __uint(max_entries, 1);
} my_hash_map SEC(".maps");

volatile const __u32 const_u32 = 50;
volatile __u32 arg_u32 = 10;

SEC("socket") int const_example() {
    return const_u32;
}

SEC("tracepoint/syscalls/sys_enter_close")
int sys_enter_close(struct trace_event_raw_sys_enter *ctx) {

    bpf_printk("sys_enter_close called: %d", const_u32);
    return 0;
}

char __license[] SEC("license") = "GPL";

Map도 있고, 전역 변수도 갖춰져 있는 제법 기본적인 구색은 갖추고 있는데 CONFIG_DEBUG_INFO_BTF 빌드가 아닌 환경에서도 잘 실행이 됩니다. 왜냐하면, 위의 eBPF 코드에서는 어떠한 BTF 정보도 참조하지 않고 있기 때문입니다.

하지만 이 상태에서, trace_event_raw_sys_enter의 인자 중에 있는 args를 읽으려는 코드를 넣는다면?

SEC("tracepoint/syscalls/sys_enter_close")
int sys_enter_close(struct trace_event_raw_sys_enter *ctx) {
    __u64 fd = BPF_CORE_READ(ctx, args[0]);
    bpf_printk("sys_enter_close called: %d", fd);
    return 0;
}

또는, 이런 식으로 풀어서 작성해도,

__u64 fd = 0;
bpf_probe_read_kernel(&fd, sizeof(__u64), ctx->args);

이제는 CONFIG_DEBUG_INFO_BTF가 없는 환경이라면 (libbpf 방식의 bpf2go로 작성한) go 측에서 eBPF 모듈 로딩 시 이런 오류가 발생합니다.

program sys_enter_close: apply CO-RE relocations: load kernel spec: btf: not found

왜냐하면, vmlinux BTF에 포함된 trace_event_raw_sys_enter 정보에서 args를 참조하기 때문입니다.

$ grep -A 5 "struct trace_event_raw_sys_enter {" vmlinux.h
struct trace_event_raw_sys_enter {
        struct trace_entry ent;
        long int id;
        long unsigned int args[6];
        char __data[0];
};

자, 그럼 위의 상태에서 BTF를 참조하지 않는 방향으로 코드를 작성하면 어떨까요? 그렇다면 libbpf 방식 역시 CONFIG_DEBUG_INFO_BTF가 없는 환경에서도 잘 동작할 것입니다.

가령 위와 같은 trace_event_raw_sys_enter의 경우, TRACE_EVENT 매크로에 따른 구조로 정의돼 있다고 설명했었는데요,

type (2바이트) == common_type
flags (1바이트) == common_flags
preempt_count (1바이트) == common_preempt_count
pid (4바이트) == common_pid
id (8바이트) == __syscall_nr (4바이트) + 패딩(4바이트)
args[6] (48바이트) != fd(8바이트) + uservaddr 포인터(8바이트) + addrlen(8바이트), 총 24바이트
                   args[0] == fd
                   args[1] == uservaddr   
                   args[2] == addrlen
__data[0]

여기서 우리가 원하는 필드가 fd라면, 저 위치만 맞춰주는 구조체를 직접 정의해 사용하면 그만입니다.

struct trace_event_raw_sys_enter_close_stub {
    __u64 unused1; // type (2바이트) + flags (1바이트) + preempt_count (1바이트) + pid (4바이트)
    __u64 unused2; // id (8바이트)
    __u64 fd; // args[0]번 위치
}

그다음, 이걸 가지고 eBPF 코드를 작성하면,

SEC("tracepoint/syscalls/sys_enter_close")
int sys_enter_close(void* ctx) {

    struct trace_event_raw_sys_enter_close_stub close_arg = {};
    if (bpf_probe_read(&close_arg, sizeof(close_arg), ctx) < 0) {
        return 0;
    }

    __u64 fd = close_arg.fd;
    bpf_printk("sys_enter_close called: %d", fd);
    return 0;
}

저 코드는 CONFIG_DEBUG_INFO_BTF가 없는 환경에서도 잘 동작합니다. 차이점을 대충 아시겠죠? ^^




물론, trace_event_raw_sys_enter_close_stub과 같은 구조체를 대상 커널 구조체에 일치하는 형태로 만들면 CO-RE의 혜택이 없습니다. 다시 말해, 만약 향후 커널, 또는 다른 커널에서 "struct trace_event_raw_sys_enter"의 정의를 다음과 같이 바꾼다면,

struct trace_event_raw_sys_enter {
        struct trace_entry ent;
        long int id;
        long int extension;
        long unsigned int args[6];
        char __data[0];
};

BTF 없이 만들었던 코드에서는 args[0]번 필드를 접근하지 못하고, 그 위치를 대신하고 있는 extension 값을 읽게 돼 결국 프로그램은 의도치 않은 동작을 하게 됩니다.

반면, vmlinux BTF에 의존해 만들었다면 args 필드를 참조할 때 eBPF가 적재되면서 자동으로 extension 필드를 건너 뛴 args를 참조하게 되는 CO-RE의 혜택을 받게 됩니다.

그러니까, 서로 장단점이 있는 것입니다. BTF 의존성 없이 만들면 보다 많은 상황에서 동작은 하겠지만 자칫 커널의 구조체가 바뀌었을 때는 오동작할 여지가 있습니다. 반면 BTF 의존성을 갖게 만들면 대상 운영체제의 BTF 설정은 필요하지만 대신 커널 구조체가 바뀌어도 자동으로 대응할 수 있습니다.

이런 것을 감안했을 때 현실적인 기준으로 보면, trace_event_raw_sys_enter와 같은 커널 구조체는 거의 바뀌지 않는다고 기대할 수 있으므로 BTF 의존성 없이 만들어도 나쁘지 않은 선택일 수 있습니다. 하지만, 이게 거의 불가능한 경우도 있는데요, 단적인 예로 task_struct를 건드리는 코드가 대표적입니다.

struct task_struct* current_task = (struct task_struct*)bpf_get_current_task();

struct task_struct* parent_task;
bpf_probe_read(&parent_task, sizeof(parent_task), &task->real_parent);

위의 경우라면, BTF 의존성을 갖는 경우 vmlinux.h에 정의된 task_struct 구조체를 참조하면서 빌드도 자연스럽고, 이후 CO-RE의 혜택으로 실행도 (필드가 없어지지만 않는다면) 보장이 됩니다.

하지만, BTF 의존성을 없애려고 task_struct를 real_parent 필드까지만 정의한 구조체로 정의하려고 해도,

linux/include/linux/sched.h
; https://github.com/torvalds/linux/blob/master/include/linux/sched.h#L778

위의 task_struct 정의에서 보듯이 수많은 #ifdef CONFIG_... 정의에 따라 바뀔 수 있으므로 다양한 환경에 대응할 수 없습니다. 아마도, 특정 시스템을 타깃팅하지 않는 경우를 제외하고는 저것을 BTF 의존성 없이 만들 장점이 전혀 없을 텐데요, 즉, BTF 의존성을 없애려고 했다가 오히려 더 많은 의존성 문제를 낳게 될 수 있는 것입니다.




저렇게 보면, libbcc의 방식도 나쁘지 않은 선택일 수 있습니다. 그런 경우라면 task_struct의 필드 접근 코드를 대상 컴퓨터에서 eBPF 코드를 컴파일할 때 자동으로 맞춰서 바꿔주는 방식이기 때문에 오히려 BTF 의존성 없이 만들어야 하면서 범용성을 갖고 싶은 경우 고려할 수 있는 선택지 중의 하나가 됩니다.

실제로 libbcc 예제 코드에서는 task_struct에 대한 접근을 예사로 하는 코드를 종종 볼 수 있는 이유가 있던 것입니다.

정리해 보면, libbcc 또는 libbpf CO-RE를 선택하는 기준은 분명합니다. 대상 시스템에 BTF 의존성을 갖거나, clang/libbcc 의존성을 갖거나!




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







[최초 등록일: ]
[최종 수정일: 11/19/2024]

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)
13667정성태7/7/20246593닷넷: 2273. C# - 리눅스 환경에서의 Hyper-V Socket 연동 (AF_VSOCK)파일 다운로드1
13666정성태7/7/20247673Linux: 74. C++ - Vsock 예제 (Hyper-V Socket 연동)파일 다운로드1
13665정성태7/6/20247846Linux: 73. Linux 측의 socat을 이용한 Hyper-V 호스트와의 vsock 테스트파일 다운로드1
13663정성태7/5/20247456닷넷: 2272. C# - Hyper-V Socket 통신(AF_HYPERV, AF_VSOCK)의 VMID Wildcards 유형파일 다운로드1
13662정성태7/4/20247463닷넷: 2271. C# - WSL 2 VM의 VM ID를 알아내는 방법 - Host Compute System API파일 다운로드1
13661정성태7/3/20247383Linux: 72. g++ - 다른 버전의 GLIBC로 소스코드 빌드
13660정성태7/3/20247487오류 유형: 912. Visual C++ - Linux 프로젝트 빌드 오류
13659정성태7/1/20247824개발 환경 구성: 715. Windows - WSL 2 환경의 Docker Desktop 네트워크
13658정성태6/28/20248200개발 환경 구성: 714. WSL 2 인스턴스와 호스트 측의 Hyper-V에 운영 중인 VM과 네트워크 연결을 하는 방법 - 두 번째 이야기
13657정성태6/27/20247879닷넷: 2270. C# - Hyper-V Socket 통신(AF_HYPERV, AF_VSOCK)을 위한 EndPoint 사용자 정의
13656정성태6/27/20248035Windows: 264. WSL 2 VM의 swap 파일 위치
13655정성태6/24/20247830닷넷: 2269. C# - Win32 Resource 포맷 해석파일 다운로드1
13654정성태6/24/20247772오류 유형: 911. shutdown - The entered computer name is not valid or remote shutdown is not supported on the target computer.
13653정성태6/22/20247910닷넷: 2268. C# 코드에서 MAKEINTREOURCE 매크로 처리
13652정성태6/21/20249218닷넷: 2267. C# - Linux 환경에서 (Reflection 없이) DLL AssemblyFileVersion 구하는 방법파일 다운로드2
13651정성태6/19/20248458닷넷: 2266. C# - (Reflection 없이) DLL AssemblyFileVersion 구하는 방법파일 다운로드1
13650정성태6/18/20248382개발 환경 구성: 713. "WSL --debug-shell"로 살펴보는 WSL 2 VM의 리눅스 환경
13649정성태6/18/20247932오류 유형: 910. windbg - !py 확장 명령어 실행 시 "failed to find python interpreter" (2)
13648정성태6/17/20248246오류 유형: 909. C# - DynamicMethod 사용 시 System.TypeAccessException
13647정성태6/16/20249305개발 환경 구성: 712. Windows - WSL 2의 네트워크 통신 방법 - 세 번째 이야기 (같은 IP를 공유하는 WSL 2 인스턴스) [1]
13646정성태6/14/20247722오류 유형: 908. Process Explorer - "Error configuring dump resources: The system cannot find the file specified."
13645정성태6/13/20248183개발 환경 구성: 711. Visual Studio로 개발 시 기본 등록하는 dev tag 이미지로 Docker Desktop k8s에서 실행하는 방법
13644정성태6/12/20248839닷넷: 2265. C# - System.Text.Json의 기본적인 (한글 등에서의) escape 처리 [1]
13643정성태6/12/20248279오류 유형: 907. MySqlConnector 사용 시 System.IO.FileLoadException 오류
13642정성태6/11/20248181스크립트: 65. 파이썬 - asgi 버전(2, 3)에 따라 달라지는 uvicorn 호스팅
13641정성태6/11/20248644Linux: 71. Ubuntu 20.04를 22.04로 업데이트
1  2  3  4  5  6  7  8  9  10  [11]  12  13  14  15  ...