Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 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 - BPF_PERF_OUTPUT / BPF_MAP_TYPE_PERF_EVENT_ARRAY 사용법

지난 글에서 기본적인 Map 사용법을 알아봤는데요,

eBPF / bpf2go - (BPF_MAP_TYPE_HASH) Map을 이용한 전역 변수 구현
; https://www.sysnet.pe.kr/2/0/13817

BPF_MAP_TYPE_HASH와 같은 종류의 맵이 갖는 한 가지 특징이라면 max_entries 속성이 정적으로 설정된다는 점입니다.

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

물론, 예측 가능한 정도의 크기라면 문제가 되지 않겠지만, 그렇지 않은 경우라면 무작정 크게만 잡을 수도 없는 애매한 제약인데요, 만약 저런 제약이 문제가 된다면 BPF_MAP_TYPE_PERF_EVENT_ARRAY와 같은 Stream 유형의 Map을 사용할 수 있습니다.

Map type BPF_MAP_TYPE_PERF_EVENT_ARRAY (리눅스 커널 4.3부터 구현)
; https://docs.ebpf.io/linux/map-type/BPF_MAP_TYPE_PERF_EVENT_ARRAY/

This is a specialized map type which holds file descriptors to perf events. It is most commonly used by eBPF programs to efficiently send large amounts of data from kernel space to userspace, but it also has other uses.

...[생략]...

This usage scenario allows eBPF logic to piggy-back on the existing perf-subsystem implementation of ring-buffers to transfer data from the kernel to userspace.

...[생략]...

To recap, we create a perf event for every logical CPU, and every perf event gets its own ring-buffer.


CPU마다 할당된 ring-buffer 형식의 자료 구조로 커널에서는 끊임없이 쓰는(write) 용도로, user 모드의 프로그램에서는 그렇게 출력되는 데이터를 읽어내는(read) 식으로 동작하는 방식입니다. 즉, 커널에서 값을 보관해야 하는 등의 유지 비용을 최소화하고 그것을 사용자 모드의 프로그램에 내보내 후처리하는 거라고 보면 됩니다.




사용법도 특이합니다. 다른 Map은 그 구조를 정의하는 내부에 value의 타입을 지정하는데요, stream map의 경우에는 Map에 추가할 항목의 타입은 별도로 정의하고 단순히 Map에 대해서만 정의하면 됩니다.

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(int));
} my_map SEC(".maps");

또한 key_size, value_size는 무조건 4로 설정합니다.

Both the key_size and value_size must be exactly 4.

While the value_size is essentially unrestricted, the must always be 4 indicating the key is a 32-bit unsigned integer.


게다가 BPF_MAP_TYPE_PERF_EVENT_ARRAY의 경우에는 max_entries가 없는데요, 하지만 어쨌든 (쓰고 읽어내야만 하는) 매핑 공간의 크기는 나중에 클라이언트 측, 이 글에서는 go 언어 측에서 결정하게 됩니다.

실제로 쓸만한 예제가 하나 있으니,

eBPF Tutorial by Example 7: Capturing Process Execution, Output with perf event array
; https://eunomia.dev/en/tutorials/7-execsnoop/

BPF: Go frontend for execsnoop
; https://marselester.com/bpf-go-frontend-for-execsnoop.html

// 윈도우라면, 아마도 sysmon과 같은 프로그램을 만드는 것과 비슷한 느낌입니다. ^^

위의 글에 따라 그대로 구현해 보겠습니다. 우선, stream map을 하나 정의하고,

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(int));
} task_creation_events SEC(".maps");

/* 참고로, libbcc 방식이었다면 BPF_PERF_OUTPUT 매크로를 이용해 정의합니다.

2. BPF_PERF_OUTPUT
; https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#2-bpf_perf_output
*/

저 map에 들어갈 항목, 예제에서는 새로 실행하는 task의 정보를 저장할 구조체를 정의합니다.

