BPF过滤器实现
在HURD中 目前 1dbc5f61中 BPF的实现为cBPF 即传统BPF.
在Hurd里, BPF承担的作用会和Hurd的微内核架构的IPC紧密结合.
它决定了一个网络包最终应该被送到哪个Mach receive port.
BPF过滤器
BPF最初是 Berkeley Packet Filter 就是用来过滤网络包的过滤器.
在UNIX中cBPF(classic BPF)的执行位于内核态.
而在Hurd中cBPF是hurd的用户态实现.
BPF使用了一种基于寄存器的虚拟机.
ISA指令集架构
BPF两种ISA:
基本指令编码 64bit.
宽指令编码, 也就是两个64bit 一共128bit.
基本指令编码
也就是64bit的编码
|opcode 8bit|regs 8bit|offset 16bit|imm 32bit|
Linux内核文档明确要求,未使用的字段必须置0
opcode: 操作码
操作码的组成如下:
|specific 5bit|class 3bit|
- `specific`: 这些位的意义定义于不同的`instruction class`
- `class`: 指令类别
regs: 源和目标寄存器的号码
regs组成如下:
|dst_reg|src_reg| # big-endian
|src_reg|dst_reg| # little-endian, 常见PC都是little-endian x86-64, arm64
- `src_reg`: 源寄存器的号码(0-10), 64bit的imm指令集对这个字段有其他用处
- `dst_reg`: 目标寄存器号码(0-10)
offset: 指针运算中使用的有符号偏移量 相当于i16imm: 立即数 是个i32
宽指令编码
宽指令编码由两条基本指令编码组成.
第一条是基本指令编码 第二条除了imm都是0.
也就是说 宽指令编码有两个imm i32.
instruction class
opcode的class占3bit.
class |
val |
descri |
|---|---|---|
LD |
0x0 |
从内存/特殊来源load |
LDX |
0x1 |
从内存加载到寄存器 |
ST |
0x2 |
imm写入内存 |
STX |
0x3 |
把寄存器写入内存 |
ALU |
0x4 |
32bit 算术/逻辑 |
JMP |
0x5 |
32bit 条件/无条件控制流 |
JMP32 |
0x6 |
32bit 比较跳转 |
ALU64 |
0x7 |
64bit 算术/逻辑 |
算术与跳转
对于 ALU JMP 系列, opcode分为三个部分:
| code 4bit| src 1bit| class 3bit|
code: 操作码src: 0代表使用imm作为源操作数; 1代表使用src_reg作为操作数
code操作码:
code |
val |
offset |
op |
descrip |
|---|---|---|---|---|
ADD |
0x0 |
0 |
dst += src |
加 |
SUB |
0x1 |
0 |
dst -= src |
减 |
MUL |
0x2 |
0 |
dst *= src |
乘 |
DIV |
0x3 |
0 |
dst = (src !=0) ? (dst/src) : 0 |
有符号除 |
SDIV |
0x3 |
1 |
dst = (src ==0) ? 0 :((src == -1 && dst == LLONG_MIN) ? LLONG_MIN : (dst s/ src)) |
无符号除 |
OR |
0x4 |
0 |
dst |= src |
或 |
AND |
0x5 |
0 |
dst &= src |
与 |
LSH |
0x6 |
0 |
dst <<= (src & mask) |
左移 |
RSH |
0x7 |
0 |
dst >>= (src & mask) |
逻辑右移 |
NEG |
0x8 |
0 |
dst = -dst |
取负 |
MOD |
0x9 |
0 |
dst = (src != 0) ? (dst % src) : dst |
无符号取模 |
SMOD |
0x9 |
1 |
dst = (src == 0) ? dst : ((src == -1 && dst == LLONG_MIN) ? 0: (dst s% src)) |
有符号取模 |
XOR |
0xa |
0 |
dst ^= src |
异或 |
MOV |
0xb |
0 |
dst = src |
复制src到dst |
MOVSX |
0xb |
8/16/32 |
dst = (s8,s16,s32)src |
符号拓展 |
ARSH |
0xc |
0 |
sign extending dst >>= (src & mask) |
算术右移 |
END |
0xd |
0 |
字节交换操作 |
字节交换 |
边界行为规约:
算术运算允许 下溢 与 溢出.
如果除0 那么
dst_reg会被设置为0.ALU64 如果
LLONG_MIN除以-1, 那么dst_reg置为LLONG_MINALU 如果
INT_MIN除以-1, 那么dst_reg置为INT_MIN如果
MOD了0, 那么ALU64的dst_reg不变.如果
MOD了0, 则ALU的dst_reg的高32位 置为0.如果 是
LLONG_MINMOD了 -1 或者INT_MINMOD了 -1,dst_reg都置为0.
字节交换指令END
hurd实现
hurd/libbpf/: cBPF的用户态实现gnumach/device/net_io.c: Mach的内核侧实现gnumach/device/net_io.hgnumach/include/device/bpf.h
流程
GNU Mach在受到packet后, 核心流程是:
网络设备
执行net_filter()
选择输入或输出过滤器队列
bpf_do_filter()
返回需要投递的字节数
复制到目标 Mach receive port
queue
BPF要用到的双向链表.
hurd/libbpf/queue.h
hurd/libbpf/queue.c
双向链表.
struct queue_entry {
struct queue_entry *next; /* next element */
struct queue_entry *prev; /* previous element */
};
typedef struct queue_entry *queue_t;
typedef struct queue_entry queue_head_t;
typedef struct queue_entry queue_chain_t;
typedef struct queue_entry *queue_entry_t;
if_filter_list_t
每个网络接口维护两类过滤器队列 定义于hurd/libbpf/bpf_impl.h:
输入过滤器:
if_rcv_port_list输出过滤器:
if_snd_port_list
typedef struct
{
queue_head_t if_rcv_port_list; /* input filter list */
queue_head_t if_snd_port_list; /* output filter list */
} if_filter_list_t;
net_rev_port
每个队列的对象是 net_rev_port_t
/*
* Receive port for net, with packet filter.
* This data structure by itself represents a packet
* filter for a single session.
*/
struct net_rcv_port {
queue_chain_t input; /* list of input open_descriptors */
queue_chain_t output; /* list of output open_descriptors */
mach_port_t rcv_port; /* port to send packet to */
int rcv_count; /* number of packets received */
int priority; /* priority for filter */
filter_t *filter_end; /* pointer to end of filter */
filter_t filter[NET_MAX_FILTER];
/* filter operations */
};
其中:
rev_port: 匹配成功后 投递到的Mach Port.priority: 过滤器优先级filter[]: 过滤器程序filter_end: 程序结束位置