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;
continue;
case BPF_JMP|BPF_JGT|BPF_K: //大於
pc += (A > fentry->k) ? fentry->jt : fentry->jf;
continue;
...//類似實現小於等於等比較操作,然後分支跳轉
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;
break;
...
}
}
}
return 1;
}
[PS]:這個代碼是從很早之前(3 years ago)我寫的一個驅動程式中抽出來的,所使用的思想竟然和BPF(2 years ago)的一致。