Microsoft MVP성태의 닷넷 이야기
Linux: 101. eBPF 함수의 인자를 다루는 방법 [링크 복사], [링크+제목 복사],
조회: 1197
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)

eBPF 함수의 인자를 다루는 방법

지난 글에 실습한 kprobe 예제는,

SEC("kprobe/sys_clone") int kprobe_sys_clone(void *ctx)
{
    u64 pid_tgid = bpf_get_current_pid_tgid();
    pid_t tgid = pid_tgid >> 32;
    pid_t pid = pid_tgid;

    bpf_printk("pid == %d, thread_id == %d, newsp == %x\n", tgid, pid, clone_flags);
    return -1;
}

인자를 void* 타입으로 퉁쳤었는데요, 이제 그것을 수정해 보겠습니다. 이를 위해 우선 kprobe 대상 함수("sys_clone")의 인자를 확인해 보면,

// https://github.com/torvalds/linux/blob/ae90f6a6170d7a7a1aa4fddf664fbd093e3023bc/include/linux/syscalls.h#L786

#ifdef CONFIG_CLONE_BACKWARDS
asmlinkage long sys_clone(unsigned long, unsigned long, int __user *, unsigned long,
           int __user *);
#else
#ifdef CONFIG_CLONE_BACKWARDS3
asmlinkage long sys_clone(unsigned long, unsigned long, int, int __user *,
              int __user *, unsigned long);
#else
asmlinkage long sys_clone(unsigned long, unsigned long, int __user *,
           int __user *, unsigned long);
#endif

5개의 인자가 나오는데요, 이것을 접근하는 방법은 eBPF 함수에 기본적으로 전달되는 pt_regs를 이용해 PT_REGS_PARAM 매크로를 사용하면 됩니다.

SEC("kprobe/sys_clone") int kprobe_sys_clone(struct pt_regs* ctx)
{
    unsigned long clone_flags = PT_REGS_PARM1(ctx);
    unsigned long newsp = PT_REGS_PARM2(ctx);
    int* parent_tidptr = (int*)PT_REGS_PARM3(ctx);
    unsigned long tls = PT_REGS_PARM4(ctx);
    int* child_tidptr = (int*)PT_REGS_PARM5(ctx);

    // ...[생략]...
}

여기서 pt_regs는 Win32 환경이라면 CONTEXT 구조체와 비슷한 역할을 합니다. 따라서 CPU 레지스터를 대표하기 때문에 운영체제가 정한 ABI에 따라 레지스터를 통해 들어오는 인자와 Stack Pointer로부터의 오프셋 계산으로 인자를 추출할 수 있습니다.

리눅스의 x86-64 ABI인 경우, 첫 6개의 매개변수를 rdi, rsi, rdx, rcx, r8, r9 레지스터에 전달한다고 합니다. 만약 부동 소수점 타입인 경우에는 xmm 레지스터로 전달하고, 그 이상의 매개변수인 경우에는 스택을 이용합니다.




위와 같은 접근법이 libbpf를 도입하면 약간 바뀌는데요, 이에 대해서는 설명한 적이 있습니다.

eBPF의 2가지 방식 - libbcc와 libbpf(CO-RE) - 8. Kprobes
; https://www.sysnet.pe.kr/2/0/13801#8

즉, BPF_KPROBE 매크로 함수를 사용해 pt_regs를 숨기고 인자를 직접 지정할 수 있는데,

#ifdef __BCC__
int kprobe__acct_collect(struct pt_regs *ctx, long exit_code, int group_dead)
#else
SEC("kprobe/acct_collect")
int BPF_KPROBE(kprobe__acct_collect, long exit_code, int group_dead)
#endif
{
    /* BPF code accessing exit_code and group_dead here */
}

원한다면 일부 인자만 정의해도 됩니다. 가령 kprobe_sys_clone 함수를 첫 번째 인자(clone_flags)만 관심이 있다면 이렇게 정의할 수 있습니다.

