【原創】Kernel調試追蹤技術之 eBPF on ARM64


【原創】Kernel調試追蹤技術之 eBPF on ARM64

本文目標:

  1. 理解eBPF的核心概念和實現方法
  2. 探索ARM64 Linux上eBPF的使用

1. eBPF是什么?

eBPF是一種不需要修改kernel代碼,不需要加載內核模塊,就可以擴展內核功能的技術。可用於網絡包過濾,kernel行為監控和觀測,安全檢查等。

1.1 BPF and tcpdump

BPF (Berkeley Packet Filter) 是1992年就有的技術,最初用在unix上,用於網絡包過濾。

BPF的實現原理是把包過濾命令翻譯成字節碼(bytecode),在kernel中轉譯並執行這些字節碼。通過在Kernel各處加入鈎子函數,實現網絡數據包的捕獲的過濾,例如tcpdump就是基於bpf實現的。

classic BPF Overview http://www.tcpdump.org/papers/bpf-usenix93.pdf

  • tcpdump 的-d參數可以dump出執行的字節碼

    $ sudo tcpdump -i enp0s10  port 22 -d
    [sudo] password for yu: 
    (000) ldh      [12]
    (001) jeq      #0x86dd          jt 2    jf 10
    (002) ldb      [20]
    (003) jeq      #0x84            jt 6    jf 4
    (004) jeq      #0x6             jt 6    jf 5
    (005) jeq      #0x11            jt 6    jf 23
    (006) ldh      [54]
    (007) jeq      #0x16            jt 22   jf 8
    (008) ldh      [56]
    (009) jeq      #0x16            jt 22   jf 23
    (010) jeq      #0x800           jt 11   jf 23
    (011) ldb      [23]
    (012) jeq      #0x84            jt 15   jf 13
    (013) jeq      #0x6             jt 15   jf 14
    (014) jeq      #0x11            jt 15   jf 23
    (015) ldh      [20]
    (016) jset     #0x1fff          jt 23   jf 17
    (017) ldxb     4*([14]&0xf)
    (018) ldh      [x + 14]
    (019) jeq      #0x16            jt 22   jf 20
    (020) ldh      [x + 16]
    (021) jeq      #0x16            jt 22   jf 23
    (022) ret      #262144
    (023) ret      #0
    

1.2 eBPF

2011年左右, Plumgrid公司的Alexei Starovoitov 等人覺得經典BPF(也叫cBPF)有些過時,比如其字節碼指令只使用2個32位寄存器,功能和效率都受限,開始對BPF做出大幅修改,新版命名為extended BPF,並且使其可以用於網絡(Networking)以外的領域,比如跟蹤(Tracing)和安全等,使得BPF向通用化技術演進。

eBPF對經典BPF(cBPF)做了兼容,所以現在說的BPF通指eBPF。

eBPF對cBPF做了如下優化:

  • 字節碼寄存器從32位變為64位
  • 寄存器數從2個增加到10個,可以實現主流處理器的ABI的直接映射
  • JIT支持,預先翻譯成匯編指令,執行速度大增
  • 實現了bpf()系統調用

下圖為eBPF的框架結構圖:

下面對BPF的核心概念做簡要介紹。

Verifier

BPF字節碼程序傳入Kernel后,首先要進行嚴格的檢驗,由Verifier(檢驗器)模塊完成。

  1. 不能包含循環(Direct Acyclic),如果是小循環會被展開
  2. 檢查越界指令
  3. 檢查所有分支指令,並且仿真運行
  4. 寄存器必須先初始化再使用,
  5. 不允許指針計算
  6. 禁止隨機內存訪問

只有通過verifier驗證后的程序才能被運行。

Map

BPF中定義各種map數據結構存放數據,用於kernel和用戶層的數據交換,比如傳遞數據包、Trace 數據等。

include/uapi/linux/bpf.h
 98 enum bpf_map_type {                
 99     BPF_MAP_TYPE_UNSPEC,           
100     BPF_MAP_TYPE_HASH,       // 哈希        
101     BPF_MAP_TYPE_ARRAY,      // 數組
102     BPF_MAP_TYPE_PROG_ARRAY, // 程序數組
103     BPF_MAP_TYPE_PERF_EVENT_ARRAY,  // PERF EVENT數組
104     BPF_MAP_TYPE_PERCPU_HASH,       // PERCPU 哈希
105     BPF_MAP_TYPE_PERCPU_ARRAY,      // PERCPU 數組
106     BPF_MAP_TYPE_STACK_TRACE,       // 棧
107     BPF_MAP_TYPE_CGROUP_ARRAY,      // Cgroup數組
108     BPF_MAP_TYPE_LRU_HASH,          // LRU 哈希
109     BPF_MAP_TYPE_LRU_PERCPU_HASH,   
110     BPF_MAP_TYPE_LPM_TRIE,          // Longest-pre match
111     BPF_MAP_TYPE_ARRAY_OF_MAPS,     // map的數組
112     BPF_MAP_TYPE_HASH_OF_MAPS,      // map的哈希
113     BPF_MAP_TYPE_DEVMAP,           
114     BPF_MAP_TYPE_SOCKMAP,          
115 };

