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)
13237정성태1/30/202314294.NET Framework: 2091. C# - 웹 사이트가 어떤 버전의 TLS/SSL을 지원하는지 확인하는 방법
13236정성태1/29/202313048개발 환경 구성: 663. openssl을 이용해 인트라넷 IIS 사이트의 SSL 인증서 생성
13235정성태1/29/202312527개발 환경 구성: 662. openssl - 윈도우 환경의 명령행에서 SAN 적용하는 방법
13234정성태1/28/202314940개발 환경 구성: 661. dnSpy를 이용해 소스 코드가 없는 .NET 어셈블리의 코드를 변경하는 방법 [1]
13233정성태1/28/202316126오류 유형: 840. C# - WebClient로 https 호출 시 "The request was aborted: Could not create SSL/TLS secure channel" 예외 발생
13232정성태1/27/202312038스크립트: 43. uwsgi의 --processes와 --threads 옵션
13231정성태1/27/202311265오류 유형: 839. python - TypeError: '...' object is not callable
13230정성태1/26/202312018개발 환경 구성: 660. WSL 2 내부로부터 호스트 측의 네트워크로 UDP 데이터가 1개의 패킷으로만 제한되는 문제
13229정성태1/25/202313897.NET Framework: 2090. C# - UDP Datagram의 최대 크기
13228정성태1/24/202314451.NET Framework: 2089. C# - WMI 논리 디스크가 속한 물리 디스크의 정보를 얻는 방법 [2]파일 다운로드1
13227정성태1/23/202313354개발 환경 구성: 659. Windows - IP MTU 값을 바꿀 수 있을까요? [1]
13226정성태1/23/202312310.NET Framework: 2088. .NET 5부터 지원하는 GetRawSocketOption 사용 시 주의할 점
13225정성태1/21/202311645개발 환경 구성: 658. Windows에서 실행 중인 소켓 서버를 다른 PC 또는 WSL에서 접속할 수 없는 경우
13224정성태1/21/202312148Windows: 221. Windows - Private/Public/Domain이 아닌 네트워크 어댑터 단위로 방화벽을 on/off하는 방법
13223정성태1/20/202312064오류 유형: 838. RDP 연결 오류 - The two computers couldn't connect in the amount of time allotted
13222정성태1/20/202311923개발 환경 구성: 657. WSL - DockerDesktop.vhdx 파일 위치를 옮기는 방법
13221정성태1/19/202312122Linux: 57. C# - 리눅스 프로세스 메모리 정보파일 다운로드1
13220정성태1/19/202311785오류 유형: 837. NETSDK1045 The current .NET SDK does not support targeting .NET ...
13219정성태1/18/202311406Windows: 220. 네트워크의 인터넷 접속 가능 여부에 대한 판단 기준
13218정성태1/17/202311271VS.NET IDE: 178. Visual Studio 17.5 (Preview 2) - 포트 터널링을 이용한 웹 응용 프로그램의 외부 접근 허용
13217정성태1/13/202311988디버깅 기술: 185. windbg - 64비트 운영체제에서 작업 관리자로 뜬 32비트 프로세스의 덤프를 sos로 디버깅하는 방법
13216정성태1/12/202311900디버깅 기술: 184. windbg - 32비트 프로세스의 메모리 덤프인 경우 !peb 명령어로 나타나지 않는 환경 변수
13215정성태1/11/202314928Linux: 56. 리눅스 - /proc/pid/stat 정보를 이용해 프로세스의 CPU 사용량 구하는 방법 [1]
13214정성태1/10/202313825.NET Framework: 2087. .NET 6부터 SourceGenerator와 통합된 System.Text.Json [1]파일 다운로드1
13213정성태1/9/202312181오류 유형: 836. docker 이미지 빌드 시 "RUN apt install ..." 명령어가 실패하는 이유
13212정성태1/8/202313558기타: 85. 단정도/배정도 부동 소수점의 정밀도(Precision)에 따른 형변환 손실
... 16  17  18  19  20  21  22  23  24  25  26  27  [28]  29  30  ...