9.1.5 BPF虚拟机
BPF数据包过滤的实现采用了虚拟机技术。BPF虚拟机(BPF Pseudo-Machine)由一个累加器、一个索引寄存器、一块内存区与一个隐含的指令计数器组成。虚拟机采用固定长度的指令,指令格式定义如表9-2所示。
在表9-2所示中,Opcode为16位的操作码,用来指示指令类型和寻址模式;jt(真跳转)和jf(假跳转)用于条件跳转指令,代表对应过滤成功或失败情况下的指令跳转偏移;字段k为通用字段,用于各种情况。
虚拟机所采用的指令有以下六种类型:
(1)加载指令(LOAD INSTRUCTION)
此指令用于把一个值复制到累加器或索引寄存器上。源操作数的类型可以为一个立即数(BPF IMM)、数据包中一个固定位置的数据(BPF ABS)、数据包中一个可变位置的数据(BPF IND)、数据包的长度(BPF LEN),或者是在内存区的值(BPF MEM)。例如,下面的指令:
BPF_LD|BP_W|BPF_IND
它表示的是间接寻址(BPF_IND),意思是把寄存器中的值与k字段的值相加,得到数据包从起始端开始的偏移,并从那个位置开始取出一个4字节长度的数据,保存到累加器中。
再例如,下面的指令:
BPF_LDX|BPF_W|BPF_IMM
就是把k字段的值放到索引寄存器中。
(2)存储指令(STORE INSTRUCTION)
此指令用于把累加器或索引寄存器中的值保存到内存中。例如,下面的指令:
BPF_ST
表示把累加器中的值保存到以k字段的值为偏移量的内存中。
(3)算术运算指令(ALU INSTRUCTION)
此指令表示在累加器上执行算术或逻辑运算,它会把索引寄存器或一个常量作为一个操作数。
此指令包括BPF_ADD、BPF_SUB、BPF_MUL、BPF_DIV、BPF_AND等指令,分别表示加、减、乘、除、逻辑与等操作。如指令:
BPF_ALU|BPF_ADD|BPF_K
表示把累加器中的值与k字段的值相加后保存到累加器中。
BPF_ALU|BPF_LSH|BPF_X
表示把累加器中的值向左移至寄存器中值的位数。
(4)分支跳转指令(BRANCH INSTRUCTION)
此指令用于调整控制流程,其把累加器中的值与一个常数(BPF_K)或寄存器(BPF_X)中的值进行比较,如果结果为真,则执行为真的分支,否则执行为假的分支。例如指令:
BPF_JMP|BPF_JGT|BPF_K
表示如果累加器中的值大于(BPF_JGT)k字段中的值,则跳转到该指令下面第jt字段值条数的语句处,否则跳转到该指令下面第jf字段值条数的语句处。而指令BPF_JMP|BPF_JA则是无条件跳转语句,跳转到下面第k字段值条数的语句处。
(5)返回指令(RETURN INSTRUCTION)
此指令表示终止过滤器程序,并指明需接收数据包的多少字节。如果返回值是零,则表示忽略该数据包。例如,下面的指令:
BPF_RET|BPF_A
表示接收累加器中值的字节数。
BPF_RET|BPF_K
表示接收k字段值的字节数。
(6)杂类指令(MISCELLANEOUS INSTRUCTION)
此指令包括不能归到上面类别中的所有指令。例如,下面的指令:
BPF_MISC|BPF_TAX
表示把累加器中的值传到寄存器中。
BPF_MISC|BPF_TXA
表示把寄存器中的值传到累加器中。
表9-3所示为所有的BPF指令集。表中采用“汇编语法”的方式,来说明BPF过滤器与调试输出。实际的编码是通过C语言的宏来定义的(例如wpcap\libpcap\pcap\bpf.h文件中的#define BPF_ALU 0x04定义),表9-3中的"addr modes"列给出了在"opcodes"列中每个指令所允许使用的寻址模式。各寻址模式的含义如表9-4所示。
加载指令只是简单地把指定值复制给累加器(ld、ldh、ldb指令)或索引寄存器(ldx指令)。索引寄存器不能使用数据包寻址模式,相反,一个数据包值必须被加载到累加器上,并通过tax指令转移到索引寄存器中,但该操作并不常见。使用索引寄存器的主要目的是解析变长IP头,IP头能通过4*([k]&0xf)寻址模式被直接加载。除了数据包的数据能够作为无符号字符(1字节,ldb)或无符号半字(2字节,ldh)被加载进累加器外,其他所有值都是32位长的字。同样的,内存存储是作为32位字的数组被寻址的。所有的指令字段都是用主机字节顺序表示的,并且加载指令把数据包数据从网络字节顺序转换到了主机字节顺序。所有被引用的数据如果超出了数据包的结尾,都将导致终止该过滤操作,过滤器返回0值(也就是该数据包将被丢弃)。
ALU操作(add、sub等指令)使用累加器与操作数执行指定的操作,并把结果存储回累加器,如果出现除零操作,将终止过滤。
跳转指令会将累加器中的值与一个常数(其中,jset执行一个“位与”操作——对条件位的检测有用)进行比较。如果结果为真(或非0),真分支将被执行,否则假分支被执行。任何比较,都能够通过把减法的结果与0比较来实现,但这并不常见。注意,并不存在jlt、jle或jne等操作符,因为这些都能通过上述指令利用反转分支来构建。同样,因为跳转偏移被编码为8位,那么最大跳转长度就为256个指令,但跳转更长的情况也有可能出现,因此专门提供一个总是使用32位偏移操作数的跳转操作(jmp操作符)。
返回指令会终止程序并指明需要接收数据包的字节数。如果为0,该数据包将被整个丢弃。实际接收的字节数为数据包长度与过滤器所指明长度的最小值。