BPF是一個過濾機制,它用于過濾送往特定地點比如使用者空間的資料包,它被設計成一種類似彙編語言的語言,可以稱之為僞彙編碼。雖然被設計用來過濾資料包,但這種設計方式更适合用于操作硬體,特别用來編寫需要寫少量固定序列的硬體驅動程式。不管用于什麼,BPF的設計是優秀的,是狀态機實作控制邏輯的完美執行個體。BPF實際上是一組基于狀态機的比對過濾序列,用于簡單的資料包模式比對。每個比對包含四個元素,定義為一個結構體:
struct socket_filter
{
__u16 code; //操作碼,可以實作數值運算,加載,比較等操作
__u8 jt; //如果比對跳轉到哪裡
__u8 jf; //如果不比對跳轉到哪裡
__u32 k; //參數字段,對于不同的操作碼有不同的用途。比如在操作碼是比較時存放比較鍵,操作碼為加載時存放載入資料在資料包(鍊路幀/資料報)的偏移
}
比對序列很像一個彙程式設計式,有其自身的操作碼,操作數以及分支跳轉功能,于是這段比對序列的執行過程自然就類似一個馮諾依曼機器上單程序的執行緒了,它的本質從執行上講是一個狀态機(從資料角度講,程序又是一個過濾器,它的名字恰就是過濾器...),很顯然其實作應該是一個狀态驅動的循環:
while(序列中還有比對){
switch(目前操作碼)
case 加減乘除:
...
case 加載:
載入目前比對項的k值便宜的資料,設為d
下一個比對項
case 比較跳轉:
程式計數器 += 比較結果?目前比對項的jt字段:jf字段
...
看看linux實作的代碼,它基本就是這麼實作的:
int sk_run_filter(struct sk_buff *skb, struct sock_filter *filter, int flen)
...//定義中間變量,儲存臨時計算結果
int k;
int pc; //程式計數器,用于分支跳轉
for (pc = 0; pc < flen; pc++) {
fentry = &filter[pc];
switch (fentry->code) {
case BPF_ALU|BPF_ADD|BPF_X:
A += X;
continue;
...//類似實作減法,乘法,除法,取反,與,或..等操作
case BPF_JMP|BPF_JA: //涉及分支跳轉
pc += fentry->k;
case BPF_JMP|BPF_JGT|BPF_K: //大于
pc += (A > fentry->k) ? fentry->jt : fentry->jf;
...//類似實作小于等于等比較操作,然後分支跳轉
load_w: //加載操作,類似x86彙編中的mov,這些load操作也是要區分大小的,比如是load一個字還是雙字,還是位元組...
if (k >= 0 && (unsigned int)(k+sizeof(u32)) <= len) {
A = ntohl(*(u32*)&data[k]);
continue;
}
...
BPF用于很多抓包程式,在linux中,一般核心自動編譯進了af_packet這個驅動,是以隻需要在使用者态配制一個PACKET類型的socket,然後将filter配制進核心即可--使用setsockopt的SO_ATTACH_FILTER指令,這個filter是在使用者空間配制的,比如tcpdump應用程式,tcpdump和核心BPF過濾器的關系類似iptables和netfilter的關系,隻是netfilter實作了match/target的複雜配合,而BPF的target僅僅是“該資料包要”和“該資料包不要”。當在使用者态配制
tcpdump -i eth0 host 1.2.3.4 ...
的時候,實際上進入核心的filter就是以下的序列,每個{}中的都是一個socket_filter:
...
n: {加載,0,0,源ip位址在以太幀中的偏移},
n+1: {比較跳轉,n+3,n+2,"1.2.3.4"},
n+2: {加載,0,0,目标ip位址在以太幀中的偏移},
n+3: {比較跳轉,n+4,n+m,"1.2.3.4"},
n+4: {...},
n+m: {傳回...}
然後當有資料包進來的時候,由于tcpdump的socket事先注冊進了ptype_all這個list,那麼資料包将會複制一份給了tcpdump的socket,然後在其packet_type的func函數中調用run_filter來進行資料包過濾,确定到底需不需要将這個包交給tcpdump。
在windows中,由于其羸弱的網絡處理能力以及過渡的分層,或者說為了創立業界标準而導緻過度接口化的實作,其核心并沒有直接包含BPF,需要一個NDIS過濾驅動來實作,這個實作起來也是蠻簡單的,很子產品化的。在上面蓋一個類似libpcap的接口,這樣就可以實作ethereal了。不管在什麼作業系統上,如果能将這種僞彙編指令及時編譯成機器指令,利用馮諾依曼機器cpu狀态機的本質來代替軟體函數--比如sk_run_filter,那性能将會有很大的提升。
最後看看BPF的設計理念用于硬體驅動程式的情形,首先定義一個結構體,類似linux的BPF中的socket_filter,但是更加緊湊備援了,實際上沒有必要實作這麼多的字段,不過那樣的話driver函數就要更複雜了,總之理念一緻即可:
struct sequence_item {
int opt; //操作碼:讀/寫/加減乘除,取反...
int data; //操作數
int port; //第二操作數,可以為端口
int flag; //标志,可存儲是否使用中間結果
char reverse[0] //預留
};
int driver(struct sequence_item *sequence, unsigned int len)
int i = 0;
int result = -1;
struct sequence_item si;
for (; i < len; i++) {
si = sequence[i];
if (si.opt == 0) {
outb_p(si.flag?result:si.data, si.port);
} else if (si.opt == 1){
result = inb_p(si.port);
} else {
switch (si.opt) {
case '~':
result ~= si.data;
break;
case '^':
result ^= si.data;
...
}
}
return 1;
[PS]:這個代碼是從很早之前(3 years ago)我寫的一個驅動程式中抽出來的,所使用的思想竟然和BPF(2 years ago)的一緻。
本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1271741