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

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13718정성태8/27/20247401오류 유형: 921. Visual C++ - error C1083: Cannot open include file: 'float.h': No such file or directory [2]
13717정성태8/26/20247007VS.NET IDE: 192. Visual Studio 2022 - Windows XP / 2003용 C/C++ 프로젝트 빌드
13716정성태8/21/20246741C/C++: 167. Visual C++ - 윈도우 환경에서 _execv 동작 [1]
13715정성태8/19/20247337Linux: 78. 리눅스 C/C++ - 특정 버전의 glibc 빌드 (docker-glibc-builder)
13714정성태8/19/20246734닷넷: 2295. C# 12 - 기본 생성자(Primary constructors) (책 오타 수정) [3]
13713정성태8/16/20247432개발 환경 구성: 721. WSL 2에서의 Hyper-V Socket 연동
13712정성태8/14/20247210개발 환경 구성: 720. Synology NAS - docker 원격 제어를 위한 TCP 바인딩 추가
13711정성태8/13/20248047Linux: 77. C# / Linux - zombie process (defunct process) [1]파일 다운로드1
13710정성태8/8/20247964닷넷: 2294. C# 13 - (6) iterator 또는 비동기 메서드에서 ref와 unsafe 사용을 부분적으로 허용파일 다운로드1
13709정성태8/7/20247734닷넷: 2293. C# - safe/unsafe 문맥에 대한 C# 13의 (하위 호환을 깨는) 변화파일 다운로드1
13708정성태8/7/20247522개발 환경 구성: 719. ffmpeg / YoutubeExplode - mp4 동영상 파일로부터 Audio 파일 추출
13707정성태8/6/20247754닷넷: 2292. C# - 자식 프로세스의 출력이 4,096보다 많은 경우 Process.WaitForExit 호출 시 hang 현상파일 다운로드1
13706정성태8/5/20247870개발 환경 구성: 718. Hyper-V - 리눅스 VM에 새로운 디스크 추가
13705정성태8/4/20248139닷넷: 2291. C# 13 - (5) params 인자 타입으로 컬렉션 허용 [2]파일 다운로드1
13704정성태8/2/20248095닷넷: 2290. C# - 간이 dotnet-dump 프로그램 만들기파일 다운로드1
13703정성태8/1/20247422닷넷: 2289. "dotnet-dump ps" 명령어가 닷넷 프로세스를 찾는 방법
13702정성태7/31/20247821닷넷: 2288. Collection 식을 지원하는 사용자 정의 타입을 CollectionBuilder 특성으로 성능 보완파일 다운로드1
13701정성태7/30/20248092닷넷: 2287. C# 13 - (4) Indexer를 이용한 개체 초기화 구문에서 System.Index 연산자 허용파일 다운로드1
13700정성태7/29/20247689디버깅 기술: 200. DLL Export/Import의 Hint 의미
13699정성태7/27/20248222닷넷: 2286. C# 13 - (3) Monitor를 대체할 Lock 타입파일 다운로드1
13698정성태7/27/20248178닷넷: 2285. C# - async 메서드에서의 System.Threading.Lock 잠금 처리파일 다운로드1
13697정성태7/26/20247905닷넷: 2284. C# - async 메서드에서의 lock/Monitor.Enter/Exit 잠금 처리파일 다운로드1
13696정성태7/26/20247439오류 유형: 920. dotnet publish - error NETSDK1047: Assets file '...\obj\project.assets.json' doesn't have a target for '...'
13695정성태7/25/20247422닷넷: 2283. C# - Lock / Wait 상태에서도 STA COM 메서드 호출 처리파일 다운로드1
13694정성태7/25/20247886닷넷: 2282. C# - ASP.NET Core Web App의 Request 용량 상한값 (Kestrel, IIS)
13693정성태7/24/20247218개발 환경 구성: 717. Visual Studio - C# 프로젝트에서 레지스트리에 등록하지 않은 COM 개체 참조 및 사용 방법파일 다운로드1
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...