SEC("kprobe/sys_clone") int BPF_KPROBE(kprobe_sys_clone, unsigned long clone_flags)
{
    bpf_printk("clone_flags == %x\n", clone_flags);
    return 0;
}

그런데 사실 pt_regs 인자가 없어진 것이 아닙니다. 이를 위해 BPF_KPROBE 매크로 정의를 보면,

#define BPF_KPROBE(name, args...)                       \
name(struct pt_regs *ctx);                          \
static __always_inline typeof(name(0))                      \
____##name(struct pt_regs *ctx, ##args);                    \
typeof(name(0)) name(struct pt_regs *ctx)                   \
{                                       \
    _Pragma("GCC diagnostic push")                      \
    _Pragma("GCC diagnostic ignored \"-Wint-conversion\"")          \
    return ____##name(___bpf_kprobe_args(args));                \
    _Pragma("GCC diagnostic pop")                       \
}   

pt_regs 인자를 ctx라는 숨겨진 변수로 처리하고 있습니다. 결국 다음과 같이 BPF_KPROBE의 인자 지정과 함께 혼용하는 것도 가능합니다.

// 이렇게 사용하는 것도 가능
SEC("kprobe/sys_clone") int BPF_KPROBE(kprobe_sys_clone, unsigned long clone_flags)
{
    unsigned long newsp = PT_REGS_PARM2(ctx); // BPF_KPROBE에 지정하지 않은 2번째 인자를 매크로로 추출
    bpf_printk("clone_flags == %x, newsp == %x\n", clone_flags, newsp);

    return 0;
}

다시 말해, pt_regs* ctx가 없어진 것이 아니라 그냥 가볍게 감싼 것에 불과하기 때문에 PT_REGS_PARM 매크로를 이용한 방법은 여전히 유효합니다.

SEC("kprobe/sys_clone") int BPF_KPROBE(kprobe_sys_clone, unsigned long clone_flags, unsigned long newsp, int* parent_tidptr, unsigned long tls, int* child_tidptr)
{
    unsigned long clone_flags2 = PT_REGS_PARM1(ctx);
    bpf_printk("clone_flags == %x, clone_flags2 == %x\n", clone_flags, clone_flags2);

    unsigned long newsp2 = PT_REGS_PARM2(ctx);
    bpf_printk("newsp == %x, newsp2 == %x\n", newsp, newsp2);

    int* parent_tidptr2 = (int*)PT_REGS_PARM3(ctx);
    bpf_printk("parent_tidptr == %x, parent_tidptr2 == %x\n", parent_tidptr, parent_tidptr2);

    unsigned long tls2 = PT_REGS_PARM4(ctx);
    bpf_printk("tls == %x, tls2 == %x\n", tls, tls2);

    int* child_tidptr2 = (int*)PT_REGS_PARM5(ctx);
    bpf_printk("child_tidptr == %x, child_tidptr2 == %x\n", child_tidptr, child_tidptr2);
    return 0;
}




그럼 커널 구조체도 접근해 볼까요? ^^ 예를 들기 위해 tcp_connect 함수를 대상으로 할 텐데요, 그 함수의 첫 번째 인자는 struct sock *sk입니다.

struct sock
; https://github.com/torvalds/linux/blob/master/include/net/sock.h#L342

struct sock 구조체의 내부에는 다시 struct sock_common 구조체가 있는데요,

struct sock_common
; https://github.com/torvalds/linux/blob/master/include/net/sock.h#L150

물론, 커널 헤더 파일의 정의대로 직접 eBPF 소스코드에 복사해 사용할 수도 있지만 vmlinux.h를 생성하면 저 구조체들이 포함돼 있으므로 그걸 활용하는 것이 더 깔끔합니다.