struct task_creation_info {
    int pid;
    int ppid;
    int uid;
    char comm[32];
};

이 중에서, 일단 pid, ppid, uid는 BPF 측에서 제공하는 함수를 이용해 구할 수 있습니다.

struct task_creation_info item={};

uint64_t pid_tgid = bpf_get_current_pid_tgid();
item.pid = pid_tgid >> 32;

struct task_struct *task = (struct task_struct*)bpf_get_current_task();
item.ppid = BPF_CORE_READ(task, real_parent, tgid);

item.uid = (uint32_t)bpf_get_current_uid_gid();

마지막 남은 comm 필드는 tracepoint의 sys_enter_execve에 정의된 filename 인자의 값을 담고 싶은 건데요,

$ sudo cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/format
name: sys_enter_execve
ID: 767
format:
        field:unsigned short common_type;       offset:0;       size:2; signed:0;
        field:unsigned char common_flags;       offset:2;       size:1; signed:0;
        field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
        field:int common_pid;   offset:4;       size:4; signed:1;

        field:int __syscall_nr; offset:8;       size:4; signed:1;
        field:const char * filename;    offset:16;      size:8; signed:0;
        field:const char *const * argv; offset:24;      size:8; signed:0;
        field:const char *const * envp; offset:32;      size:8; signed:0;

print fmt: "filename: 0x%08lx, argv: 0x%08lx, envp: 0x%08lx", ((unsigned long)(REC->filename)), ((unsigned long)(REC->argv)), ((unsigned long)(REC->envp))

이런 식으로 코딩이 가능합니다.

// ...[생략: pid, ppid, uid]...

char* cmd_ptr = (char*)BPF_CORE_READ(ctx, args[0]);
bpf_probe_read_str(&item.comm, sizeof(item.comm), cmd_ptr);

여기까지 데이터 구성을 완료했으면, 이제 stream map으로 보내기 위해 bpf_perf_event_output 함수를 이용하는 것으로 마무리할 수 있습니다.

SEC("tracepoint/syscalls/sys_enter_execve")
int sys_enter_execve(struct trace_event_raw_sys_enter* ctx)
{
    struct task_creation_info item={};

    // ...[생략: pid, ppid, uid. comm]...

    bpf_perf_event_output(ctx, &task_creation_events, BPF_F_CURRENT_CPU, &item, sizeof(item));
    return 0;
}




일단, eBPF를 저렇게 만들어 bpf2go로 코드를 자동 생성했으면 이제 go 측에서 저 stream map을 읽어내는 동작을 해야 하는데요, 이 작업은 (cilium 측 패키지에서 제공하는) perf.NewReader와 자동 생성된 코드를 기반으로 다음과 같이 간단하게 처리할 수 있습니다.

func main() {
	// ...[생략]...

	go ReadExeCve(bpfObj)

	// ...[생략: 프로세스 중지를 막기 위한 코드]...
}

func ReadExeCve(bpfObj ebpf_basicObjects) {
    rd, err := perf.NewReader(bpfObj.TaskCreationEvents, os.Getpagesize() * 4)
    if err != nil {
        log.Printf("perf.NewReader: %v\n", err)
        return
    }
    defer func(rd *perf.Reader) {
        _ = rd.Close()
    }(rd)

    for {
        record, err := rd.Read()
        if err != nil {
            if errors.Is(err, perf.ErrClosed) {
                break
            }

            fmt.Printf("failed to Read ExeCve: %v\n", err)
        }

        fmt.Printf("Read ExeCve: %v\n", record.RawSample)
    }
}

BPF_MAP_TYPE_PERF_EVENT_ARRAY의 경우 max_entries를 eBPF 코드에서 지정하지 않고 클라이언트 측에서 설정한다고 했는데요, 위에서 perf.NewReader에 os.Getpagesize()를 넘겨준 것이 바로 그 크기입니다. 보통 운영체제 CPU의 페이지 크기가 4KB이므로, 위에서는 4개의 페이지 크기만큼을 할당한 것입니다.

