ebpf
bpf 介绍
cbpf和ebpf
Alexei Starovoitov 在2013年提出了我们现在所称的BPF。 然后,它被称为"extended"BPF或eBPF。
cBPF虚拟机非常有限。cbpf只有两个寄存器和一个隐藏帧地址并且是32位。他可以被附加到socket,xtables或seccomp之后。
ebpf增加到10个64位寄存器一个只读帧地址(支持32位cbpf语义)。并允许调用一些受限制的内核。
cbpf是一个非常复杂的技术,并且cbpf只应用于网络过滤。但是现在eBPF更像是一种通用虚拟机。ebpf可以在各类内核事件上运行,包括磁盘IO,网络IO,可以使用tracepoints和kprobes的内核调用。
使用ebpf的64位寄存器,可以实现主机到寄存器的一一映射(这是ebpf map的作用)。这样将会降低很多开销
ebpf增加到一下使用场景
- Networking: XDP, tc, socket progs, kcm, reuseport, …
- Tracing: kprobes, uprobes, tracepoints, …
- Security: seccomp, landlock
ebpf 和 kubernetes
两个不一样的代码
在实际的学习ebpf中,我们发现两种不一样的代码。前面说过ebpf程序实际上是一组指令集。两种代码实际上是一样的。一种是直接写指令集,另一种是编写C语言,再从C转为指令集。而从C转为指令集需要一些工具。这也就是为什么第二种方法都有一个.o(elf格式)的文件。
- 直接编写指令集的程序,可以直接使用gcc来编译,甚至你不需要其他的第三方依赖库(如BCC,libbpf)。但是这样编写的难度非常大。在后文,我会介绍指令集的说明。
- 使用C语言的程序,必须有一个转译的工具,目前有两种选择1.llvm+clang。2.BCC。BCC也是由llvm编译而来。目前为止绝大部分的ebpf程序应该是不能保持CO-RE(一次编译,处处运行)。在libbpf中提出了CO-RE的方案。同样在使用第二种方式编译过程中需要生成.o的elf文件。你可以直接使用iproute等命令直接加载。或者你写C程序来加载。
在本篇文章中,大部分代码源于libbpf-bootstrap
ebpf类型
各个linux版本支持的不一样
enum bpf_prog_type {
BPF_PROG_TYPE_UNSPEC,
BPF_PROG_TYPE_SOCKET_FILTER,
BPF_PROG_TYPE_KPROBE,
BPF_PROG_TYPE_SCHED_CLS,
BPF_PROG_TYPE_SCHED_ACT,
BPF_PROG_TYPE_TRACEPOINT,
BPF_PROG_TYPE_XDP,
BPF_PROG_TYPE_PERF_EVENT,
BPF_PROG_TYPE_CGROUP_SKB,
BPF_PROG_TYPE_CGROUP_SOCK,
BPF_PROG_TYPE_LWT_IN,
BPF_PROG_TYPE_LWT_OUT,
BPF_PROG_TYPE_LWT_XMIT,
BPF_PROG_TYPE_SOCK_OPS,
BPF_PROG_TYPE_SK_SKB,
BPF_PROG_TYPE_CGROUP_DEVICE,
BPF_PROG_TYPE_SK_MSG,
BPF_PROG_TYPE_RAW_TRACEPOINT,
BPF_PROG_TYPE_CGROUP_SOCK_ADDR,
BPF_PROG_TYPE_LWT_SEG6LOCAL,
BPF_PROG_TYPE_LIRC_MODE2,
BPF_PROG_TYPE_SK_REUSEPORT,
BPF_PROG_TYPE_FLOW_DISSECTOR,
};
不同的ebpf中断的点不一样。
不同的ebpf类型所能使用的帮助函数不一样。
不同的ebpf类型R0上下文也不一样。
ebpf可以track多个事件。同时多个ebpf可以访问同一个map
tracing tracing tracing packet packet packet
event A event B event C on eth0 on eth1 on eth2
| | | | | ^
| | | | v |
--> tracing <-- tracing socket tc ingress tc egress
prog_1 prog_2 prog_3 classifier action
| | | | prog_4 prog_5
|--- -----| |------| map_3 | |
map_1 map_2 --| map_4 |--
指令集
寄存器
先介绍寄存器,ebpf的寄存器如下。
- R0: 函数的参数以及eBPF程序的退出值。在指令集中我们可以调用函数(比如BPF_FUNC_map_lookup_elem)。该函数的结果储存到R0,同时ebpf程序退出时,我们也必须要设定R0值。
- R1-R5: 内核函数的参数。在调用ebpf程序时,部分的寄存器上已经有数据了,R0保存上下文(比如包的指针)。同时R1-R5也用于帮助函数的参数。
- R6-R9: 在函数调用期间该数据会被保持。
- R10: 只读帧指针,用于访问ebpf程序的栈(该栈大小512byte)。使用例子:将R1和R2压入栈然后调用ebpf帮助函数,该帮助函数就会使用R1和R2作为参数。
指令
一条指令就是一个结构体:
指令结构如下(lsb低位,msb高位)(ebpf):
msb lsb
+------------------------+----------------+----+----+--------+
|immediate |offset |src |dst |opcode |
+------------------------+----------------+----+----+--------+
结构体如下(ebpf):
struct bpf_insn {
__u8 opcode; /* 操作 */
__u8 dst_reg:4; /* 目标寄存器 */
__u8 dst_reg:4; /* 源寄存器 */
__s16 offset; /* 偏移量 */
__s32 immediate; /* 数值 */
};
通常我们使用宏定义来封装bpf_insn(ebpf):
#define BPF_MOV64_IMM(DST, IMM) \
((struct bpf_insn) { \
.code = BPF_ALU64 | BPF_MOV | BPF_K, \
.dst_reg = DST, \
.src_reg = 0, \
.off = 0, \
.imm = IMM })
显然他们是一一对应的。从最低有效位到最高有效位:
- 8位操作码
- 4位目标寄存器(DST)
- 4位源寄存器(src)
- 16位偏移
- 32位立即数(imm),常量
指令集如图所示:
我们用BPF_CLASS(code)表示inst class
,BPF_OP(code)表示opcode
。
这里应该是兼容cbpf的。BPF_CLASS(code)如下(BPF_CLASS(code)表示了指令集的操作):
Classic BPF classes: eBPF classes:
BPF_LD 0x00 BPF_LD 0x00 // 将上下文值复制到R0
BPF_LDX 0x01 BPF_LDX 0x01
BPF_ST 0x02 BPF_ST 0x02
BPF_STX 0x03 BPF_STX 0x03 // 将上下文加载栈(R10)
BPF_ALU 0x04 BPF_ALU 0x04
BPF_JMP 0x05 BPF_JMP 0x05 // 条件跳转
BPF_RET 0x06 BPF_JMP32 0x06 // cbpf表示:结束指令,设定接收的包的长度,在ebpf中使用BPF_JMP | BPF_EXIT表示退出
BPF_MISC 0x07 BPF_ALU64 0x07 // cbpf表示:将A中的值存入X中,或将X中的值存入A中。在ebpf使用 BPF_MOV | BPF_X | BPF_ALU 替代。BPF_ALU64表示64位操作数
BPF_ALU or BPF_JMP
+----------------+--------+--------------------+
| 4 bits | 1 bit | 3 bits |
| operation code | source | instruction class |
+----------------+--------+--------------------+
(MSB) (LSB)
当 BPF_CLASS(code) == BPF_ALU or BPF_JMP,第四个bit source有两种
BPF_K 0x00
BPF_X 0x08
- 在cBPF, BPF_X表示使用寄存器X作为源操作数。BPF_Y表示使用imm立即数作为源操作数。
- 在ebpf, BPF_X表示使用src_reg作为源操作数,BPF_Y表示使用32位imm立即数作为源操作数。
如果 BPF_CLASS(code) == BPF_ALU or BPF_ALU64 (eBPF), BPF_OP(code)是以下:
BPF_ADD 0x00 // 加,A <- A+k。这里只列出一个,其他的类似
BPF_SUB 0x10
BPF_MUL 0x20
BPF_DIV 0x30
BPF_OR 0x40
BPF_AND 0x50
BPF_LSH 0x60
BPF_RSH 0x70
BPF_NEG 0x80
BPF_MOD 0x90
BPF_XOR 0xa0
BPF_MOV 0xb0 /* eBPF only: mov reg to reg */
BPF_ARSH 0xc0 /* eBPF only: sign extending shift right */
BPF_END 0xd0 /* eBPF only: endianness conversion */
- 如果 BPF_CLASS(code) == BPF_JMP or BPF_JMP32 (eBPF), BPF_OP(code)是以下:
BPF_JA 0x00 /* BPF_JMP only */
BPF_JEQ 0x10
BPF_JGT 0x20
BPF_JGE 0x30
BPF_JSET 0x40
BPF_JNE 0x50 /* eBPF only: jump != */
BPF_JSGT 0x60 /* eBPF only: signed '>' */
BPF_JSGE 0x70 /* eBPF only: signed '>=' */
BPF_CALL 0x80 /* eBPF BPF_JMP only: function call */
BPF_EXIT 0x90 /* eBPF BPF_JMP only: function return */
BPF_JLT 0xa0 /* eBPF only: unsigned '<' */
BPF_JLE 0xb0 /* eBPF only: unsigned '<=' */
BPF_JSLT 0xc0 /* eBPF only: signed '<' */
BPF_JSLE 0xd0 /* eBPF only: signed '<=' */
所以BPF_ADD | BPF_X | BPF_ALU
在cBPF和eBPF中都表示32位加法。cBPF中只有两个寄存器,因此它意味着A + =X。
在eBPF中,它意味着dst_reg =(u32)dst_reg +(u32)src_reg; 相似地,
BPF_XOR | BPF_K | BPF_ALU
在cBPF
中表示A ^ = imm32
,在ebpf表示src_reg =(u32)src_reg ^(u32)
,u32是eBPF中的imm32。
cbpf中使用BPF_RET表示退出。ebpf中使用BPF_JMP | BPF_EXIT
表示退出。
cbpf使用 BPF_RET | BPF_K 表示高级语言的return 0;
,cbpf将返回数放在寄存器上。
ebpf则是使用两个指令表示返回.BPF_ALU64 | BPF_MOV | BPF_K
表示将32位放到寄存器r0。BPF_JMP | BPF_EXIT
表示退出
// 这两个宏定义,就是上面的指令
BPF_MOV64_IMM(BPF_REG_0, 0), /* r0 = 0 */
BPF_EXIT_INSN(),
BPF_LD(X) or BPF_ST(X)
+--------+--------+-------------------+
| 3 bits | 2 bits | 3 bits |
| mode | size | instruction class |
+--------+--------+-------------------+
(MSB) (LSB)
Size 是以下的其中之一
BPF_W 0x00 /* 字节 */
BPF_H 0x08 /* 半个字节 */
BPF_B 0x10 /* byte */
BPF_DW 0x18 /* eBPF only, 双字节 */
Mode 是以下之一
BPF_IMM 0x00 /* used for 32-bit mov in classic BPF and 64-bit in eBPF */
BPF_ABS 0x20
BPF_IND 0x40
BPF_MEM 0x60
BPF_LEN 0x80 /* classic BPF only, reserved in eBPF */
BPF_MSH 0xa0 /* classic BPF only, reserved in eBPF */
BPF_XADD 0xc0 /* eBPF only, exclusive add */
eBPF有两个非通指令访问package信息: (BPF_ABS | <size> | BPF_LD) 和(BPF_IND | <size> | BPF_LD)。只有当上下文是skb指针时并且具有7个隐式操作数,才可以使用。例如:
//R6 储存skb指针
BPF_IND | BPF_W | BPF_LD means:
R0 = ntohl(*(u32 *) (((struct sk_buff *) R6)->data + src_reg + imm32))
与cbpf不同。ebpf有load/store操作,如:
BPF_MEM | <size> | BPF_STX: *(size *) (dst_reg + off) = src_reg
BPF_MEM | <size> | BPF_ST: *(size *) (dst_reg + off) = imm32
BPF_MEM | <size> | BPF_LDX: dst_reg = *(size *) (src_reg + off)
BPF_XADD | BPF_W | BPF_STX: lock xadd *(u32 *)(dst_reg + off16) += src_reg
BPF_XADD | BPF_DW | BPF_STX: lock xadd *(u64 *)(dst_reg + off16) += src_reg
你可以在https://github.com/iovisor/bpf-docs/blob/master/eBPF.md 获取到更易于理解的信息
我们平时看到的其他函数比如
BPF_PERF_OUTPUT(events);
int kprobe__tcp_connect(struct pt_regs *ctx,struct sock *sk) {
struct data_t data = {};
data.pid = bpf_get_current_pid_tgid();
data.ts = bpf_ktime_get_ns();
bpf_get_current_comm(&data.comm, sizeof(data.comm));
events.perf_submit(ctx, &data, sizeof(data));
return 0;
}
实际上是翻译成指令集数组。
指令转译
llvm-objdump提供了将.o文件转译为指令集的方法。如下
[root@1111 .output]# llvm-objdump -d minimal.bpf.o
minimal.bpf.o: file format elf64-bpf
Disassembly of section tp/syscalls/sys_enter_write:
0000000000000000 <handle_tp>:
0: 85 00 00 00 0e 00 00 00 call 14
1: 77 00 00 00 20 00 00 00 r0 >>= 32
2: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll
4: 61 11 00 00 00 00 00 00 r1 = *(u32 *)(r1 + 0)
5: 5d 01 10 00 00 00 00 00 if r1 != r0 goto +16 <LBB0_2>
6: b7 01 00 00 64 2e 0a 00 r1 = 667236
7: 63 1a f8 ff 00 00 00 00 *(u32 *)(r10 - 8) = r1
8: 18 01 00 00 6f 6d 20 50 00 00 00 00 49 44 20 25 r1 = 2675213260325678447 ll
10: 7b 1a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r1
11: 18 01 00 00 67 65 72 65 00 00 00 00 64 20 66 72 r1 = 8243311783519085927 ll
13: 7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 24) = r1
14: 18 01 00 00 42 50 46 20 00 00 00 00 74 72 69 67 r1 = 7451612901544448066 ll
16: 7b 1a e0 ff 00 00 00 00 *(u64 *)(r10 - 32) = r1
17: bf a1 00 00 00 00 00 00 r1 = r10
18: 07 01 00 00 e0 ff ff ff r1 += -32
19: b7 02 00 00 1c 00 00 00 r2 = 28
20: bf 03 00 00 00 00 00 00 r3 = r0
21: 85 00 00 00 06 00 00 00 call 6
00000000000000b0 <LBB0_2>:
22: b7 00 00 00 00 00 00 00 r0 = 0
23: 95 00 00 00 00 00 00 00 exit
map
BPF maps: the data structures of BPF
- BPF中最大的特征就是BPF maps。BPF maps 是储存数据的地方。
- maps 储存key/value数据结构
- 不同的BPF程序可以使用BPF 相互访问。用户态的程序也可以使用BPF maps获取BPF数据。
- 不同的进程也可以访问同一个map(一个exit-close的fd)。同时如果你调用BPF_OBJ_PIN。则可以将map固定到fd上,即使进程退出,该map也不会消失
下面是man bpf释义
maps 储存通用的数据类型,这些数据被认为是二进制对象。所以用户在创建maps数据时,只需要指定key和value的大小。
用户程序可以创建多个maps,但不能显示的使用maps数据,通过句柄(文件描述符)来访问他们。不同的BPF程序可以并发的访问maps。
map类型
enum bpf_map_type {
BPF_MAP_TYPE_UNSPEC, /* Reserve 0 as invalid map type */
BPF_MAP_TYPE_HASH, // 最简单的key,value储存。空间大小有限制(max_entries)
BPF_MAP_TYPE_ARRAY, // 数组类型的map(你可以认为就是一个数组)。查询速度更快。同时没有delete操作
BPF_MAP_TYPE_PROG_ARRAY,// 内容为其他eBPF程序的fd
BPF_MAP_TYPE_PERF_EVENT_ARRAY,
BPF_MAP_TYPE_PERCPU_HASH,// 和BPF_MAP_TYPE_HASH一样,但是会写到特定cpu号的map。如ebpf程序两次运行在不同CPU上。每次则每个map都写入一次
BPF_MAP_TYPE_PERCPU_ARRAY,
BPF_MAP_TYPE_STACK_TRACE,
BPF_MAP_TYPE_CGROUP_ARRAY,
BPF_MAP_TYPE_LRU_HASH,
BPF_MAP_TYPE_LRU_PERCPU_HASH,
BPF_MAP_TYPE_LPM_TRIE,
BPF_MAP_TYPE_ARRAY_OF_MAPS,
BPF_MAP_TYPE_HASH_OF_MAPS,
BPF_MAP_TYPE_DEVMAP,
BPF_MAP_TYPE_SOCKMAP,
BPF_MAP_TYPE_CPUMAP,
BPF_MAP_TYPE_XSKMAP,
BPF_MAP_TYPE_SOCKHASH,
BPF_MAP_TYPE_CGROUP_STORAGE,
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
BPF_MAP_TYPE_QUEUE,
BPF_MAP_TYPE_STACK,
/* See /usr/include/linux/bpf.h for the full list. */
};
ebpf主要分为三大类:
- 通用的BPF maps
- BPF_HAS: hash map, 最简单的maps 数据结构
- BPF(PERCPU)ARRAY: hash array。使用整数索引(key是整型),实现lookup/update方法来查找更新。
- 用于统计的maps
- BPF_HISTOGRAM用来生成直方图。在用户空间,可以使用辅助函数打印出来。
- BPF_PERF_ARRAY是计数器。 (BPF_PERF_ARRAY is used to return a hardware-calculated counter of the number of cycles elapsed)
- BPF_STACK_TRACE储存堆栈跟踪
map Pinning
map绑定到文件系统上(必须是ebpf文件系统),这样第三方程序可以读写该map
下面是使用ebpf map和Object Pinning的方法
// clang -g -fPIC example.c -o example -lbpf -lelf
// 可以直接使用gcc
static const char *file_path = "/sys/fs/bpf/lsm_hash";
int main(int argc, char **argv){
int map_fd ;
int prog_fd;
int ret;
// 创建一个map
map_fd = bpf_create_map(BPF_MAP_TYPE_HASH, sizeof(int), sizeof(int), 32, BPF_F_NO_PREALLOC);
printf("%d",map_fd);
printf("bpf: map fd:%d (%s)\n", map_fd, strerror(errno));
assert(map_fd > 0);
//add
int key = 1, value = 1234;
int added;
//#define BPF_ANY 0 /* create new element or update existing */
//#define BPF_NOEXIST 1 /* create new element only if it didn't exist */
//#define BPF_EXIST 2 /* only update existing element */
added = bpf_map_update_elem(map_fd, &key, &value, BPF_ANY);
if (added < 0) {
printf("Failed to update map: %d (%s)\n", added, strerror(errno));
return -1;
}
// 将map 绑定到fd,这样进程退出时,map 不会消失。
// ls -la /proc/{pid}/fd检查你的fd。
ret = bpf_obj_pin(map_fd, file_path);
printf("bpf: pin ret:(%d,%s)\n", ret, strerror(errno));
assert(ret == 0);
//做一个阻塞
}
即使程序退出,也可以获取到map数据
[root@localhost willow]# mount -t bpf none /sys/fs/bpf/
[root@localhost willow]# ./example
3bpf: map fd:3 (Success)
bpf: pin ret:(0,Success)
[root@localhost willow]# bpftool map list
7: hash flags 0x1
key 4B value 4B max_entries 32 memlock 4096B
我们可以使用一下代码获取map数据
static const char *file_path = "/sys/fs/bpf/lsm_hash";
int main(int argc, char **argv){
int map_fd ;
int ret,lookup;
int key = 1, value = 0;
// 获取一个map
map_fd = bpf_obj_get("/sys/fs/bpf/lsm_hash");
printf("%d",map_fd);
printf("bpf: map fd:%d (%s)\n", map_fd, strerror(errno));
assert(map_fd > 0);
key = 1;
lookup = bpf_map_lookup_elem(map_fd, &key, &value);
if (lookup < 0) {
printf("Failed to lookup map: %d (%s)\n", lookup, strerror(errno));
return -1;
}
printf("lookup map: %d\n",value);
return 0;
}
//clang -g -fPIC map.c -o map -lbpf -lelf
[root@localhost willow]# ./map
3bpf: map fd:3 (Success)
lookup map: 1234
bpf_obj_pin 和 bpf_obj_get 是固定map 和 prog。因此对于ebpf程序来说,也可以固定到文件上。注意该文件必须是bpf文件系统
除了bpf_obj_pin的方式固定map之外,也可以使用socket实现多程序共享。不过socket需要更强大的编程能力
帮助函数
帮助函数(注意linux版本):
#define __BPF_FUNC_MAPPER(FN) \
FN(unspec), \
FN(map_lookup_elem), \
FN(map_update_elem), \
FN(map_delete_elem), \
FN(probe_read), \
FN(ktime_get_ns), \
FN(trace_printk), \
FN(get_prandom_u32), \
FN(get_smp_processor_id), \
FN(skb_store_bytes), \
FN(l3_csum_replace), \
FN(l4_csum_replace), \
FN(tail_call), \
FN(clone_redirect), \
FN(get_current_pid_tgid), \
FN(get_current_uid_gid), \
FN(get_current_comm), \
FN(get_cgroup_classid), \
FN(skb_vlan_push), \
FN(skb_vlan_pop), \
FN(skb_get_tunnel_key), \
FN(skb_set_tunnel_key), \
FN(perf_event_read), \
FN(redirect), \
FN(get_route_realm), \
FN(perf_event_output), \
FN(skb_load_bytes), \
FN(get_stackid), \
FN(csum_diff), \
FN(skb_get_tunnel_opt), \
FN(skb_set_tunnel_opt), \
FN(skb_change_proto), \
FN(skb_change_type), \
FN(skb_under_cgroup), \
FN(get_hash_recalc), \
FN(get_current_task), \
FN(probe_write_user), \
FN(current_task_under_cgroup), \
FN(skb_change_tail), \
FN(skb_pull_data), \
FN(csum_update), \
FN(set_hash_invalid), \
FN(get_numa_node_id), \
FN(skb_change_head), \
FN(xdp_adjust_head), \
FN(probe_read_str), \
FN(get_socket_cookie), \
FN(get_socket_uid), \
FN(set_hash), \
FN(setsockopt), \
FN(skb_adjust_room), \
FN(redirect_map), \
FN(sk_redirect_map), \
FN(sock_map_update), \
FN(xdp_adjust_meta), \
FN(perf_event_read_value), \
FN(perf_prog_read_value), \
FN(getsockopt), \
FN(override_return), \
FN(sock_ops_cb_flags_set), \
FN(msg_redirect_map), \
FN(msg_apply_bytes), \
FN(msg_cork_bytes), \
FN(msg_pull_data), \
FN(bind), \
FN(xdp_adjust_tail), \
FN(skb_get_xfrm_state), \
FN(get_stack), \
FN(skb_load_bytes_relative), \
FN(fib_lookup), \
FN(sock_hash_update), \
FN(msg_redirect_hash), \
FN(sk_redirect_hash), \
FN(lwt_push_encap), \
FN(lwt_seg6_store_bytes), \
FN(lwt_seg6_adjust_srh), \
FN(lwt_seg6_action), \
FN(rc_repeat), \
FN(rc_keydown), \
FN(skb_cgroup_id), \
FN(get_current_cgroup_id), \
FN(get_local_storage), \
FN(sk_select_reuseport), \
FN(skb_ancestor_cgroup_id), \
FN(sk_lookup_tcp), \
FN(sk_lookup_udp), \
FN(sk_release), \
FN(map_push_elem), \
FN(map_pop_elem), \
FN(map_peek_elem), \
FN(msg_push_data),
#define __BPF_ENUM_FN(x) BPF_FUNC_ ## x
enum bpf_func_id {
__BPF_FUNC_MAPPER(__BPF_ENUM_FN)
__BPF_FUNC_MAX_ID,
};
#undef __BPF_ENUM_FN
在BPF有专门的指令调用ebpf帮助程序:BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem),
这样我们就可以在指令集中使用帮助函数了。
ebpf帮助函数使用压栈R1-R5的参数。
当然我们也可以在C语言中使用帮助函数,llvm会帮助我们转化为上面的指令。
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
char LICENSE[] SEC("license") = "Dual BSD/GPL";
int my_pid = 0;
SEC("tp/syscalls/sys_enter_write")
int handle_tp(void *ctx)
{
int pid = bpf_get_current_pid_tgid() >> 32;
if (pid != my_pid)
return 0;
bpf_printk("BPF triggered from PID %d.\n", pid);
return 0;
}
程序验证
BPF verify 需要确保BPF程序能安全的运行。他会做一下检查。同时也会优化程序。**
- 检查控制流程图是否存在循环。
- 检测超出范围的跳跃,无法到达的指令。
- 跟踪上下文访问,初始化的内存,堆栈溢出/填充。
- 检查未特权的指针泄漏。
- 验证辅助函数调用参数。
- 数据访问的值和对齐跟踪(pkt指针,地图访问)。
- 注册活动性分析以进行修剪。
- 状态修剪以减少验证复杂性。
尾调
尾调是使用ebpf的一种特殊的机制。和普通的系统调用一致,尾调可以使得一个ebpf程序调用另一个ebpf程序。另外尾调不反回原来的ebpf。也就是A调用B,并不会返回到A.
实际上尾调是由jmp实现。
对于验证程序来说,会分别验证每一个ebpf合法性。
只有相同的ebpf程序可以尾调.
实现尾调,需要两部分:BPF_MAP_TYPE_PROG_ARRAY和bpf_tail_call()。第一个map储存尾调的文件描述符。bpf_tail_call()是bpf帮助函数。
bpf call bpf
bpf 程序之间可以调用。这和平时高级语言的函数调用是一致。
BCC
BCC 使用python 加载C代码的方式实现BPF。python 帮助你完成编译部分,C语言则是你的BPF程序高级表示。
最方便和最快捷的编写BPF程序就是使用BCC,你可以写一个C语言的BPF代码,在python使用b = BPF(src_file="bpf_program.c")
在加载你的C代码。
在C 代码里面,你可以使用BPF_PERF_OUTPUT(name)
宏定义。在python中访问b["name"]
即可获得数据
有一个技巧,你可以在python内联你的C代码。这样你可以在python中替换你的C语言代码。就像这样
#!/usr/bin/python
import bpf from bcc
c = """
#include <>
.....
"""
b = BPF(c)