$ grep -A 5 "struct sock {" vmlinux.h
struct sock {
        struct sock_common __sk_common;
        socket_lock_t sk_lock;
        atomic_t sk_drops;
        int sk_rcvlowat;
        struct sk_buff_head sk_error_queue;

$ grep -A 60 "struct sock_common {" vmlinux.h
struct sock_common {
        union {
                __addrpair skc_addrpair;
                struct {
                        __be32 skc_daddr;
                        __be32 skc_rcv_saddr;
                };
        };
        union {
                unsigned int skc_hash;
                __u16 skc_u16hashes[2];
        };
        union {
                __portpair skc_portpair;
                struct {
                        __be16 skc_dport;
                        __u16 skc_num;
                };
        };
        short unsigned int skc_family;
        volatile unsigned char skc_state;
        unsigned char skc_reuse: 4;
        unsigned char skc_reuseport: 1;
        unsigned char skc_ipv6only: 1;
        unsigned char skc_net_refcnt: 1;
        int skc_bound_dev_if;
        union {
                struct hlist_node skc_bind_node;
                struct hlist_node skc_portaddr_node;
        };
        struct proto *skc_prot;
        possible_net_t skc_net;
        struct in6_addr skc_v6_daddr;
        struct in6_addr skc_v6_rcv_saddr;
        atomic64_t skc_cookie;
        union {
                long unsigned int skc_flags;
                struct sock *skc_listener;
                struct inet_timewait_death_row *skc_tw_dr;
        };
        int skc_dontcopy_begin[0];
        union {
                struct hlist_node skc_node;
                struct hlist_nulls_node skc_nulls_node;
        };
        short unsigned int skc_tx_queue_mapping;
        short unsigned int skc_rx_queue_mapping;
        union {
                int skc_incoming_cpu;
                u32 skc_rcv_wnd;
                u32 skc_tw_rcv_nxt;
        };
        refcount_t skc_refcnt;
        int skc_dontcopy_end[0];
        union {
                u32 skc_rxhash;
                u32 skc_window_clamp;
                u32 skc_tw_snd_nxt;
        };
};

그럼, 이걸 이용해 eBPF 내부에서 INET/INET6 패밀리만을 추적하도록 코드를 수정할 수 있습니다.

SEC("fentry/tcp_connect")
int BPF_PROG(tcp_connect, struct sock *sk)
{
    if (sk->__sk_common.skc_family != AF_INET &&
        sk->__sk_common.skc_family != AF_INET6)
    {
        return 0;
    }

    pid_t tgid = bpf_get_current_pid_tgid() >> 32;
    bpf_printk("fentry-tcp_connect: pid = %d\n", tgid);
    return 0;
}

물론 저렇게 코딩하는 것은 libbcc의 방법에서 가능한 것이고, libbpf에서라면 bpf_probe_read로 다음과 같이 접근해야 합니다.

// BPF_CORE_READ() - #include <bpf/bpf_core_read.h>
// https://nakryiko.com/posts/bpf-core-reference-guide/#bpf-core-read-1

unsigned short sk_family = BPF_CORE_READ(sk, __sk_common.skc_family);




PT_REGS_PARM1 ~ PT_REGS_PARM5 매크로를 사용한 경우, 또는 BPF_KPROBE 매크로에 의해 내부적으로 PT_REGS_PARM을 사용하는 경우,

SEC("kprobe/sys_clone") int BPF_KPROBE(kprobe_sys_clone, unsigned long clone_flags)
{
    bpf_printk("clone_flags == %x\n", clone_flags);
    return 0;
}

기존 bpf2go 명령어로 컴파일하면,

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go ebpf_basic basic.c

이런 오류가 발생합니다.

$ go generate
/mnt/c/temp/ebpf_sample/basic.c:20:29: error: The eBPF is using target specific macros, please provide -target that is not bpf, bpfel or bpfeb
   20 | SEC("kprobe/sys_clone") int BPF_KPROBE(kprobe_sys_clone, unsigned long clone_flags)
      |                             ^
/usr/include/bpf/bpf_tracing.h:429:20: note: expanded from macro 'BPF_KPROBE'
  429 |         return ____##name(___bpf_kprobe_args(args));                        \
      |                           ^
/usr/include/bpf/bpf_tracing.h:409:2: note: expanded from macro '___bpf_kprobe_args'
  409 |         ___bpf_apply(___bpf_kprobe_args, ___bpf_narg(args))(args)
      |         ^
/usr/include/bpf/bpf_helpers.h:165:29: note: expanded from macro '___bpf_apply'
  165 | #define ___bpf_apply(fn, n) ___bpf_concat(fn, n)
      |                             ^
note: (skipping 2 expansions in backtrace; use -fmacro-backtrace-limit=0 to see all)
/usr/include/bpf/bpf_tracing.h:399:33: note: expanded from macro '___bpf_kprobe_args1'
  399 |         ___bpf_kprobe_args0(), (void *)PT_REGS_PARM1(ctx)
      |                                        ^
/usr/include/bpf/bpf_tracing.h:309:29: note: expanded from macro 'PT_REGS_PARM1'
  309 | #define PT_REGS_PARM1(x) ({ _Pragma(__BPF_TARGET_MISSING); 0l; })
      |                             ^
:21:6: note: expanded from here
   21 |  GCC error "The eBPF is using target specific macros, please provide -target that is not bpf, bpfel or bpfeb"
      |      ^
1 error generated.
Error: compile: exit status 1
exit status 1
main.go:3: running "go": exit status 1

왜냐하면, 해당 매크로를 이용하게 되면 더 이상 endian 중립적인 코드를 사용할 수 없기 때문에 명시적으로 대상 플랫폼을 지정해야 하는데요, 이를 위해 "-target" 인자를 추가할 수 있습니다.

The eBPF is using target specific macros, please provide -target #772
; https://github.com/cilium/ebpf/discussions/772

//go2:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 ebpf_basic basic.c

가능한 platform 타깃은 bpf, bpfel, bpfeb, 386, amd64, arm, arm64, loong64, mips, ppc64, ppc64le, riscv64, s390x입니다.

참고로, arm64 타깃으로 컴파일하는 경우,

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target arm64 ebpf_basic basic.c

이런 오류가 발생합니다.

$ go generate
Compiled /mnt/c/temp/ebpf_sample/ebpf_basic_x86_bpfel.o
Stripped /mnt/c/temp/ebpf_sample/ebpf_basic_x86_bpfel.o
Wrote /mnt/c/temp/ebpf_sample/ebpf_basic_x86_bpfel.go
/mnt/c/temp/ebpf_sample/basic.c:18:33: error: incomplete definition of type 'struct user_pt_regs'
   18 |     unsigned long clone_flags = PT_REGS_PARM1(ctx);
      |                                 ^~~~~~~~~~~~~~~~~~
/usr/include/bpf/bpf_tracing.h:195:49: note: expanded from macro 'PT_REGS_PARM1'
  195 | #define PT_REGS_PARM1(x) (((PT_REGS_ARM64 *)(x))->regs[0])
      |                           ~~~~~~~~~~~~~~~~~~~~~~^
/mnt/c/temp/ebpf_sample/basic.c:18:33: note: forward declaration of 'struct user_pt_regs'
/usr/include/bpf/bpf_tracing.h:195:29: note: expanded from macro 'PT_REGS_PARM1'
  195 | #define PT_REGS_PARM1(x) (((PT_REGS_ARM64 *)(x))->regs[0])
      |                             ^
/usr/include/bpf/bpf_tracing.h:194:45: note: expanded from macro 'PT_REGS_ARM64'
  194 | #define PT_REGS_ARM64 const volatile struct user_pt_regs
      |                                             ^
1 error generated.
Error: compile: exit status 1
exit status 1
main.go:4: running "go": exit status 1

ARM의 경우 CPU 레지스터 구조가 바뀌는데 이를 표현하기 위한 구조체가 user_pt_regs로 정의돼 있습니다. 위의 코드는 PT_REGS_ARM64 매크로가 user_pt_regs를 사용해 펼치긴 했지만 정작 그 구조체가 정의돼 있지 않아 컴파일 오류가 발생한 것입니다. 그런데, 이 오류를 해결하는 방법이 애매합니다. 아마도 ARM64 환경에서 저 빌드를 해야 하는 것 같은데요, 일단 user_pt_regs 구조체의 정의는 다음과 같습니다.

// https://github.com/torvalds/linux/blob/master/arch/arm64/include/uapi/asm/ptrace.h

struct user_pt_regs {
	__u64		regs[31];
	__u64		sp;
	__u64		pc;
	__u64		pstate;
};




PT_REGS_PARM 매크로를 이용해 인자를 포인터로 받으면,

int* parent_tidptr = PT_REGS_PARM3(ctx);
int value = *parent_tidptr;

이런 컴파일 오류가 발생합니다.

$ go generate
/mnt/c/temp/ebpf_sample/basic.c:28:10: error: incompatible integer to pointer conversion initializing 'int *' with an expression of type 'unsigned long' [-Wint-conversion]
   28 |     int* parent_tidptr = PT_REGS_PARM3(ctx);
      |          ^               ~~~~~~~~~~~~~~~~~~
1 error generated.
Error: compile: exit status 1
exit status 1
main.go:4: running "go": exit status 1

당연히 형변환이 필요한데요,

int* parent_tidptr = (int*)PT_REGS_PARM3(ctx);

문제는, 이렇게 포인터 변수로부터 값을 참조해 사용하면,

unsigned long clone_flags = PT_REGS_PARM1(ctx);
int* parent_tidptr = (int*)PT_REGS_PARM3(ctx);
int ptid = *parent_tidptr;

bpf_printk("clone_flags == %x, ptid == %x\n", clone_flags, ptid);

런타임 시 이런 오류가 발생합니다.

field KprobeSysClone: program kprobe_sys_clone: load program: permission denied: 2: (61) r7 = *(u32 *)(r1 +0): R1 invalid mem access 'scalar' (7 line(s) omitted)

이에 대해서 검색해 보면,

How to correctly read socket->sk from pt_regs* in ebpf program?
; https://stackoverflow.com/questions/76960866/how-to-correctly-read-socket-sk-from-pt-regs-in-ebpf-program

eBPF Verifier 입장에서는 저 포인터의 타입은 물론, 그것이 포인터인지조차도 알 수 없다고 합니다. 따라서 그런 식으로 포인터의 값을 읽을 수는 없고 별도로 bpf_probe_read_kernel/bpf_probe_read_user 함수를 이용해야 한다고 합니다.

int ptid = 0;
int* parent_tidptr = (int*)PT_REGS_PARM3(ctx);
if (parent_tidptr != NULL)
{
    bpf_probe_read_kernel(&ptid, sizeof(__u32), parent_tidptr);
}




BPF 함수는 매개변수의 수가 5개를 넘을 수 없습니다. 따라서 그 이상의 매개변수가 정의된 경우가 있다면,

// https://github.com/torvalds/linux/blob/ae90f6a6170d7a7a1aa4fddf664fbd093e3023bc/include/linux/syscalls.h#L786

unsigned long ksys_mmap_pgoff(unsigned long addr, unsigned long len,
                  unsigned long prot, unsigned long flags,
                  unsigned long fd, unsigned long pgoff);

SEC("kprobe/ksys_mmap_pgoff") int BPF_KPROBE(ksys_mmap_pgoff, unsigned long addr, unsigned long len,
                                                            unsigned long prot, unsigned long flags,
                                                            unsigned long fd, unsigned long pgoff)
{
    bpf_printk("addr == %x\n", addr);
    bpf_printk("len == %x\n", len);
    bpf_printk("prot == %x\n", prot);
    bpf_printk("flags == %x\n", flags);
    bpf_printk("fd == %x\n", fd);
    bpf_printk("pgoff == %x\n", pgoff);
    return 0;
}

빌드 시 이런 오류가 발생합니다.

/mnt/c/temp/ebpf_sample/basic.c:26:35: error: call to undeclared function '___bpf_kprobe_args6'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]
   26 | SEC("kprobe/ksys_mmap_pgoff") int BPF_KPROBE(ksys_mmap_pgoff, unsigned long addr, unsigned long len,
      |                                   ^
/usr/include/bpf/bpf_tracing.h:429:20: note: expanded from macro 'BPF_KPROBE'
  429 |         return ____##name(___bpf_kprobe_args(args));                        \
      |                           ^
/usr/include/bpf/bpf_tracing.h:409:2: note: expanded from macro '___bpf_kprobe_args'
  409 |         ___bpf_apply(___bpf_kprobe_args, ___bpf_narg(args))(args)
      |         ^
/usr/include/bpf/bpf_helpers.h:165:29: note: expanded from macro '___bpf_apply'
  165 | #define ___bpf_apply(fn, n) ___bpf_concat(fn, n)
      |                             ^
/usr/include/bpf/bpf_helpers.h:162:29: note: expanded from macro '___bpf_concat'
  162 | #define ___bpf_concat(a, b) a ## b
      |                             ^
<scratch space>:36:1: note: expanded from here
   36 | ___bpf_kprobe_args6
      | ^
/mnt/c/temp/ebpf_sample/basic.c:26:63: error: expected expression
   26 | SEC("kprobe/ksys_mmap_pgoff") int BPF_KPROBE(ksys_mmap_pgoff, unsigned long addr, unsigned long len,
      |                                                               ^
/mnt/c/temp/ebpf_sample/basic.c:26:83: error: expected expression
   26 | SEC("kprobe/ksys_mmap_pgoff") int BPF_KPROBE(ksys_mmap_pgoff, unsigned long addr, unsigned long len,
      |                                                                                   ^
/mnt/c/temp/ebpf_sample/basic.c:27:61: error: expected expression
   27 |                                                             unsigned long prot, unsigned long flags,
      |                                                             ^
/mnt/c/temp/ebpf_sample/basic.c:27:81: error: expected expression
   27 |                                                             unsigned long prot, unsigned long flags,
      |                                                                                 ^
/mnt/c/temp/ebpf_sample/basic.c:28:61: error: expected expression
   28 |                                                             unsigned long fd, unsigned long pgoff)
      |                                                             ^
/mnt/c/temp/ebpf_sample/basic.c:28:79: error: expected expression
   28 |                                                             unsigned long fd, unsigned long pgoff)
      |                                                                               ^
7 errors generated.
Error: compile: exit status 1
exit status 1
main.go:4: running "go": exit status 1

___bpf_kprobe_args6 정의가 없다는 것인데, 실제로 제가 빌드하는 환경에 있는 "/usr/include/bpf/bpf_tracing.h" 파일에 그 정의를 찾아볼 수 없습니다. 반면 libbpf 소스코드를 보면,

// https://github.com/libbpf/libbpf/blob/master/src/bpf_tracing.h

// ...[생략]...

#define ___bpf_kprobe_args6(x, args...) ___bpf_kprobe_args5(args), (unsigned long long)PT_REGS_PARM6(ctx)
#define ___bpf_kprobe_args7(x, args...) ___bpf_kprobe_args6(args), (unsigned long long)PT_REGS_PARM7(ctx)
#define ___bpf_kprobe_args8(x, args...) ___bpf_kprobe_args7(args), (unsigned long long)PT_REGS_PARM8(ctx)

이렇게 6 ~ 8개의 인자를 처리할 수 있는 것처럼 나오는데, 이게 버전에 따라 지원을 한다는 것인지는 잘 모르겠습니다. 어쨌든, (특별한 방법이 있는 것인지는 알 수 없으나) 5개를 초과해서는 매개변수 정의가 안 됩니다. 반면, 아키텍처 구조를 잘 알고 있다면 추출하는 것이 가능한 것 같은데요, 이에 관해서는 다음의 글을 참고하세요. ^^

Extracting kprobe parameters in eBPF
; https://eyakubovich.github.io/2022-04-19-ebpf-kprobe-params/

결국 저런 방법까지 동원할 생각이 없다면 5개 이하로만 매개변수를 정의해야 합니다.




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

[연관 글]






[최초 등록일: ]
[최종 수정일: 11/19/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)
13838정성태12/4/2024633오류 유형: 935. Windbg - Breakpoint 0's offset expression evaluation failed.
13837정성태12/3/2024704디버깅 기술: 204. Windbg - 윈도우 핸들 테이블 (3) - Windows 10 이상인 경우
13836정성태12/3/2024926디버깅 기술: 203. Windbg - x64 가상 주소를 물리 주소로 변환 (페이지 크기가 2MB인 경우)
13835정성태12/2/2024985오류 유형: 934. Azure - rm: cannot remove '...': Directory not empty
13834정성태11/29/20241073Windows: 275. C# - CUI 애플리케이션과 Console 윈도우 (Windows 10 미만의 Classic Console 모드인 경우)파일 다운로드1
13833정성태11/29/20241077개발 환경 구성: 737. Azure Web App에서 Scale-out으로 늘어난 리눅스 인스턴스에 SSH 접속하는 방법
13832정성태11/27/20241110Windows: 274. Windows 7부터 도입한 conhost.exe
13831정성태11/27/2024977Linux: 111. eBPF - BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_RINGBUF에 대한 다양한 용어들
13830정성태11/25/20241069개발 환경 구성: 736. 파이썬 웹 앱을 Azure App Service에 배포하기
13829정성태11/25/20241026스크립트: 67. 파이썬 - Windows 버전에서 함께 설치되는 py.exe
13828정성태11/25/20241047개발 환경 구성: 735. Azure - 압축 파일을 이용한 web app 배포 시 디렉터리 구분이 안 되는 문제파일 다운로드1
13827정성태11/25/20241117Windows: 273. Windows 환경의 파일 압축 방법 (tar, Compress-Archive)
13826정성태11/21/20241174닷넷: 2313. C# - (비밀번호 등의) Console로부터 입력받을 때 문자열 출력 숨기기(echo 끄기)파일 다운로드1
13825정성태11/21/20241144Linux: 110. eBPF / bpf2go - BPF_RINGBUF_OUTPUT / BPF_MAP_TYPE_RINGBUF 사용법
13824정성태11/20/20241083Linux: 109. eBPF / bpf2go - BPF_PERF_OUTPUT / BPF_MAP_TYPE_PERF_EVENT_ARRAY 사용법
13823정성태11/20/20241084개발 환경 구성: 734. Ubuntu에 docker, kubernetes (k3s) 설치
13822정성태11/20/20241044개발 환경 구성: 733. Windbg - VirtualBox VM의 커널 디버거 연결 시 COM 포트가 없는 경우
13821정성태11/18/20241170Linux: 108. Linux와 Windows의 프로세스/스레드 ID 관리 방식
13820정성태11/18/20241127VS.NET IDE: 195. Visual C++ - C# 프로젝트처럼 CopyToOutputDirectory 항목을 추가하는 방법
13819정성태11/15/20241122Linux: 107. eBPF - libbpf CO-RE의 CONFIG_DEBUG_INFO_BTF 빌드 여부에 대한 의존성
13818정성태11/15/20241215Windows: 272. Windows 11 24H2 - sudo 추가
13817정성태11/14/20241100Linux: 106. eBPF / bpf2go - (BPF_MAP_TYPE_HASH) Map을 이용한 전역 변수 구현
13816정성태11/14/20241157닷넷: 2312. C#, C++ - Windows / Linux 환경의 Thread Name 설정파일 다운로드1
13815정성태11/13/20241101Linux: 105. eBPF - bpf2go에서 전역 변수 설정 방법
13814정성태11/13/20241214닷넷: 2311. C# - Windows / Linux 환경에서 Native Thread ID 가져오기파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...