【原创】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

posted @ 2021-01-30 13:39  HPYU  阅读(5269)  评论(0编辑  收藏  举报