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

비밀번호

댓글 작성자
 




... 151  152  153  154  155  156  157  158  159  160  [161]  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1023정성태4/20/201130053.NET Framework: 210. Windbg 환경에서 확인해 본 .NET 메서드 JIT 컴파일 전과 후 [1]
1022정성태4/19/201125619디버깅 기술: 38. .NET Disassembly 창에서의 F11(Step-into) 키 동작파일 다운로드1
1021정성태4/18/201127903디버깅 기술: 37. .NET 4.0 응용 프로그램의 Main 함수에 BreakPoint 걸기
1020정성태4/18/201128529오류 유형: 117. Failed to find runtime DLL (mscorwks.dll), 0x80004005
1019정성태4/17/201129171디버깅 기술: 36. Visual Studio의 .NET Disassembly 창의 call 호출에 사용되는 주소의 의미는? [1]파일 다운로드1
1018정성태4/16/201132811오류 유형: 116. 윈도우 업데이트 오류 - 0x8020000E
1017정성태4/14/201127618개발 환경 구성: 115. MSBuild - x86/x64, .NET 2/4, debug/release 빌드에 대한 배치 처리파일 다운로드1
1016정성태4/13/201143640개발 환경 구성: 114. Windows Thin PC 설치 [2]
1015정성태4/9/201128988.NET Framework: 209. AutoReset, ManualReset, Monitor.Wait의 차이파일 다운로드1
1014정성태4/7/2011106429오류 유형: 115. ORA-12516: TNS:listener could not find available handler with matching protocol stack [2]
1013정성태4/7/201124253Team Foundation Server: 45. SharePoint 2010 + TFS 2010 환경에서 ProcessGuidance.html 파일 다운로드 문제
1012정성태4/6/201132998.NET Framework: 208. WCF - 접속된 클라이언트의 IP 주소 알아내는 방법 [1]
1011정성태3/31/201135361오류 유형: 114. 인증서 갱신 오류 - The request contains no certificate template information.
1010정성태3/30/201126139개발 환경 구성: 113. 응용 프로그램 디자인 스케치 도구 - SketchFlow [4]
1009정성태3/29/201138440개발 환경 구성: 112. Visual Studio 2010 - .NET Framework 소스 코드 디버깅 [4]
1008정성태3/27/201130826.NET Framework: 207. C# - Right operand가 음수인 Shift 연산 결과 [2]
1007정성태3/16/201131665개발 환경 구성: 111. Excel - XML 파일 연동 [5]파일 다운로드1
1006정성태3/15/201125423.NET Framework: 206. XML/XSD - 외래키처럼 참조 제한 거는 방법파일 다운로드1
1005정성태3/11/201135264개발 환경 구성: 110. 엑셀 매크로 함수 관련 오류 [2]
1004정성태3/3/201124460개발 환경 구성: 109. SharePoint Health Analyzer 디스크 부족 경고 제어
1003정성태3/3/201125500오류 유형: 113. SQL Server - DB Attach 시 Parameter name: nColIndex 오류 발생
1002정성태3/2/201123898Team Foundation Server: 44. TFS 설치 후, Team Portal의 Dashboard를 빠르게 확인하는 방법
1001정성태3/2/201127913Team Foundation Server: 43. TFS 2010 + SharePoint 2010 설치
1000정성태3/1/201132866오류 유형: 112. Remote FX RDP 연결 시 오류 유형 2가지 [5]
999정성태2/28/201146417개발 환경 구성: 108. RemoteFX - Windows 7 가상 머신에서 DirectX 9c 환경을 제공 [5]
998정성태2/27/201120111Team Foundation Server: 42. TFS Application-Tier만 재설치
... 151  152  153  154  155  156  157  158  159  160  [161]  162  163  164  165  ...