Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 

(시리즈 글이 11개 있습니다.)
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




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)
13861정성태1/11/2025455Windows: 276. 명령행에서 원격 서비스를 동기/비동기로 시작/중지
13860정성태1/10/2025508디버깅 기술: 216. WinDbg - 2가지 유형의 식 평가 방법(MASM, C++)
13859정성태1/9/2025596디버깅 기술: 215. Windbg - syscall 이후 실행되는 KiSystemCall64 함수 및 SSDT 디버깅
13858정성태1/8/2025656개발 환경 구성: 738. PowerShell - 원격 호출 시 "powershell.exe"가 아닌 "pwsh.exe" 환경으로 명령어를 실행하는 방법
13857정성태1/7/2025751C/C++: 187. Golang - 콘솔 응용 프로그램을 Linux 데몬 서비스를 지원하도록 변경파일 다운로드1
13856정성태1/6/2025815디버깅 기술: 214. Windbg - syscall 단계까지의 Win32 API 호출 (예: Sleep)
13855정성태12/28/20241435오류 유형: 941. Golang - os.StartProcess() 사용 시 오류 정리
13854정성태12/27/20241414C/C++: 186. Golang - 콘솔 응용 프로그램을 NT 서비스를 지원하도록 변경파일 다운로드1
13853정성태12/26/20241582디버깅 기술: 213. Windbg - swapgs 명령어와 (Ring 0 커널 모드의) FS, GS Segment 레지스터
13852정성태12/25/20241596디버깅 기술: 212. Windbg - (Ring 3 사용자 모드의) FS, GS Segment 레지스터파일 다운로드1
13851정성태12/23/20241514디버깅 기술: 211. Windbg - 커널 모드 디버깅 상태에서 사용자 프로그램을 디버깅하는 방법
13850정성태12/23/20241524오류 유형: 940. "Application Information" 서비스를 중지한 경우, "This file does not have an app associated with it for performing this action."
13849정성태12/20/20241558디버깅 기술: 210. Windbg - 논리(가상) 주소를 Segmentation을 거쳐 선형 주소로 변경
13848정성태12/18/20241784디버깅 기술: 209. Windbg로 알아보는 Prototype PTE파일 다운로드2
13847정성태12/18/20241676오류 유형: 939. golang - 빌드 시 "unknown directive: toolchain" 오류 빌드 시 이런 오류가 발생한다면?
13846정성태12/17/20241768디버깅 기술: 208. Windbg로 알아보는 Trans/Soft PTE와 2가지 Page Fault 유형파일 다운로드1
13845정성태12/16/20241958디버깅 기술: 207. Windbg로 알아보는 PTE (_MMPTE)
13844정성태12/14/20241833디버깅 기술: 206. Windbg로 알아보는 PFN (_MMPFN)파일 다운로드1
13843정성태12/13/20241869오류 유형: 938. Docker container 내에서 빌드 시 error MSB3021: Unable to copy file "..." to "...". Access to the path '...' is denied.
13842정성태12/12/20242264디버깅 기술: 205. Windbg - KPCR, KPRCB
13841정성태12/11/20242198오류 유형: 937. error MSB4044: The "ValidateValidArchitecture" task was not given a value for the required parameter "RemoteTarget"
13840정성태12/11/20241998오류 유형: 936. msbuild - Your project file doesn't list 'win' as a "RuntimeIdentifier"
13839정성태12/11/20241972오류 유형: 936. msbuild - error CS1617: Invalid option '12.0' for /langversion. Use '/langversion:?' to list supported values.
13838정성태12/4/20242244오류 유형: 935. Windbg - Breakpoint 0's offset expression evaluation failed.
13837정성태12/3/20242257디버깅 기술: 204. Windbg - 윈도우 핸들 테이블 (3) - Windows 10 이상인 경우
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...