디버깅과 테스트

디버깅과 테스트 (Debugging and Testing)

이 문서는 BPF 프로그램과 맵을 디버깅하고 테스트하는 방법을 다뤄요. bpftool을 사용한 검사, 커널 BPF selftest, JIT 디버깅, perf를 통한 프로파일링, 트레이스포인트 관찰, 추적 파이프 사용법을 소개합니다.

출처: Debugging and Testing

본문

bpftool

bpftool은 BPF 주변의 주요 인트로스펙션·디버깅 도구이며, 리눅스 커널 트리와 함께 tools/bpf/bpftool/ 아래에서 개발·제공돼요.

이 도구는 시스템에 현재 로드된 모든 BPF 프로그램과 맵을 덤프하거나, 특정 프로그램이 사용하는 모든 BPF 맵을 나열·연관지을 수 있어요. 또한 전체 맵의 키/값 쌍을 덤프하거나, 개별 항목을 lookup·update·delete하거나 맵에서 키의 이웃 키를 가져올 수도 있어요. 이러한 연산은 BPF 프로그램이나 맵 ID를 기반으로, 또는 BPF 파일 시스템에 고정된 프로그램·맵 위치를 지정해 수행할 수 있어요. 이 도구는 맵이나 프로그램을 BPF 파일 시스템에 고정(pin)하는 옵션도 제공해요.

호스트에 현재 로드된 모든 BPF 프로그램의 빠른 개요를 보려면 다음 명령을 실행하세요:

# bpftool prog
398: sched_cls  tag 56207908be8ad877
   loaded_at Apr 09/16:24  uid 0
   xlated 8800B  jited 6184B  memlock 12288B  map_ids 18,5,17,14
399: sched_cls  tag abc95fb4835a6ec9
   loaded_at Apr 09/16:24  uid 0
   xlated 344B  jited 223B  memlock 4096B  map_ids 18
400: sched_cls  tag afd2e542b30ff3ec
   loaded_at Apr 09/16:24  uid 0
   xlated 1720B  jited 1001B  memlock 4096B  map_ids 17
401: sched_cls  tag 2dbbd74ee5d51cc8
   loaded_at Apr 09/16:24  uid 0
   xlated 3728B  jited 2099B  memlock 4096B  map_ids 17
[...]

마찬가지로 모든 활성 맵의 개요를 얻으려면:

# bpftool map
5: hash  flags 0x0
    key 20B  value 112B  max_entries 65535  memlock 13111296B
6: hash  flags 0x0
    key 20B  value 20B  max_entries 65536  memlock 7344128B
7: hash  flags 0x0
    key 10B  value 16B  max_entries 8192  memlock 790528B
8: hash  flags 0x0
    key 22B  value 28B  max_entries 8192  memlock 987136B
9: hash  flags 0x0
    key 20B  value 8B  max_entries 512000  memlock 49352704B
[...]

각 명령에 대해 bpftool은 명령줄 끝에 --json을 추가해 JSON 기반 출력도 지원해요. 추가 --pretty는 출력을 더 읽기 쉽게 개선해요.

# bpftool prog --json --pretty

특정 BPF 프로그램의 post-verifier BPF 명령어 이미지를 덤프하려면, 예를 들어 tc ingress 훅에 부착된 특정 프로그램을 검사하는 것부터 시작할 수 있어요:

# tc filter show dev cilium_host egress
filter protocol all pref 1 bpf chain 0
filter protocol all pref 1 bpf chain 0 handle 0x1 bpf_host.o:[from-netdev] \
                    direct-action not_in_hw id 406 tag e0362f5bd9163a0a jited

오브젝트 파일 bpf_host.o의 from-netdev 섹션에 있는 프로그램은 id 406에 명시된 대로 BPF 프로그램 ID가 406이에요. 이 정보를 바탕으로 bpftool은 프로그램 특정의 고급 메타데이터를 제공할 수 있어요:

# bpftool prog show id 406
406: sched_cls  tag e0362f5bd9163a0a
     loaded_at Apr 09/16:24  uid 0
     xlated 11144B  jited 7721B  memlock 12288B  map_ids 18,20,8,5,6,14

ID 406 프로그램은 유형 sched_cls(BPF_PROG_TYPE_SCHED_CLS)이고 e0362f5bd9163a0a의 tag(명령어 시퀀스에 대한 SHA 합)를 가지며, Apr 09/16:24에 루트 uid 0에 의해 로드됐어요. BPF 명령어 시퀀스는 11,144 bytes 길이이고 JIT된 이미지는 7,721 bytes예요. 프로그램 자체(맵 제외)는 사용자 uid 0에 부과되는(charge) 12,288 bytes를 소비해요. 그리고 BPF 프로그램은 ID가 18, 20, 8, 5, 6, 14인 BPF 맵을 사용해요. 후자의 ID로 맵 자체에 대한 정보를 얻거나 덤프할 수 있어요.

또한 bpftool은 프로그램이 실행하는 BPF 명령어의 덤프 요청을 내릴 수 있어요:

# bpftool prog dump xlated id 406
 0: (b7) r7 = 0
 1: (63) *(u32 *)(r1 +60) = r7
 2: (63) *(u32 *)(r1 +56) = r7
 3: (63) *(u32 *)(r1 +52) = r7
[...]
47: (bf) r4 = r10
48: (07) r4 += -40
49: (79) r6 = *(u64 *)(r10 -104)
50: (bf) r1 = r6
51: (18) r2 = map[id:18]                    <-- BPF map id 18
53: (b7) r5 = 32
54: (85) call bpf_skb_event_output#5656112  <-- BPF helper call
55: (69) r1 = *(u16 *)(r6 +192)
[...]

bpftool은 위와 같이 BPF 맵 ID를 명령어 스트림에 연관시키고, BPF 헬퍼나 다른 BPF 프로그램에 대한 호출도 연관시켜요.

명령어 덤프는 커널의 BPF verifier와 같은 'pretty-printer'를 재사용해요. 프로그램이 JIT됐고 따라서 위 xlated 명령어에서 생성된 실제 JIT 이미지가 실행되므로, 이를 bpftool로도 덤프할 수 있어요:

# bpftool prog dump jited id 406
 0:        push   %rbp
 1:        mov    %rsp,%rbp
 4:        sub    $0x228,%rsp
 b:        sub    $0x28,%rbp
 f:        mov    %rbx,0x0(%rbp)
13:        mov    %r13,0x8(%rbp)
17:        mov    %r14,0x10(%rbp)
1b:        mov    %r15,0x18(%rbp)
1f:        xor    %eax,%eax
21:        mov    %rax,0x20(%rbp)
25:        mov    0x80(%rdi),%r9d
[...]

주로 BPF JIT 개발자를 위해 역어셈블리를 실제 네이티브 opcode와 인터리브하는 옵션도 있어요:

# bpftool prog dump jited id 406 opcodes
 0:        push   %rbp
           55
 1:        mov    %rsp,%rbp
           48 89 e5
 4:        sub    $0x228,%rsp
           48 81 ec 28 02 00 00
 b:        sub    $0x28,%rbp
           48 83 ed 28
 f:        mov    %rbx,0x0(%rbp)
           48 89 5d 00
13:        mov    %r13,0x8(%rbp)
           4c 89 6d 08
17:        mov    %r14,0x10(%rbp)
           4c 89 75 10
1b:        mov    %r15,0x18(%rbp)
           4c 89 7d 18
[...]

같은 인터리브는 일반 BPF 명령어에서도 할 수 있으며, 커널에서 디버깅할 때 유용할 수 있어요:

# bpftool prog dump xlated id 406 opcodes
 0: (b7) r7 = 0
    b7 07 00 00 00 00 00 00
 1: (63) *(u32 *)(r1 +60) = r7
    63 71 3c 00 00 00 00 00
 2: (63) *(u32 *)(r1 +56) = r7
    63 71 38 00 00 00 00 00
 3: (63) *(u32 *)(r1 +52) = r7
    63 71 34 00 00 00 00 00
 4: (63) *(u32 *)(r1 +48) = r7
    63 71 30 00 00 00 00 00
 5: (63) *(u32 *)(r1 +64) = r7
    63 71 40 00 00 00 00 00
 [...]

프로그램의 기본 블록은 graphviz를 사용해 시각화할 수도 있어요. 이를 위해 bpftool은 나중에 png 파일로 변환할 수 있는 일반 BPF xlated 명령어 덤프 대신 dot 파일을 생성하는 visual 덤프 모드가 있어요:

# bpftool prog dump xlated id 406 visual &> output.dot
$ dot -Tpng output.dot -o output.png

또 다른 옵션은 dot 파일을 dotty에 뷰어로 전달하는 것인데, 즉 dotty output.dot이며, bpf_host.o 프로그램에 대한 결과는 다음과 같습니다(일부 발췌):

bpf_dot

xlated 명령어 덤프는 post-verifier BPF 명령어 이미지를 제공하는데, 즉 BPF 인터프리터로 실행되는 것처럼 명령어를 덤프한다는 뜻이에요. 커널에서 verifier는 BPF 로더가 제공한 원래 명령어에 대해 다양한 다시 쓰기(rewrite)를 수행해요.

다시 쓰기의 한 예는 런타임 성능을 개선하기 위한 헬퍼 함수의 인라인화인데, 여기서는 해시 테이블에 대한 맵 lookup의 경우예요:

# bpftool prog dump xlated id 3
 0: (b7) r1 = 2
 1: (63) *(u32 *)(r10 -4) = r1
 2: (bf) r2 = r10
 3: (07) r2 += -4
 4: (18) r1 = map[id:2]                      <-- BPF map id 2
 6: (85) call __htab_map_lookup_elem#77408   <-+ BPF helper inlined rewrite
 7: (15) if r0 == 0x0 goto pc+2                |
 8: (07) r0 += 56                              |
 9: (79) r0 = *(u64 *)(r0 +0)                <-+
10: (15) if r0 == 0x0 goto pc+24
11: (bf) r2 = r10
12: (07) r2 += -4
[...]