하지만, BPF_MAP_TYPE_PERF_EVENT_ARRAY는 CPU별로 메모리 버퍼를 유지하기 때문에 다중 코어에서는, 가령 16개 코어라면 16 * (4KB * 4) = 256KB가 할당됩니다.

그럼 실행해 볼까요? ^^

그동안은 테스트 코드에서 bpf_printk를 사용해 그 결과를 /sys/kernel/debug/tracing/trace_pipe로 확인했는데요, 이제는 Map의 사용으로 인해 응용 프로그램 내에서 전용으로 출력 결과를 확인할 수 있게 됐습니다. 단지, record.RawSample은 바이트 배열에 불과하므로 출력이 현재는 다음과 같은 식으로 나오는데요,

...[생략]...
2024/11/15 23:56:37 Read ExeCve: [44 116 0 0 178 ...[생략]... 105 112 54 116 97 0]
2024/11/15 23:56:37 Read ExeCve: [45 116 0 0 178 ...[생략]... 105 112 54 116 97 0]
2024/11/15 23:56:37 Read ExeCve: [46 116 0 0 178 ...[생략]... 105 112 54 116 97 0]
2024/11/15 23:56:37 Read ExeCve: [47 116 0 0 178 ...[생략]... 105 112 54 116 97 0]

당연히 해석해야죠. ^^ 이를 위해 eBPF 코드에서 정의한 구조체와 동일한 바이트 형식을 갖는 go 구조체를 정의한 다음,

type TaskCreationInfo struct {
    PID  int32
    PPID int32
    UID  int32
    Comm [32]byte
}

코드에서 역직렬화 단계를 거쳐줍니다.

for {
    record, err := rd.Read()
    // ...[생략]...

    var e TaskCreationInfo
    err = binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, &e)
    if err != nil {
        fmt.Printf("failed to deserialize: %v\n", err)
        continue
    }

    pathBytes, _, _ := bytes.Cut(e.Comm[:], []byte{0}) // null 문자 이후를 제거
    fmt.Printf("pid: %v, ppid: %v, uid: %v, comm: %v\n", e.PID, e.PPID, e.UID, string(pathBytes))
}

이후, 출력은 대충 이런 식으로 나올 텐데요,

pid: 184640, ppid: 2508, uid: 0, comm: /usr/sbin/iptable
pid: 184641, ppid: 2508, uid: 0, comm: /usr/sbin/ip6table

훨씬 보기가 좋군요. ^^




혹시 이 예제를 CO-RE 도움 없이 제작할 수 있을까요? 일단은, tracepoint의 sys_enter_execve에 넘어오는 인자를 다음과 같이 정의하는 단계까지는 가능합니다.

// sudo cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_execve/format

struct tracepoint_syscalls_enter_execve_stub {
    __u64 unused1;
    int syscall_nr;
    __u32 padding;
    char* filename_ptr;
    /* char* argv_ptr; // 사용하지 않는다면 정의에서 제거
    char* envp_ptr; */
};

그리고 이전에 만들었던 예제에서, pid, uid, comm을 구하는 코드까지는 모두 작성할 수 있는데요, 문제는 ppid에서 걸립니다.

item.ppid = BPF_CORE_READ(task, real_parent, tgid);

보는 바와 같이 task->real_parent->tgid 필드를 접근하는 방식을 취하고 있는데, (libbpf 방식에서라면) 이것은 CO-RE를 사용하지 않으면 꽤나 제한된 환경에서만 동작할 수 있는 task_struct 구조체를 직접 정의해서 사용해야만 컴파일이 가능합니다.

어쩔 수 없습니다, 결국 1) ppid를 제거하거나 2) CO-RE를 사용하거나 3) libbcc 방식으로의 접근 중 한 가지를 선택해야 합니다.




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

[연관 글]






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

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 16  17  18  19  [20]  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13439정성태11/10/202311531닷넷: 2158. C# - 소켓 포트를 미리 시스템에 등록/예약해 사용하는 방법(Port Exclusion Ranges)파일 다운로드1
13438정성태11/9/202311037닷넷: 2157. C# - WinRT 기능을 이용해 윈도우에서 실행 중인 Media App 제어
13437정성태11/8/202311249닷넷: 2156. .NET 7 이상의 콘솔 프로그램을 (dockerfile 없이) 로컬 docker에 배포하는 방법
13436정성태11/7/202311332닷넷: 2155. C# - .NET 8 런타임부터 (Reflection 없이) 특성을 이용해 public이 아닌 멤버 호출 가능
13435정성태11/6/202310602닷넷: 2154. C# - 네이티브 자원을 포함한 관리 개체(예: 스레드)의 GC 정리
13434정성태11/1/202310581스크립트: 62. 파이썬 - class의 정적 함수를 동적으로 교체
13433정성태11/1/20239416스크립트: 61. 파이썬 - 함수 오버로딩 미지원
13432정성태10/31/202310222오류 유형: 878. 탐색기의 WSL 디렉터리 접근 시 "Attempt to access invalid address." 오류 발생
13431정성태10/31/202310748스크립트: 60. 파이썬 - 비동기 FastAPI 앱을 gunicorn으로 호스팅
13430정성태10/30/202310947닷넷: 2153. C# - 사용자가 빌드한 ICU dll 파일을 사용하는 방법
13429정성태10/27/202311086닷넷: 2152. Win32 Interop - C/C++ DLL로부터 이중 포인터 버퍼를 C#으로 받는 예제파일 다운로드1
13428정성태10/25/202311227닷넷: 2151. C# 12 - ref readonly 매개변수
13427정성태10/18/202310730닷넷: 2150. C# 12 - 정적 문맥에서 인스턴스 멤버에 대한 nameof 접근 허용(Allow nameof to always access instance members from static context)
13426정성태10/13/202311242스크립트: 59. 파이썬 - 비동기 호출 함수(run_until_complete, run_in_executor, create_task, run_in_threadpool)
13425정성태10/11/202311118닷넷: 2149. C# - PLinq의 Partitioner<T>를 이용한 사용자 정의 분할파일 다운로드1
13423정성태10/6/202311003스크립트: 58. 파이썬 - async/await 기본 사용법
13422정성태10/5/202310764닷넷: 2148. C# - async 유무에 따른 awaitable 메서드의 병렬 및 예외 처리 [1]
13421정성태10/4/202311027닷넷: 2147. C# - 비동기 메서드의 async 예약어 유무에 따른 차이
13420정성태9/26/202319272스크립트: 57. 파이썬 - UnboundLocalError: cannot access local variable '...' where it is not associated with a value
13419정성태9/25/202310868스크립트: 56. 파이썬 - RuntimeError: dictionary changed size during iteration
13418정성태9/25/202312496닷넷: 2146. C# - ConcurrentDictionary 자료 구조의 동기화 방식
13417정성태9/19/202311787닷넷: 2145. C# - 제네릭의 형식 매개변수에 속한 (매개변수를 가진) 생성자를 호출하는 방법
13416정성태9/19/202310389오류 유형: 877. redis-py - MISCONF Redis is configured to save RDB snapshots, ...
13415정성태9/18/202311819닷넷: 2144. C# 12 - 컬렉션 식(Collection Expressions) [2]
13414정성태9/16/202311159디버깅 기술: 193. Windbg - ThreadStatic 필드 값을 조사하는 방법
13413정성태9/14/202311976닷넷: 2143. C# - 시스템 Time Zone 변경 시 이벤트 알림을 받는 방법
... 16  17  18  19  [20]  21  22  23  24  25  26  27  28  29  30  ...