這些map在編寫BPF kernel程序時定義,在BPF user程序中通過bpf() 系統調用讀到用戶空間。

bpf() syscall

eBPF應用程序通過唯一的系統調用接口與kernel通信。

SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)

  • cmd 參數指定執行的操作

    enum bpf_cmd {             
        BPF_MAP_CREATE,          // 創建一個map
        BPF_MAP_LOOKUP_ELEM,     // 查詢一個map 元素
        BPF_MAP_UPDATE_ELEM,     // 更新一個map 元素
        BPF_MAP_DELETE_ELEM,     // 刪除一個map 元素
        BPF_MAP_GET_NEXT_KEY,    // 獲取下一個map的key值
        BPF_PROG_LOAD,           // 加載bpf程序
        BPF_OBJ_PIN,             // pin bpf program和map 組成的對象,防止進程退出后刪除
        BPF_OBJ_GET,             // 獲取object
        BPF_PROG_ATTACH,         // attach bpf 程序到cgroup
        BPF_PROG_DETACH,       
        BPF_PROG_TEST_RUN,       // 測試性執行一次
        BPF_PROG_GET_NEXT_ID,    // 獲取下一個prog ID
        BPF_MAP_GET_NEXT_ID,     // 獲取下一個map ID
        BPF_PROG_GET_FD_BY_ID,   // 通過ID獲取prog的fd
        BPF_MAP_GET_FD_BY_ID,    // 通過ID獲取map的fd
        BPF_OBJ_GET_INFO_BY_FD,  // 通過fd獲取obj的信息
    };
    
  • uattr 是針對各個CMD傳入的參數,比如map信息,program信息等,size是uattr的大小。

JIT (Just-in-time Compilation)

JIT是在運行時把Bytecode翻譯成Machine Code(機器碼)的技術,JAVA就是通過JIT技術使得性能大幅提升。因為Bytecode是一種虛擬的指令集,不能直接運行在CPU上,如果不支持JIT,bytecode要被通過C語言仿真執行,效率很低。目前Kernel支持的主流處理器架構都支持JIT,比如X86,ARM64, Powerpc。

BPF程序通過verifier后會被通過JIT翻譯成ASM,構造成一個函數,注冊到struct bpf_progbpf_func 接口上,這樣bytecode的執行效率和原生代碼就幾乎沒差別了。

Clang 和LLVM

因為BPF使用自身特定的Bytecode,需要把C代碼編譯成bytecode,LLVM實現了BPF后端支持,所以目前主要使用Clang+LLVM的方法。GCC 需要GCC10版本才支持

Clang是LLVM的C語言前端,負責把C編譯成LLVM的中間語言IR,再由LLVM的bpf后端編譯成bytecode。

詳細比較可參看 https://stackoverflow.com/questions/24836183/understanding-g-vs-clang-vs-llvm

eBPF工具和項目

2014年Alexei去Netflix拜訪性能分析的大神Brendan Gregg,介紹了eBPF,Brendan大贊,認為eBPF可以更方便地實現很多性能分析工具,因為類似虛擬機運行,可以對執行代碼進行驗證(Verify),安全性高,可以方便用於生產系統。Brendan把他之前開發的很多工具用eBPF改寫,且擴充了許多工具,促進了eBPF的大發展和普及。

並且寫了一本書《BPF Performance Tool - Linux System and Application Observation》。

http://www.brendangregg.com/blog/2019-07-15/bpf-performance-tools-book.html

