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

비밀번호

댓글 작성자
 




... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...
NoWriterDateCnt.TitleFile(s)
1780정성태10/15/201424168오류 유형: 249. The application-specific permission settings do not grant Local Activation permission for the COM Server application with CLSID
1779정성태10/15/201419679오류 유형: 248. Active Directory에서 OU가 지워지지 않는 경우
1778정성태10/10/201418136오류 유형: 247. The Netlogon service could not create server share C:\Windows\SYSVOL\sysvol\[도메인명]\SCRIPTS.
1777정성태10/10/201421222오류 유형: 246. The processing of Group Policy failed. Windows attempted to read the file \\[도메인]\sysvol\[도메인]\Policies\{...GUID...}\gpt.ini
1776정성태10/10/201418279오류 유형: 245. 이벤트 로그 - Name resolution for the name _ldap._tcp.dc._msdcs.[도메인명]. timed out after none of the configured DNS servers responded.
1775정성태10/9/201419394오류 유형: 244. Visual Studio 디버깅 (2) - Unable to break execution. This process is not currently executing the type of code that you selected to debug.
1774정성태10/9/201426598개발 환경 구성: 246. IIS 작업자 프로세스의 20분 자동 재생(Recycle)을 끄는 방법
1773정성태10/8/201429767.NET Framework: 471. 웹 브라우저로 다운로드가 되는 파일을 왜 C# 코드로 하면 안되는 걸까요? [1]
1772정성태10/3/201418550.NET Framework: 470. C# 3.0의 기본 인자(default parameter)가 .NET 1.1/2.0에서도 실행될까? [3]
1771정성태10/2/201428056개발 환경 구성: 245. 실행된 프로세스(EXE)의 명령행 인자를 확인하고 싶다면 - Sysmon [4]
1770정성태10/2/201421680개발 환경 구성: 244. 매크로 정의를 이용해 파일 하나로 C++과 C#에서 공유하는 방법 [1]파일 다운로드1
1769정성태10/1/201424099개발 환경 구성: 243. Scala 개발 환경 구성(JVM, 닷넷) [1]
1768정성태10/1/201419522개발 환경 구성: 242. 배치 파일에서 Thread.Sleep 효과를 주는 방법 [5]
1767정성태10/1/201424625VS.NET IDE: 94. Visual Studio 2012/2013에서의 매크로 구현 - Visual Commander [2]
1766정성태10/1/201422457개발 환경 구성: 241. 책 "프로그래밍 클로저: Lisp"을 읽고 나서. [1]
1765정성태9/30/201426043.NET Framework: 469. Unity3d에서 transform을 변수에 할당해 사용하는 특별한 이유가 있을까요?
1764정성태9/30/201422271오류 유형: 243. 파일 삭제가 안 되는 경우 - The action can't be comleted because the file is open in System
1763정성태9/30/201423845.NET Framework: 468. PDB 파일을 연동해 소스 코드 라인 정보를 알아내는 방법파일 다운로드1
1762정성태9/30/201424546.NET Framework: 467. 닷넷에서 EIP/RIP 레지스터 값을 구하는 방법 [1]파일 다운로드1
1761정성태9/29/201421560.NET Framework: 466. 윈도우 운영체제의 보안 그룹 이름 및 설명 문자열을 바꾸는 방법파일 다운로드1
1760정성태9/28/201419823.NET Framework: 465. ICorProfilerInfo::GetILToNativeMapping 메서드가 0x80131358을 반환하는 경우
1759정성태9/27/201430968개발 환경 구성: 240. Visual C++ / x64 환경에서 inline-assembly를 매크로 어셈블리로 대체하는 방법파일 다운로드1
1758정성태9/23/201437859개발 환경 구성: 239. 원격 데스크톱 접속(RDP)을 기존의 콘솔 모드처럼 사용하는 방법 [1]
1757정성태9/23/201418392오류 유형: 242. Lync로 모임 참여 시 소리만 들리지 않는 경우 - 두 번째 이야기
1756정성태9/23/201427393기타: 48. NVidia 제품의 과다한 디스크 사용 [2]
1755정성태9/22/201434193오류 유형: 241. Unity Web Player를 설치해도 여전히 설치하라는 화면이 나오는 경우 [4]
... 121  122  123  124  125  126  127  128  129  130  [131]  132  133  134  135  ...