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

비밀번호

댓글 작성자
 




... 106  107  108  109  110  111  112  113  114  115  [116]  117  118  119  120  ...
NoWriterDateCnt.TitleFile(s)
11025정성태8/12/201622337개발 환경 구성: 294. .NET Core 프로젝트에서 "Copy to Output Directory" 처리 [1]
11024정성태8/12/201621648오류 유형: 350. "nProtect GameMon" 실행 중에는 Visual Studio 디버깅이 안됩니다! [1]
11023정성태8/10/201623128개발 환경 구성: 293. Azure 구독 후 PaaS 서비스 만들어 보기
11022정성태8/10/201623804개발 환경 구성: 292. Azure Cloud Service 배포시 사용자 정의 작업을 추가하는 방법
11021정성태8/10/201620871오류 유형: 349. System.Runtime.Remoting.RemotingException - Type '..., ..., Version=..., Culture=neutral, PublicKeyToken=null' is not registered for activation [2]
11020정성태8/10/201623596VC++: 98. 원본과 대상 버퍼가 같은 경우 memcpy, wmemcpy 주의점
11019정성태8/10/201640266기타: 60. 도서: 시작하세요! C# 6.0 프로그래밍: 기본 문법부터 실전 예제까지 (2쇄 정오표)
11018정성태8/9/201624737.NET Framework: 600. 단일 메서드 내에서의 할당으로 알아보는 자바와 닷넷의 GC 차이점 [1]
11017정성태8/9/201626788웹: 33. HTTP 쿠키에 한글 값을 설정하는 방법
11016정성태8/7/201623993개발 환경 구성: 291. Windows Server Containers 소개
11015정성태8/7/201622262오류 유형: 348. Windows Server 2016 TP5에서 Windows Containers의 docker run 실행 시 encountered an error during Start failed in Win32
11014정성태8/6/201623050오류 유형: 347. Hyper-V Virtual Machine Management service Account does not have permission to open attachment
11013정성태8/6/201633806개발 환경 구성: 290. Windows 10에서 경험해 보는 Windows Containers와 docker [4]
11012정성태8/6/201623849오류 유형: 346. Windows 10에서 Windows Containers의 docker run 실행 시 encountered an error during CreateContainer failed in Win32 발생
11011정성태8/6/201625462기타: 59. outlook.live.com 메일 서비스의 아웃룩 POP3 설정하는 방법
11010정성태8/6/201622876기타: 58. Outlook에 설정한 SMTP/POP3(예:천리안 메일) 계정 암호를 잊어버린 경우
11009정성태8/3/201628063개발 환경 구성: 289. 2016-08-02부터 시작된 윈도우 10 1주년 업데이트에서 Bash Shell 사용 [8]
11008정성태8/1/201621854오류 유형: 345. 2의 30승 이상의 원소를 갖는 경우 버그가 발생하는 이진 검색(Binary Search) 코드
11007정성태8/1/201623560오류 유형: 344. RDP ActiveX 컨트롤로 특정 PC에 연결할 수 없을 때, 오류 상황을 해결하기 위한 팁파일 다운로드1
11006정성태7/22/201626575개발 환경 구성: 288. SSL 인증서를 Azure Cloud Service에 적용하는 방법
11005정성태7/22/201625212개발 환경 구성: 287. Let's Encrypt 인증서 업데이트 주기: 90일
11004정성태7/22/201620070오류 유형: 343. Invalid service definition or service configuration. Please see the Error List for more details.
11003정성태7/20/201627343VS.NET IDE: 110. Visual Studio 2015에서 .NET Core 응용 프로그램 개발 [1]
11002정성태7/20/201620832개발 환경 구성: 286. Microsoft Azure 서비스의 구독은 반드시 IE로!
11001정성태7/19/201631868.NET Framework: 599. .NET Core/SDK 설치 및 기본 사용법 [6]
11000정성태7/16/201620600오류 유형: 342. Microsoft Visual Studio 2010 Tools for Office Runtime (x86 and x64) 설치 시 오류
... 106  107  108  109  110  111  112  113  114  115  [116]  117  118  119  120  ...