為了方便eBPF應用開發,eBPF開發者開發了BCC (BPF Compiler Collection),包含BPF的輔助庫和對Python、Lua等前端程序的支持,降低了編程難度。 BCC 放在ioviso (https://github.com/iovisor)項目中,除此之外還有bpftrace、ply等項目。

eBPF被稱為目前Linux上新的supper power,因為可以在產品系統中使用,可以基於eBPF在應用層實現許多網絡應用程序,例如:

  • Cilium & Hubble 基於eBPF的網絡管理、監控軟件
    • Cilium可以算是eBPF的官方項目,由eBPF主要設計者開發,是cilium eBPF的詳細設計和使用文檔。
  • Sysdig 系統行為可視化工具
    • 最初由Kernel module實現,后基於eBPF實現

eBPF對於Kernel,就像Javascript對於HTML一樣 [BPF-Rethink the Linux kernel]。

1.3 eBPF和Ftrace的區別

eBPF和tracing是不同的技術。

tracing往往會產生大量數據,需要做篩選,甚至編寫解析程序實現可視化和統計。

eBPF對內核事件、數據的獲取更有針對性,方便編成實現精細控制。

ftrace的優勢在於大多kernel原生支持,方便使用;

eBPF優勢在於擴展性極強。

2. eBPF的實現

2.1 代碼框圖

eBPF的相關代碼可以分成三個部分

  1. Kernel代碼
    • 包括bpf核心實現
    • 處理器相關的JIT支持
    • 各個子系統對bpf的支持
  2. Kernel代碼樹的用戶層代碼
    • tools/lib/bpf中的輔助函數庫libbpf.c
    • samples/bpf/中的例子程序和測試程序
  3. iovisor項目中的BCC、bpftrace、ply等BPF開發框架和工具的代碼
    • 包括bpf的C庫和python、Lua、CC等前端支持,以及應用程序

下圖為eBPF源碼結構圖

2.2 bpf程序加載

用戶通過Clang+LLVM編譯好的bpf bytecode 通過bpf() syscall的BPF_PROG_LOAD命令傳入kernel。

進入kernel后進行license檢查、verify、JIT、創建文件描述符,最后得到一個有符號名的bpf 函數,

# echo 1 > /proc/sys/net/core/bpf_jit_kallsyms
# cat /proc/kallsyms | grep -E "bpf_prog_.+_sys_[enter|exit]"
 ffff000000086a84 t bpf_prog_f173133dc38ccf87_sys_enter [bpf]
 ffff000000088618 t bpf_prog_c1bd85c092d6e4aa_sys_exit [bpf]

2.3 eBPF core, ARM64 JIT

Bytecode到ARM執行的轉換,需要先定義eBPF和ARM寄存器和指令的映射關系,代碼如下:

//寄存器映射
41 /* Map BPF registers to A64 registers */                                
42 static const int bpf2a64[] = {                                          
43     /* return value from in-kernel function, and exit value from eBPF */
44     [BPF_REG_0] = A64_R(7),                                             
45     /* arguments from eBPF program to in-kernel function */             
46     [BPF_REG_1] = A64_R(0),                                             
47     [BPF_REG_2] = A64_R(1),                                             
48     [BPF_REG_3] = A64_R(2),                                             
49     [BPF_REG_4] = A64_R(3),                                             
50     [BPF_REG_5] = A64_R(4),                                             
51     /* callee saved registers that in-kernel function will preserve */  
52     [BPF_REG_6] = A64_R(19),                                            
53     [BPF_REG_7] = A64_R(20),                                            
54     [BPF_REG_8] = A64_R(21),                                            
55     [BPF_REG_9] = A64_R(22),                                            
56     /* read-only frame pointer to access stack */                       
57     [BPF_REG_FP] = A64_R(25),                                           
58     /* temporary registers for internal BPF JIT */                      
59     [TMP_REG_1] = A64_R(10),                                            
60     [TMP_REG_2] = A64_R(11),                                            
61     [TMP_REG_3] = A64_R(12),                                            
62     /* tail_call_cnt */                                                 
63     [TCALL_CNT] = A64_R(26),                                            
64     /* temporary register for blinding constants */                     
65     [BPF_REG_AX] = A64_R(9),                                            
66 };

// 指令映射
320 static int build_insn(const struct bpf_insn *insn, struct jit_ctx *ctx) 
321 {                                                                       
...                            
346                                                                         
347     switch (code) {                                                     
348     /* dst = src */                                                     
349     case BPF_ALU | BPF_MOV | BPF_X:                                     
350     case BPF_ALU64 | BPF_MOV | BPF_X:                                   
351         emit(A64_MOV(is64, dst, src), ctx);                             
352         break;                                                          
353     /* dst = dst OP src */                                              
354     case BPF_ALU | BPF_ADD | BPF_X:                                     
355     case BPF_ALU64 | BPF_ADD | BPF_X:                                   
356         emit(A64_ADD(is64, dst, dst, src), ctx);                        
357         break;                                                          
..          
539     case BPF_JMP | BPF_JSGE | BPF_X:                 
540     case BPF_JMP | BPF_JSLE | BPF_X:                 
541         emit(A64_CMP(1, dst, src), ctx);             
542 emit_cond_jmp:                                       
543         jmp_offset = bpf2a64_offset(i + off, i, ctx);
544         check_imm19(jmp_offset);                     
545         switch (BPF_OP(code)) {                      
546         case BPF_JEQ:                                
547             jmp_cond = A64_COND_EQ;                  
548             break;                                   
....

下圖為Bytecode轉換為ARM64指令的函數調用過程:

2.4 Verifier

Verifier是一個靜態代碼分析器,用來分析傳入Kernel 的BPF程序。

eBPF程序是從在用戶空間傳入的,必須進行嚴格限制,防止其導致kernel 崩潰或性能下降,所以Verifier定義了嚴格的審查規則。

Verifier對用戶是透明的,但是理解Verifier,對於理解eBPF非常有幫助,ksrc/kernel/bpf/verifier.c 開頭的一段注釋解釋了verifier的核心驗證規則,要點如下圖:

驗證主要分成兩個階段。

第一個階段檢查函數的深度和指令數,使用深度優先遍歷,並且確保沒有循環(DAG:有向無環圖),用壓棧出棧的方式實現。

第二個階段檢查每條bytecode指令,根據指令的分類(class),檢查其操作寄存器的讀寫屬性、內存訪問是否越界、BPF_CALL是否符合接口協議等。

下圖為指令檢查過程:

BCC中runqlen工具的轉譯實例:

src/iovisor/bcc-master/libbpf-tools/bin $ ./bpftool prog show
...
59: cgroup_skb  tag 2a142ef67aaad174  gpl
        loaded_at 2020-09-02T08:23:41+0800  uid 0
        xlated 296B  jited 200B  memlock 4096B  map_ids 58,59
73: perf_event  name do_perf_event  tag 168815884b13b0c4  gpl
        loaded_at 2020-09-02T09:33:25+0800  uid 0
        xlated 360B  jited 210B  memlock 4096B  map_ids 67

$ sudo bcc.runqlen

C code

int do_perf_event()
{
    unsigned int len = 0;
    pid_t pid = 0;
    struct task_struct *task = NULL;
    struct cfs_rq_partial *my_q = NULL;

    task = (struct task_struct *)bpf_get_current_task();
    my_q = (struct cfs_rq_partial *)task->se.cfs_rq;
    len = my_q->nr_running;

    if (len > 0)
        len--;

    STORE

    return 0;
}

Tranlated Bytecode

$ sudo ./bpftool prog dump xlated tag 168815884b13b0c4
   0: (85) call bpf_get_current_task#-59136
   1: (b7) r6 = 0
   2: (7b) *(u64 *)(r10 -8) = r6
   3: (07) r0 += 472
   4: (bf) r1 = r10
   5: (07) r1 += -8
   6: (b7) r2 = 8
   7: (bf) r3 = r0
   8: (85) call bpf_probe_read#-56224
   9: (79) r3 = *(u64 *)(r10 -8)
  10: (63) *(u32 *)(r10 -8) = r6
...
  40: (07) r3 += -8
  41: (b7) r4 = 1
  42: (85) call htab_map_update_elem#109984
  43: (b7) r0 = 0
 **** 44: (95) exit

2.5 BPF_CALL

BPF程序只能調用通過BPF_CALL聲明的kernel接口,這樣可以保障安全和性能。

這些接口主要是在網絡、trace和bpf核心代碼中實現的的一些幫助函數。

bpf程序通過這些接口和kernel各個模塊進行交互。

Kerne4.14.74中有78個,kernel5.8.0中有170個

# Helper
./kernel/bpf/helpers.c:31:	BPF_CALL_2(bpf_map_lookup_elem, struct bpf_map *, map, void *, key)
./kernel/bpf/helpers.c:46:	BPF_CALL_4(bpf_map_update_elem, struct bpf_map *, map, void *, key,
./kernel/bpf/helpers.c:64:	BPF_CALL_2(bpf_map_delete_elem, struct bpf_map *, map, void *, key)
./kernel/bpf/helpers.c:85:	BPF_CALL_0(bpf_get_smp_processor_id)
./kernel/bpf/helpers.c:96:	BPF_CALL_0(bpf_get_numa_node_id)
./kernel/bpf/helpers.c:107:	BPF_CALL_0(bpf_ktime_get_ns)
./kernel/bpf/helpers.c:119:	BPF_CALL_0(bpf_get_current_pid_tgid)
./kernel/bpf/helpers.c:135:	BPF_CALL_0(bpf_get_current_uid_gid)
./kernel/bpf/helpers.c:155:	BPF_CALL_2(bpf_get_current_comm, char *, buf, u32, size)
./kernel/bpf/core.c:1423:	BPF_CALL_0(bpf_user_rnd_u32)
./kernel/bpf/stackmap.c:118:BPF_CALL_3(bpf_get_stackid, struct pt_regs *, regs, struct bpf_map *, map,
./kernel/bpf/sockmap.c:901:	BPF_CALL_4(bpf_sock_map_update, struct bpf_sock_ops_kern *, bpf_sock,

# Trace
./kernel/trace/bpf_trace.c:64:	BPF_CALL_3(bpf_probe_read, void *, dst, u32, size, const void *, unsafe_ptr)
./kernel/trace/bpf_trace.c:84:	BPF_CALL_3(bpf_probe_write_user, void *, unsafe_ptr, const void *, src,
./kernel/trace/bpf_trace.c:128:	BPF_CALL_5(bpf_trace_printk, char *, fmt, u32, fmt_size, u64, arg1,
./kernel/trace/bpf_trace.c:258:	BPF_CALL_2(bpf_perf_event_read, struct bpf_map *, map, u64, flags)
./kernel/trace/bpf_trace.c:329:	BPF_CALL_5(bpf_perf_event_output, struct pt_regs *, regs, struct bpf_map *, map,
./kernel/trace/bpf_trace.c:390:	BPF_CALL_0(bpf_get_current_task)
./kernel/trace/bpf_trace.c:401:	BPF_CALL_2(bpf_current_task_under_cgroup, struct bpf_map *, map, u32, idx)
./kernel/trace/bpf_trace.c:426:	BPF_CALL_3(bpf_probe_read_str, void *, dst, u32, size,
./kernel/trace/bpf_trace.c:537:	BPF_CALL_5(bpf_perf_event_output_tp, void *, tp_buff, struct bpf_map *, map,
./kernel/trace/bpf_trace.c:561:	BPF_CALL_3(bpf_get_stackid_tp, void *, tp_buff, struct bpf_map *, map,

# Network
./net/core/filter.c:161:	BPF_CALL_0(__get_raw_cpu_id)
./net/core/filter.c:112:	BPF_CALL_1(__skb_get_pay_offset, struct sk_buff *, skb)
./net/core/filter.c:117:	BPF_CALL_3(__skb_get_nlattr, struct sk_buff *, skb, u32, a, u32, x)
./net/core/filter.c:137:	BPF_CALL_3(__skb_get_nlattr_nest, struct sk_buff *, skb, u32, a, u32, x)
./net/core/filter.c:1521:	BPF_CALL_5(bpf_l3_csum_replace, struct sk_buff *, skb, u32, offset,
./net/core/filter.c:1565:	BPF_CALL_5(bpf_l4_csum_replace, struct sk_buff *, skb, u32, offset,
./net/core/filter.c:1618:	BPF_CALL_5(bpf_csum_diff, __be32 *, from, u32, from_size,
./net/core/filter.c:1657:	BPF_CALL_2(bpf_csum_update, struct sk_buff *, skb, __wsum, csum)
./net/core/filter.c:1758:	BPF_CALL_3(bpf_clone_redirect, struct sk_buff *, skb, u32, ifindex, u64, flags)
./net/core/filter.c:1808:	BPF_CALL_2(bpf_redirect, u32, ifindex, u64, flags)

./net/core/filter.c:1885:	BPF_CALL_1(bpf_get_cgroup_classid, const struct sk_buff *, skb)
./net/core/filter.c:1897:	BPF_CALL_1(bpf_get_route_realm, const struct sk_buff *, skb)
./net/core/filter.c:1909:	BPF_CALL_1(bpf_get_hash_recalc, struct sk_buff *, skb)
./net/core/filter.c:1926:	BPF_CALL_1(bpf_set_hash_invalid, struct sk_buff *, skb)
./net/core/filter.c:3035:	BPF_CALL_1(bpf_get_socket_cookie, struct sk_buff *, skb)
./net/core/filter.c:3047:	BPF_CALL_1(bpf_get_socket_uid, struct sk_buff *, skb)
./net/core/filter.c:1942:	BPF_CALL_2(bpf_set_hash, struct sk_buff *, skb, u32, hash)

./net/core/filter.c:1844:	BPF_CALL_4(bpf_sk_redirect_map, struct sk_buff *, skb,
./net/core/filter.c:1499:	BPF_CALL_2(bpf_skb_pull_data, struct sk_buff *, skb, u32, len)
./net/core/filter.c:1432:	BPF_CALL_5(bpf_skb_store_bytes, struct sk_buff *, skb, u32, offset,
./net/core/filter.c:1469:	BPF_CALL_4(bpf_skb_load_bytes, const struct sk_buff *, skb, u32, offset,
./net/core/filter.c:3065:	BPF_CALL_5(bpf_setsockopt, struct bpf_sock_ops_kern *, bpf_sock,
./net/core/filter.c:1960:	BPF_CALL_3(bpf_skb_vlan_push, struct sk_buff *, skb, __be16, vlan_proto,
./net/core/filter.c:1987:	BPF_CALL_1(bpf_skb_vlan_pop, struct sk_buff *, skb)
./net/core/filter.c:2163:	BPF_CALL_3(bpf_skb_change_proto, struct sk_buff *, skb, __be16, proto,
./net/core/filter.c:2202:	BPF_CALL_2(bpf_skb_change_type, struct sk_buff *, skb, u32, pkt_type)
./net/core/filter.c:2318:	BPF_CALL_4(bpf_skb_adjust_room, struct sk_buff *, skb, s32, len_diff,
./net/core/filter.c:2367:	BPF_CALL_3(bpf_skb_change_tail, struct sk_buff *, skb, u32, new_len,
./net/core/filter.c:2418:	BPF_CALL_3(bpf_skb_change_head, struct sk_buff *, skb, u32, head_room,
./net/core/filter.c:2722:	BPF_CALL_5(bpf_skb_event_output, struct sk_buff *, skb, struct bpf_map *, map,
./net/core/filter.c:2752:	BPF_CALL_4(bpf_skb_get_tunnel_key, struct sk_buff *, skb, struct bpf_tunnel_key *, to,
./net/core/filter.c:2819:	BPF_CALL_3(bpf_skb_get_tunnel_opt, struct sk_buff *, skb, u8 *, to, u32, size)
./net/core/filter.c:2855:	BPF_CALL_4(bpf_skb_set_tunnel_key, struct sk_buff *, skb,
./net/core/filter.c:2925:	BPF_CALL_3(bpf_skb_set_tunnel_opt, struct sk_buff *, skb,
./net/core/filter.c:2974:	BPF_CALL_3(bpf_skb_under_cgroup, struct sk_buff *, skb, struct bpf_map *, map,

./net/core/filter.c:2458:	BPF_CALL_2(bpf_xdp_adjust_head, struct xdp_buff *, xdp, int, offset)
./net/core/filter.c:2639:	BPF_CALL_2(bpf_xdp_redirect, u32, ifindex, u64, flags)
./net/core/filter.c:2662:	BPF_CALL_4(bpf_xdp_redirect_map, struct bpf_map *, map, u32, ifindex, u64, flags,
./net/core/filter.c:3010:	BPF_CALL_5(bpf_xdp_event_output, struct xdp_buff *, xdp, struct bpf_map *, map,

每個BPF_CALL都要定義一個對應的proto接口,用於verifier做參數檢檢查

const struct bpf_func_proto bpf_map_lookup_elem_proto = {
    .func       = bpf_map_lookup_elem,                   
	  .gpl_only   = false,                                 
    .pkt_access = true,                                  
    .ret_type   = RET_PTR_TO_MAP_VALUE_OR_NULL,          
    .arg1_type  = ARG_CONST_MAP_PTR,                     
    .arg2_type  = ARG_PTR_TO_MAP_KEY,                    
};

2.6 BPF xxx_kern.c , xxx_user.c

BPF的編程包括兩個部分

  • bpf 的kernel程序
    • 定義map數據結構
    • 使用有限的C語言特性,比如不能有循環
    • 可以定義多個bpf prog函數,被調用的函數要用inline
    • 只能調用BPF_CALL的kernel的接口
  • bpf的用戶程序
    • 一個可執行程序
    • 調用bpf()系統調用傳入bpf kernel程序編譯后的的bytecode
    • 使用循環獲取map值

示例代碼

BPF kernel 代碼

samples/bpf/tracex4_kern.c

 #include <linux/ptrace.h>
 #include <linux/version.h>
 #include <uapi/linux/bpf.h>
 #include "bpf_helpers.h"
 
 struct pair {    //Hash map的value
     u64 val;
     u64 ip;
 };
 
 struct bpf_map_def SEC("maps") my_map = {
     .type = BPF_MAP_TYPE_HASH,  // 使用Hash類型的map
     .key_size = sizeof(long),   // key是一個long,實際是分配的內存的指針
     .value_size = sizeof(struct pair),
     .max_entries = 1000000,     // 存放1M個記錄
 };
 
 /* kprobe is NOT a stable ABI. If kernel internals change t
  * example will no longer be meaningful
  */
// void kmem_cache_free(struct kmem_cache *s, void *x)
 SEC("kprobe/kmem_cache_free")   // 使用kprobe event,探測kmem_cache_free()接口
 int bpf_prog1(struct pt_regs *ctx)
 {
     long ptr = PT_REGS_PARM2(ctx);   // 得到kmem_cache_free第二個參數, 即要釋放的地址
 
     bpf_map_delete_elem(&my_map, &ptr); // 刪除hash map entry
     return 0;
 }
 
// void *kmem_cache_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node)
 SEC("kretprobe/kmem_cache_alloc_node") //使用kretprobe event,記錄分配內存的指針
 int bpf_prog2(struct pt_regs *ctx)
 {
     long ptr = PT_REGS_RC(ctx);  // 存放返回指針的寄存器是R0
     long ip = 0;
 
     /* get ip address of kmem_cache_alloc_node() caller */
     BPF_KRETPROBE_READ_RET_IP(ip, ctx); // 記錄caller 到ip,即內存分配者
 
     struct pair v = {
         .val = bpf_ktime_get_ns(),
         .ip = ip,
     };
 
     bpf_map_update_elem(&my_map, &ptr, &v, BPF_ANY); // 記錄pair到hash map
     return 0;
 }
 char _license[] SEC("license") = "GPL";
 u32 _version SEC("version") = LINUX_VERSION_CODE;

BPF user 代碼

samples/bpf/tracex4_user.c

#include "libbpf.h"                                                    
#include "bpf_load.h"                                                  
                                                                       
struct pair {                                                          
    long long val;                                                     
    __u64 ip;                                                          
};                                                                     
                                                                       
static __u64 time_get_ns(void)                                         
{                                                                      
    struct timespec ts;                                                
                                                                       
    clock_gettime(CLOCK_MONOTONIC, &ts);                               
    return ts.tv_sec * 1000000000ull + ts.tv_nsec;                     
}                                                                      
                                                                       
static void print_old_objects(int fd)                                  
{                                                                      
    long long val = time_get_ns();                                     
    __u64 key, next_key;                                               
    struct pair v;                                                     
                                                                       
    key = write(1, "\e[1;1H\e[2J", 12); /* clear screen */             
                                                                       
    key = -1;                                                          
    while (bpf_map_get_next_key(map_fd[0], &key, &next_key) == 0) { //遍歷map   
        bpf_map_lookup_elem(map_fd[0], &next_key, &v);    // 得到map entry的值             
        key = next_key;                                                
        if (val - v.val < 1000000000ll)                                
            /* object was allocated more then 1 sec ago */             
            continue;                                                  
        printf("obj 0x%llx is %2lldsec old was allocated at ip %llx\n",
               next_key, (val - v.val) / 1000000000ll, v.ip);          
    }                                                                  
}                                                                      
                                                                       
int main(int ac, char **argv)                                          
{                                                                      
    struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};                  
    char filename[256];                                                
    int i;                                                             
                                                                       
    snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);        
                                                                       
    if (setrlimit(RLIMIT_MEMLOCK, &r)) {                               
        perror("setrlimit(RLIMIT_MEMLOCK, RLIM_INFINITY)");            
        return 1;                                                      
    }                                                                  
    // load_bpf_file -> do_load_bpf_file -> load_and_attach ->  bpf_load_program  -> sys_bpf(BPF_PROG_LOAD, &attr, sizeof(attr))         
                                  |                  |                  |                        |
                      samples/bpf/bpf_load.c samples/bpf/bpf_load.c  tools/lib/bpf/bpf.c      tools/lib/bpf/bpf.c
		if (load_bpf_file(filename)) {                                    
        printf("%s", bpf_log_buf);                                     
        return 1;                                                      
    }                                                                  
                                                                       
    for (i = 0; ; i++) {                                               
        print_old_objects(map_fd[1]);                                  
        sleep(1);                                                      
    }                                                                  
                                                                       
    return 0;                                                          
}

3. Arm64 Linux上使用eBPF工具集BCC

BCC是eBPF最主要的支持庫和工具集,可以基於BCC寫自己的bpf程序,也可以使用已有的100多個bcc 工具進行跟蹤或性能分析。

但是因為BCC依賴於Clang+LLVM,導致大量的庫依賴,交叉編譯難度較大。

目前Android等Arm Linux系統比較方便的方案是使用adeb(Android Debian)項目的adeb。 [https://github.com/joelagnel/adeb] 。

adeb相當於預先構建一個ARM64的Debian rootfs,傳到板子上(或通過nfs掛載)后,用chroot切換到debian文件系統,就可以直接使用里面編譯好的Clang+LLVM+GCC工具鏈了。

這個ARM64 Debian文件系統是從Debian源里直接下載安裝的,可以根據需要定制安裝包,可以自動解決依賴關系。

Debian源里的BCC版本可能比較老,也可以在下載最新BCC源碼,直接在板子上編譯。

kernel需要打開的BPF相關配置:

CONFIG_BPF_SYSCALL=y
CONFIG_BPF_JIT_ALWAYS_ON=y
CONFIG_BPF_STREAM_PARSER=y
CONFIG_HAVE_BPF_JIT=y
CONFIG_BPF_JIT=y
CONFIG_BPF_EVENTS=y

3.1 adeb安裝

adeb為Android系統設計,默認使用adb和Android的/data目錄,也支持ssh,做少許修改就可以使用

diff --git a/androdeb b/androdeb
index a2ee65b..f215508 10075
--- a/androdeb
+++ b/androdeb
@@ -14,11 +14,11 @@ source $spath/utils/remote
 # Set default vars
 DISTRO=buster; ARCH=arm64
 ADB="adb"
-REMOTE="adb"
-FULL=0                         # Default to a minimal install
+REMOTE="ssh"
+FULL=1                         # Default to a minimal install
 DOWNLOAD=1                     # Default to downloading from web
-SKIP_DEVICE=0                  # Skip device preparation
-INSTALL_BCC=0                  # Decide if BCC is to be installed
+SKIP_DEVICE=1                  # Skip device preparation
+INSTALL_BCC=1                  # Decide if BCC is to be installed

完全安裝包需要2GB空間,可以放在/userdata分區,sd卡里,或者nfs映射。

nfs相對比較快捷,因為每次解壓安裝可能需要十幾分鍾時間。

  • 方法一: 安裝到板板子上,

    # 板子確保/userdata有2G空間,執行:
    mkdir /userdata/data
    mount -o remount,rw /
    ln -s /data /userdata/data
     
    # Linux上執行:
    解壓adeb.tgz
    cd adeb
    ./adeb --ssh root@192.168.2.12  prepare --archive tars/androdeb-fs.tgz
     
    # 等待安裝完成,大概十幾分鍾...
    ./adeb --ssh root@192.168.2.12 shell
    cd /usr/share/bcc/tools/
    ls
     
    # 即可執行bcc命令。
    
  • 方法二:nfs掛載

    # Linux上執行:
    cd adeb/tars
    tar xvf androdeb-fs.tgz
     
    # 板子上掛載nfs
    mount -o remount,rw /
    mkdir /data/
    mount -t nfs -o nolock 192.168.2.123:/home/yu/adeb/tars/debian /data
    ./adeb --ssh root@192.168.2.12 shell
    cd /usr/share/bcc/tools/
    ls
     
    # 即可執行bcc命令。
    

2. bcc 工具使用。

bcc自帶了100多個工具,/usr/share/bcc/tools/doc/下有每個工具的說明文檔

$ pwd
/home/yu/src/bpf/adeb/tars/debian/usr/share/bcc/tools/doc
yu@yu doc (master) $ ls
argdist_example.txt       fileslower_example.txt      opensnoop_example.txt    stackcount_example.txt
bashreadline_example.txt  filetop_example.txt         perlcalls_example.txt    statsnoop_example.txt
biolatency_example.txt    funccount_example.txt       perlflow_example.txt     syncsnoop_example.txt
biosnoop_example.txt      funclatency_example.txt     perlstat_example.txt     syscount_example.txt
biotop_example.txt        funcslower_example.txt      phpcalls_example.txt     tclcalls_example.txt
bitesize_example.txt      gethostlatency_example.txt  phpflow_example.txt      tclflow_example.txt
bpflist_example.txt       hardirqs_example.txt        phpstat_example.txt      tclobjnew_example.txt
btrfsdist_example.txt     inject_example.txt          pidpersec_example.txt    tclstat_example.txt
btrfsslower_example.txt   javacalls_example.txt       profile_example.txt      tcpaccept_example.txt
cachestat_example.txt     javaflow_example.txt        pythoncalls_example.txt  tcpconnect_example.txt
cachetop_example.txt      javagc_example.txt          pythonflow_example.txt   tcpconnlat_example.txt
capable_example.txt       javaobjnew_example.txt      pythongc_example.txt     tcpdrop_example.txt
cobjnew_example.txt       javastat_example.txt        pythonstat_example.txt   tcplife_example.txt
cpudist_example.txt       javathreads_example.txt     reset-trace_example.txt  tcpretrans_example.txt
cpuunclaimed_example.txt  killsnoop_example.txt       rubycalls_example.txt    tcpstates_example.txt
criticalstat_example.txt  lib                         rubyflow_example.txt     tcpsubnet_example.txt
cthreads_example.txt      llcstat_example.txt         rubygc_example.txt       tcptop_example.txt
dbslower_example.txt      mdflush_example.txt         rubyobjnew_example.txt   tcptracer_example.txt
dbstat_example.txt        memleak_example.txt         rubystat_example.txt     tplist_example.txt
dcsnoop_example.txt       mountsnoop_example.txt      runqlat_example.txt      trace_example.txt
dcstat_example.txt        mysqld_qslower_example.txt  runqlen_example.txt      ttysnoop_example.txt
deadlock_example.txt      nfsdist_example.txt         runqslower_example.txt   vfscount_example.txt
drsnoop_example.txt       nfsslower_example.txt       shmsnoop_example.txt     vfsstat_example.txt
execsnoop_example.txt     nodegc_example.txt          slabratetop_example.txt  wakeuptime_example.txt
exitsnoop_example.txt     nodestat_example.txt        sofdsnoop_example.txt    xfsdist_example.txt
ext4dist_example.txt      offcputime_example.txt      softirqs_example.txt     xfsslower_example.txt
ext4slower_example.txt    offwaketime_example.txt     solisten_example.txt     zfsdist_example.txt
filelife_example.txt      oomkill_example.txt         sslsniff_example.txt     zfsslower_example.txt

https://github.com/iovisor/bcc/tree/master/tools

下面腦圖是分類整理:

4. Reference

  1. 《BPF Performance Tool - Linux System and Application Observation》, Brendan Gregg
  2. LWN: A thorough introduction to eBPF
  3. LWN: An introduction to the BPF Compiler Collection
  4. https://events.static.linuxfound.org/sites/events/files/slides/ELC_2017_NA_dynamic_tracing_tools_on_arm_aarch64_platform.pdf
  5. eBPF In-kernel Virtual Machine & Cloud Computing
  6. https://elinux.org/images/d/dc/Kernel-Analysis-Using-eBPF-Daniel-Thompson-Linaro.pdf
  7. GCC,Clang, LLVM https://stackoverflow.com/questions/24836183/understanding-g-vs-clang-vs-llvm/24836566
  8. eBPF Trace from Kernel to Userspace PPT, eBPF基本概念介紹非常清楚
  9. Using Perf and its friend eBPF on Arm platform Leo Yan, Linaro
  10. LWN: BPFd: Running BCC tools remotely across systems and architectures, Joel Fernandes, Jan 2018
  11. Linux超能力BPF技術介紹及學習分享

— End—

文章標題:Ftrace的配置和使用
本文作者:hpyu
本文鏈接:https://www.cnblogs.com/hpyu/articles/14254250.html
歡迎轉載,請注明原文鏈接# 【原創】Kernel調試追蹤技術之 eBPF on ARM64


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM