Microsoft MVP성태의 닷넷 이야기
Linux: 101. eBPF 함수의 인자를 다루는 방법 [링크 복사], [링크+제목 복사],
조회: 5380
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 31  32  33  34  35  36  37  38  39  40  41  42  43  [44]  45  ...
NoWriterDateCnt.TitleFile(s)
12835정성태9/7/202117273.NET Framework: 1116. C# 10 - (15) CallerArgumentExpression 특성 추가 [2]파일 다운로드1
12834정성태9/7/202115412오류 유형: 762. Visual Studio 2019 Build Tools - 'C:\Program' is not recognized as an internal or external command, operable program or batch file.
12833정성태9/6/202113526VC++: 150. Golang - TCP client/server echo 예제 코드파일 다운로드1
12832정성태9/6/202115537VC++: 149. Golang - 인터페이스 포인터가 의미 있을까요?
12831정성태9/6/202112715VC++: 148. Golang - 채널에 따른 다중 작업 처리파일 다운로드1
12830정성태9/6/202117042오류 유형: 761. Internet Explorer에서 파일 다운로드 시 "Your current security settings do not allow this file to be downloaded." 오류
12829정성태9/5/202118368.NET Framework: 1115. C# 10 - (14) 구조체 타입에 기본 생성자 정의 가능파일 다운로드1
12828정성태9/4/202115598.NET Framework: 1114. C# 10 - (13) 단일 파일 내에 적용되는 namespace 선언파일 다운로드1
12827정성태9/4/202115756스크립트: 27. 파이썬 - 웹 페이지 데이터 수집을 위한 scrapy Crawler 사용법 요약
12826정성태9/3/202119460.NET Framework: 1113. C# 10 - (12) 문자열 보간 성능 개선 [1]파일 다운로드1
12825정성태9/3/202115625개발 환경 구성: 603. GoLand - WSL 환경과 연동
12824정성태9/2/202124868오류 유형: 760. 파이썬 tensorflow - Dst tensor is not initialized. 오류 메시지
12823정성태9/2/202114155스크립트: 26. 파이썬 - PyCharm을 이용한 fork 디버그 방법
12822정성태9/1/202119212오류 유형: 759. 파이썬 tensorflow - ValueError: Shapes (...) and (...) are incompatible [2]
12821정성태9/1/202114617.NET Framework: 1112. C# - .NET 6부터 공개된 ISpanFormattable 사용법
12820정성태9/1/202115537VC++: 147. Golang - try/catch에 대응하는 panic/recover [1]파일 다운로드1
12819정성태8/31/202116006.NET Framework: 1111. C# - FormattableString 타입
12818정성태8/31/202113871Windows: 198. 윈도우 - 작업 관리자에서 (tensorflow 등으로 인한) GPU 연산 부하 보는 방법
12817정성태8/31/202117620스크립트: 25. 파이썬 - 윈도우 환경에서 directml을 이용한 tensorflow의 AMD GPU 사용 방법
12816정성태8/30/202123335스크립트: 24. 파이썬 - tensorflow 2.6 NVidia GPU 사용 방법 [2]
12815정성태8/30/202115737개발 환경 구성: 602. WSL 2 - docker-desktop-data, docker-desktop (%LOCALAPPDATA%\Docker\wsl\data\ext4.vhdx) 파일을 다른 디렉터리로 옮기는 방법
12814정성태8/30/202120159.NET Framework: 1110. C# 11 - 인터페이스 내에 정적 추상 메서드 정의 가능 (DIM for Static Members) [2]파일 다운로드1
12813정성태8/29/202117245.NET Framework: 1109. C# 10 - (11) Lambda 개선파일 다운로드1
12812정성태8/28/202116573.NET Framework: 1108. C# 10 - (10) 개선된 #line 지시자
12811정성태8/27/202116449Linux: 44. 윈도우 개발자를 위한 리눅스 fork 동작 방식 설명 (파이썬 코드)
12810정성태8/27/202115450.NET Framework: 1107. .NET Core/5+에서 동적 컴파일한 C# 코드를 (Breakpoint도 활용하며) 디버깅하는 방법 - #line 지시자파일 다운로드1
... 31  32  33  34  35  36  37  38  39  40  41  42  43  [44]  45  ...