bpftool은 헬퍼 함수 호출이나 BPF to BPF 호출을 kallsyms를 통해 연관시켜요. 따라서 JIT된 BPF 프로그램이 kallsyms에 노출되고(bpf_jit_kallsyms), kallsyms 주소가 난독화되지 않도록 해야 해요(그렇지 않으면 호출이 call bpf_unspec#0으로 표시됨):

# echo 0 > /proc/sys/kernel/kptr_restrict
# echo 1 > /proc/sys/net/core/bpf_jit_kallsyms

BPF to BPF 호출도 인터프리터·JIT 양쪽 모두에서 연관돼요. 후자의 경우 서브프로그램의 태그가 호출 대상으로 표시돼요. 각 경우 pc+2는 호출 대상의 pc-상대 오프셋이며, 서브프로그램을 나타내요.

# bpftool prog dump xlated id 1
0: (85) call pc+2#__bpf_prog_run_args32
1: (b7) r0 = 1
2: (95) exit
3: (b7) r0 = 2
4: (95) exit

JIT된 덤프 변형:

# bpftool prog dump xlated id 1
0: (85) call pc+2#bpf_prog_3b185187f1855c4c_F
1: (b7) r0 = 1
2: (95) exit
3: (b7) r0 = 2
4: (95) exit

tail call의 경우 커널이 내부적으로 이를 단일 명령어로 매핑하는데, bpftool은 디버깅 편의를 위해 여전히 헬퍼 호출로 연관시켜요:

# bpftool prog dump xlated id 2
[...]
10: (b7) r2 = 8
11: (85) call bpf_trace_printk#-41312
12: (bf) r1 = r6
13: (18) r2 = map[id:1]
15: (b7) r3 = 0
16: (85) call bpf_tail_call#12
17: (b7) r1 = 42
18: (6b) *(u16 *)(r6 +46) = r1
19: (b7) r0 = 0
20: (95) exit

# bpftool map show id 1
1: prog_array  flags 0x0
      key 4B  value 4B  max_entries 1  memlock 4096B

전체 맵 덤프는 map dump 하위 명령으로 가능하며, 이는 존재하는 모든 맵 요소를 순회하며 키/값 쌍을 덤프해요.

주어진 맵에 BTF(BPFT Type Format) 데이터가 없으면 키/값 쌍이 hex로 덤프돼요:

# bpftool map dump id 5
key:
f0 0d 00 00 00 00 00 00  0a 66 00 00 00 00 8a d6
02 00 00 00
value:
00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
key:
0a 66 1c ee 00 00 00 00  00 00 00 00 00 00 00 00
01 00 00 00
value:
00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00
[...]
Found 6 elements

하지만 BTF가 있으면 맵은 키와 값 구조에 대한 디버깅 정보도 보유해요. 예를 들어 BTF를 BPF 맵 및 iproute2의 BPF_ANNOTATE_KV_PAIR() 매크로와 결합하면 다음 덤프가 나옵니다(커널 selftest의 test_xdp_noinline.o):

# cat tools/testing/selftests/bpf/test_xdp_noinline.c
  [...]
   struct ctl_value {
         union {
                 __u64 value;
                 __u32 ifindex;
                 __u8 mac[6];
         };
   };

   struct bpf_map_def __attribute__ ((section("maps"), used)) ctl_array = {
          .type            = BPF_MAP_TYPE_ARRAY,
          .key_size        = sizeof(__u32),
          .value_size      = sizeof(struct ctl_value),
          .max_entries     = 16,
          .map_flags       = 0,
   };
   BPF_ANNOTATE_KV_PAIR(ctl_array, __u32, struct ctl_value);

   [...]

BPF_ANNOTATE_KV_PAIR() 매크로는 빈 키와 값을 포함하는 맵 특정 ELF 섹션을 강제하는데, 이를 통해 iproute2 BPF 로더가 그 섹션과 BTF 데이터를 연관시켜 맵 로딩에 해당 유형을 BTF에서 선택할 수 있게 해요.

LLVM으로 컴파일하고 pahole로 디버깅 정보를 통해 BTF를 생성:

# clang [...] -O2 --target=bpf -g -emit-llvm -c test_xdp_noinline.c -o - |
  llc -march=bpf -mcpu=probe -mattr=dwarfris -filetype=obj -o test_xdp_noinline.o
# pahole -J test_xdp_noinline.o

이제 커널에 로드하고 bpftool로 맵을 덤프:

# ip -force link set dev lo xdp obj test_xdp_noinline.o sec xdp-test
# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 xdpgeneric/id:227 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
[...]
# bpftool prog show id 227
227: xdp  tag a85e060c275c5616  gpl
    loaded_at 2018-07-17T14:41:29+0000  uid 0
    xlated 8152B  not jited  memlock 12288B  map_ids 381,385,386,382,384,383
# bpftool map dump id 386
 [{
      "key": 0,
      "value": {
          "": {
              "value": 0,
              "ifindex": 0,
              "mac": []
          }
      }
  },{
      "key": 1,
      "value": {
          "": {
              "value": 0,
              "ifindex": 0,
              "mac": []
          }
      }
  },{
[...]

특정 키에 대한 맵의 lookup, update, delete, 'get next key' 연산도 bpftool로 수행할 수 있어요.

BPF 프로그램이 BTF 디버깅 정보와 함께 성공적으로 로드되면 prog show 명령 결과에 btf_id로 표시되는 BTF ID가 보여요.

# bpftool prog show id 72
72: xdp  name balancer_ingres  tag acf44cabb48385ed  gpl
   loaded_at 2020-04-13T23:12:08+0900  uid 0
   xlated 19104B  jited 10732B  memlock 20480B  map_ids 126,130,131,127,129,128
   btf_id 60

이것은 시스템에 로드된 모든 BTF 오브젝트를 덤프하는 btf show 명령으로도 확인할 수 있어요.

# bpftool btf show
60: size 12243B  prog_ids 72  map_ids 126,130,131,127,129,128

그리고 btf dump 하위 명령은 BTF에 포함된 디버깅 정보를 확인하는 데 사용할 수 있어요. 이 명령으로 BTF 덤프는 'raw' 또는 C 코드에서 쓰는 'c' 형식으로 지정할 수 있어요.

# bpftool btf dump id 60 format c
  [...]
   struct ctl_value {
         union {
                 __u64 value;
                 __u32 ifindex;
                 __u8 mac[6];
         };
   };

   typedef unsigned int u32;
   [...]

비디오: bpftool에 대해 더 배우려면 bpftool 메인테이너 Quentin Monnet와 함께하는 eCHO episode 11: Exploring bpftool을 확인하세요.

커널 테스트 (Kernel Testing)

리눅스 커널은 tools/testing/selftests/bpf/ 아래 커널 소스 트리에서 찾을 수 있는 BPF selftest 제품군을 제공해요.

$ cd tools/testing/selftests/bpf/
$ make
# make run_tests

테스트 제품군은 BPF verifier, 프로그램 태그, 다양한 BPF 맵 인터페이스·맵 유형에 대한 테스트 케이스를 포함해요. LLVM 백엔드를 확인하는 C 코드의 다양한 런타임 테스트와, 인터프리터와 JIT를 테스트하기 위해 커널에서 실행되는 eBPF·cBPF asm 코드를 포함해요.

JIT 디버깅 (JIT Debugging)

감사를 수행하거나 확장을 작성하는 JIT 개발자를 위해 각 컴파일 실행이 생성된 JIT 이미지를 커널 로그로 출력할 수 있어요:

# echo 2 > /proc/sys/net/core/bpf_jit_enable

새 BPF 프로그램이 로드될 때마다 JIT 컴파일러가 출력을 덤프하며, 이는 예를 들어 dmesg로 검사할 수 있어요:

[ 3389.935842] flen=6 proglen=70 pass=3 image=ffffffffa0069c8f from=tcpdump pid=20583
[ 3389.935847] JIT code: 00000000: 55 48 89 e5 48 83 ec 60 48 89 5d f8 44 8b 4f 68
[ 3389.935849] JIT code: 00000010: 44 2b 4f 6c 4c 8b 87 d8 00 00 00 be 0c 00 00 00
[ 3389.935850] JIT code: 00000020: e8 1d 94 ff e0 3d 00 08 00 00 75 16 be 17 00 00
[ 3389.935851] JIT code: 00000030: 00 e8 28 94 ff e0 83 f8 01 75 07 b8 ff ff 00 00
[ 3389.935852] JIT code: 00000040: eb 02 31 c0 c9 c3

flen은 BPF 프로그램 길이(여기서는 6개 BPF 명령어)이고, proglen은 JIT가 opcode 이미지에 생성한 바이트 수(여기서는 70바이트)를 알려줘요. pass는 이미지가 3개의 컴파일러 패스에서 생성됐음을 의미하며, 예를 들어 x86_64는 가능할 때 이미지 크기를 더 줄이기 위한 다양한 최적화 패스를 가질 수 있어요. image는 생성된 JIT 이미지의 주소를 포함하고, from과 pid는 컴파일 과정을 트리거한 사용자 공간 애플리케이션 이름과 PID를 각각 나타내요. eBPF와 cBPF JIT의 덤프 출력 형식은 같아요.

커널 트리의 tools/bpf/ 아래에 bpf_jit_disasm이라는 도구가 있어요. 이 도구는 최신 덤프를 읽고 추가 검사를 위해 역어셈블리를 출력해요:

# ./bpf_jit_disasm
70 bytes emitted from JIT compiler (pass:3, flen:6)
ffffffffa0069c8f + <x>:
   0:       push   %rbp
   1:       mov    %rsp,%rbp
   4:       sub    $0x60,%rsp
   8:       mov    %rbx,-0x8(%rbp)
   c:       mov    0x68(%rdi),%r9d
  10:       sub    0x6c(%rdi),%r9d
  14:       mov    0xd8(%rdi),%r8
  1b:       mov    $0xc,%esi
  20:       callq  0xffffffffe0ff9442
  25:       cmp    $0x800,%eax
  2a:       jne    0x0000000000000042
  2c:       mov    $0x17,%esi
  31:       callq  0xffffffffe0ff945e
  36:       cmp    $0x1,%eax
  39:       jne    0x0000000000000042
  3b:       mov    $0xffff,%eax
  40:       jmp    0x0000000000000044
  42:       xor    %eax,%eax
  44:       leaveq
  45:       retq

대안으로 이 도구는 역어셈블리와 함께 관련 opcode를 덤프할 수도 있어요.

# ./bpf_jit_disasm -o
70 bytes emitted from JIT compiler (pass:3, flen:6)
ffffffffa0069c8f + <x>:
   0:       push   %rbp
    55
   1:       mov    %rsp,%rbp
    48 89 e5
   4:       sub    $0x60,%rsp
    48 83 ec 60
   8:       mov    %rbx,-0x8(%rbp)
    48 89 5d f8
   c:       mov    0x68(%rdi),%r9d
    44 8b 4f 68
  10:       sub    0x6c(%rdi),%r9d
    44 2b 4f 6c
  14:       mov    0xd8(%rdi),%r8
    4c 8b 87 d8 00 00 00
  1b:       mov    $0xc,%esi
    be 0c 00 00 00
  20:       callq  0xffffffffe0ff9442
    e8 1d 94 ff e0
  25:       cmp    $0x800,%eax
    3d 00 08 00 00
  2a:       jne    0x0000000000000042
    75 16
  2c:       mov    $0x17,%esi
    be 17 00 00 00
  31:       callq  0xffffffffe0ff945e
    e8 28 94 ff e0
  36:       cmp    $0x1,%eax
    83 f8 01
  39:       jne    0x0000000000000042
    75 07
  3b:       mov    $0xffff,%eax
    b8 ff ff 00 00
  40:       jmp    0x0000000000000044
    eb 02
  42:       xor    %eax,%eax
    31 c0
  44:       leaveq
    c9
  45:       retq
    c3

더 최근에는 bpftool이 시스템에 이미 로드된 주어진 BPF 프로그램 ID를 기반으로 BPF JIT 이미지를 덤프하는 같은 기능을 채택했어요(bpftool 섹션 참고).

JIT된 BPF 프로그램의 성능 분석에는 평소처럼 perf를 사용할 수 있어요. 전제 조건으로 JIT된 프로그램이 kallsyms 인프라를 통해 내보내져야 해요.

# echo 1 > /proc/sys/net/core/bpf_jit_enable
# echo 1 > /proc/sys/net/core/bpf_jit_kallsyms

bpf_jit_kallsyms를 활성화/비활성화해도 관련 BPF 프로그램 재로드는 필요 없어요. 다음으로 BPF 프로그램 프로파일링을 위한 작은 워크플로 예시를 제공해요. 데모 목적으로 정교한 tc BPF 프로그램이 사용되며, perf는 bpf_clone_redirect() 헬퍼 내부의 실패한 할당을 기록해요. 직접 쓰기(direct write) 사용으로 인해 bpf_try_make_head_writable()이 실패했고, 이어서 복제된 skb를 다시 해제하고 오류 메시지와 함께 반환돼요. 따라서 perf는 모든 kfree_skb 이벤트를 기록해요.

# tc qdisc add dev em1 clsact
# tc filter add dev em1 ingress bpf da obj prog.o sec main
# tc filter show dev em1 ingress
filter protocol all pref 49152 bpf
filter protocol all pref 49152 bpf handle 0x1 prog.o:[main] direct-action id 1 tag 8227addf251b7543

# cat /proc/kallsyms
[...]
ffffffffc00349e0 t fjes_hw_init_command_registers    [fjes]
ffffffffc003e2e0 d __tracepoint_fjes_hw_stop_debug_err    [fjes]
ffffffffc0036190 t fjes_hw_epbuf_tx_pkt_send    [fjes]
ffffffffc004b000 t bpf_prog_8227addf251b7543

# perf record -a -g -e skb:kfree_skb sleep 60
# perf script --kallsyms=/proc/kallsyms
[...]
ksoftirqd/0     6 [000]  1004.578402:    skb:kfree_skb: skbaddr=0xffff9d4161f20a00 protocol=2048 location=0xffffffffc004b52c
   7fffb8745961 bpf_clone_redirect (/lib/modules/4.10.0+/build/vmlinux)
   7fffc004e52c bpf_prog_8227addf251b7543 (/lib/modules/4.10.0+/build/vmlinux)
   7fffc05b6283 cls_bpf_classify (/lib/modules/4.10.0+/build/vmlinux)
   7fffb875957a tc_classify (/lib/modules/4.10.0+/build/vmlinux)
   7fffb8729840 __netif_receive_skb_core (/lib/modules/4.10.0+/build/vmlinux)
   7fffb8729e38 __netif_receive_skb (/lib/modules/4.10.0+/build/vmlinux)
   7fffb872ae05 process_backlog (/lib/modules/4.10.0+/build/vmlinux)
   7fffb872a43e net_rx_action (/lib/modules/4.10.0+/build/vmlinux)
   7fffb886176c __do_softirq (/lib/modules/4.10.0+/build/vmlinux)
   7fffb80ac5b9 run_ksoftirqd (/lib/modules/4.10.0+/build/vmlinux)
   7fffb80ca7fa smpboot_thread_fn (/lib/modules/4.10.0+/build/vmlinux)
   7fffb80c6831 kthread (/lib/modules/4.10.0+/build/vmlinux)
   7fffb885e09c ret_from_fork (/lib/modules/4.10.0+/build/vmlinux)

perf가 기록한 스택 트레이스는 호출 트레이스의 일부로 bpf_prog_8227addf251b7543() 심볼을 보여주는데, 이는 태그 8227addf251b7543의 BPF 프로그램이 kfree_skb 이벤트와 관련됐고 tc가 보여주듯 ingress 훅의 netdevice em1에 부착됐음을 의미해요.

인트로스펙션 (Introspection)

리눅스 커널은 BPF와 XDP 주변에 다양한 트레이스포인트를 제공하며, 예를 들어 사용자 공간 프로그램과 bpf 시스템 콜의 상호작용을 추적하는 추가 인트로스펙션에 사용할 수 있어요.

BPF용 트레이스포인트:

# perf list | grep bpf:
bpf:bpf_map_create                                 [Tracepoint event]
bpf:bpf_map_delete_elem                            [Tracepoint event]
bpf:bpf_map_lookup_elem                            [Tracepoint event]
bpf:bpf_map_next_key                               [Tracepoint event]
bpf:bpf_map_update_elem                            [Tracepoint event]
bpf:bpf_obj_get_map                                [Tracepoint event]
bpf:bpf_obj_get_prog                               [Tracepoint event]
bpf:bpf_obj_pin_map                                [Tracepoint event]
bpf:bpf_obj_pin_prog                               [Tracepoint event]
bpf:bpf_prog_get_type                              [Tracepoint event]
bpf:bpf_prog_load                                  [Tracepoint event]
bpf:bpf_prog_put_rcu                               [Tracepoint event]

perf와 함께 사용하는 예(여기 사용된 sleep 예 대신 물론 tc 같은 특정 애플리케이션을 사용해도 됨):

# perf record -a -e bpf:* sleep 10
# perf script
sock_example  6197 [005]   283.980322:      bpf:bpf_map_create: map type=ARRAY ufd=4 key=4 val=8 max=256 flags=0
sock_example  6197 [005]   283.980721:       bpf:bpf_prog_load: prog=a5ea8fa30ea6849c type=SOCKET_FILTER ufd=5
sock_example  6197 [005]   283.988423:   bpf:bpf_prog_get_type: prog=a5ea8fa30ea6849c type=SOCKET_FILTER
sock_example  6197 [005]   283.988443: bpf:bpf_map_lookup_elem: map type=ARRAY ufd=4 key=[06 00 00 00] val=[00 00 00 00 00 00 00 00]
[...]
sock_example  6197 [005]   288.990868: bpf:bpf_map_lookup_elem: map type=ARRAY ufd=4 key=[01 00 00 00] val=[14 00 00 00 00 00 00 00]
     swapper     0 [005]   289.338243:    bpf:bpf_prog_put_rcu: prog=a5ea8fa30ea6849c type=SOCKET_FILTER

BPF 프로그램의 경우 개별 프로그램 태그가 표시돼요.

디버깅을 위해 XDP는 예외가 발생할 때 트리거되는 트레이스포인트도 있어요:

# perf list | grep xdp:
xdp:xdp_exception                                  [Tracepoint event]

예외는 다음 시나리오에서 트리거돼요:

  • BPF 프로그램이 잘못된/알 수 없는 XDP 액션 코드를 반환한 경우.
  • BPF 프로그램이 비우아한 종료를 나타내는 XDP_ABORTED로 반환한 경우.
  • BPF 프로그램이 XDP_TX로 반환했지만 전송 시 오류가 있는 경우. 예를 들어 포트가 up이 아니거나, 전송 링이 가득 차거나, 할당 실패 등으로. 두 트레이스포인트 클래스 모두 하나 이상의 트레이스포인트에 부착된 BPF 프로그램 자체로 검사할 수 있으며, 맵에서 추가 정보를 수집하거나 예를 들어 bpf_perf_event_output() 헬퍼를 통해 그러한 이벤트를 사용자 공간 수집기로 보낼 수 있어요.

추적 파이프 (Tracing pipe)

BPF 프로그램이 bpf_trace_printk()를 호출하면 출력이 커널 추적 파이프로 전송돼요. 사용자는 이 파일에서 이 버퍼로 추적되는 이벤트를 소비할 수 있어요:

# tail -f /sys/kernel/debug/tracing/trace_pipe
...

기타 (Miscellaneous)

BPF 프로그램과 맵은 perf와 유사하게 RLIMIT_MEMLOCK에 대해 메모리 계정이 부과돼요. 메모리에 잠글 수 있는 시스템 페이지 단위의 현재 사용 가능한 크기는 ulimit -l로 확인할 수 있어요. setrlimit 시스템 콜 매뉴얼 페이지가 추가 세부 정보를 제공해요.

기본 제한은 더 복잡한 프로그램이나 더 큰 BPF 맵을 로드하기에 보통 불충분해서, BPF 시스템 콜이 EPERM의 errno로 반환될 수 있어요. 그러한 상황에서는 ulimit -l unlimited 또는 충분히 큰 제한으로 해결할 수 있어요. RLIMIT_MEMLOCK은 주로 비특권 사용자에 대한 제한을 강제해요. 설정에 따라 특권 사용자에 대해 더 높은 제한을 설정하는 것이 종종 허용돼요.

더 알아보기 (Learn more)