Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 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 / 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)
13864정성태1/15/20255Linux: 114. eBPF를 위해 필요한 SELinux 보안 정책
13863정성태1/14/2025327Linux: 113. Linux - 프로세스를 위한 전용 SELinux 보안 문맥 지정
13862정성태1/13/2025234Linux: 112. Linux - 데몬을 위한 SELinux 보안 정책 설정
13861정성태1/11/2025680Windows: 276. 명령행에서 원격 서비스를 동기/비동기로 시작/중지
13860정성태1/10/2025751디버깅 기술: 216. WinDbg - 2가지 유형의 식 평가 방법(MASM, C++)
13859정성태1/9/2025823디버깅 기술: 215. Windbg - syscall 이후 실행되는 KiSystemCall64 함수 및 SSDT 디버깅
13858정성태1/8/2025856개발 환경 구성: 738. PowerShell - 원격 호출 시 "powershell.exe"가 아닌 "pwsh.exe" 환경으로 명령어를 실행하는 방법
13857정성태1/7/2025876C/C++: 187. Golang - 콘솔 응용 프로그램을 Linux 데몬 서비스를 지원하도록 변경파일 다운로드1
13856정성태1/6/2025923디버깅 기술: 214. Windbg - syscall 단계까지의 Win32 API 호출 (예: Sleep)
13855정성태12/28/20241440오류 유형: 941. Golang - os.StartProcess() 사용 시 오류 정리
13854정성태12/27/20241421C/C++: 186. Golang - 콘솔 응용 프로그램을 NT 서비스를 지원하도록 변경파일 다운로드1
13853정성태12/26/20241665디버깅 기술: 213. Windbg - swapgs 명령어와 (Ring 0 커널 모드의) FS, GS Segment 레지스터
13852정성태12/25/20241603디버깅 기술: 212. Windbg - (Ring 3 사용자 모드의) FS, GS Segment 레지스터파일 다운로드1
13851정성태12/23/20241516디버깅 기술: 211. Windbg - 커널 모드 디버깅 상태에서 사용자 프로그램을 디버깅하는 방법
13850정성태12/23/20241546오류 유형: 940. "Application Information" 서비스를 중지한 경우, "This file does not have an app associated with it for performing this action."
13849정성태12/20/20241562디버깅 기술: 210. Windbg - 논리(가상) 주소를 Segmentation을 거쳐 선형 주소로 변경
13848정성태12/18/20241797디버깅 기술: 209. Windbg로 알아보는 Prototype PTE파일 다운로드2
13847정성태12/18/20241699오류 유형: 939. golang - 빌드 시 "unknown directive: toolchain" 오류 빌드 시 이런 오류가 발생한다면?
13846정성태12/17/20241789디버깅 기술: 208. Windbg로 알아보는 Trans/Soft PTE와 2가지 Page Fault 유형파일 다운로드1
13845정성태12/16/20241985디버깅 기술: 207. Windbg로 알아보는 PTE (_MMPTE)
13844정성태12/14/20241860디버깅 기술: 206. Windbg로 알아보는 PFN (_MMPFN)파일 다운로드1
13843정성태12/13/20241877오류 유형: 938. Docker container 내에서 빌드 시 error MSB3021: Unable to copy file "..." to "...". Access to the path '...' is denied.
13842정성태12/12/20242306디버깅 기술: 205. Windbg - KPCR, KPRCB
13841정성태12/11/20242245오류 유형: 937. error MSB4044: The "ValidateValidArchitecture" task was not given a value for the required parameter "RemoteTarget"
13840정성태12/11/20242064오류 유형: 936. msbuild - Your project file doesn't list 'win' as a "RuntimeIdentifier"
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...