wpcap是windows平台下著名的数据包嗅探库,在linux下叫libpcap,数据包嗅探软件wireshark就是用来这个库,其中实现了十分高效的数据包过滤算法。很久以前就想看一下他具体的过滤机制,不过那时候没看懂。这两天看到一篇文章提到了winpcap的中间码是SSA格式的,所以又提起兴趣看了一下,发现很多地方都已经能看懂了。(好像新下的源码比以前多了很多注释?很多源码可能维护者也没看懂,注释里都打了问号。)
wpcap对过滤表达式的编译应该说是十分的简洁和清晰,而且中间码也是麻雀虽小五脏俱全,后端优化覆盖了几个基本的优化,个人感觉完全可以当做编译原理课程教科书式的范例代码(只是没有循环,所以和循环相关的算法一个都没有)。
过滤器编译的实现分为三个部分,全都在pcap_compile(gencode.c)函数中完成。
1,解析阶段。
解析阶段将用户传过来的过滤表达式解析为icode(intermediate code?我也不知道叫什么)。实现上比较偷懒直接用了flex和bison,规则由根目录下的scanner.l和grammar.y提供。由规则制导翻译成一系列的block组成的cfg。
由于过滤表达式特殊的性质,这个cfg有一些不一样的性质:
- 由于语法中没有能够产生循环语句,这个cfg是没有环的,也就是说整个cfg其实就是一个dag,由于这个性质,很多分析过程会简单许多。
- 对于branch指令,pcap中是直接和block绑定在一起的,其余的指令则是挂在block结构的stmts字段里的。
- 由于没有循环语句,自然也不存在backedge导致的block split,所以除了叶节点之外的所有基本块都有两个分支。
- 内存操作只有load没有store,因为不可能去修改数据包吧。
- 因为过滤实际上只关注结果,而不关注过程,所以后端不同于常规的编译器,对于结果和副作用相同的两个指令,即使具体指令不同,也可以合并成一个代码。
因为解析主要由flex和bison实现,具体实现这里不再多做阐述。
2,优化阶段。
优化在bpf_optimize中实现,其实我仔细看了下,icode应该不是SSA格式的,一个使用仍然可能会对应多个定值,而且在分支汇合处并没有phi指令的计算。
首先解释一些相关的结构体,不然后面可能会比较疑惑。首先是指令:
struct stmt {
// 指令码
int code;
// 如果是跳转指令,这里是true分支
struct slist *jt; /*only for relative jump in block*/
// 如果是跳转指令,这里是false分支
struct slist *jf; /*only for relative jump in block*/
// 操作数
bpf_u_int32 k;
};
可以看到,显式的操作数只有一个k,其余的都是隐式操作数,也就是说除了分支指令,这是一个一地址码。
边的结构:
struct edge {
// 边ID,用于边映射
int id;
// 边指令,用于分支优化
// 其实就是基本块的指令,但这里添加了正负号用于区分真分之和假分支
int code;
// 支配边比特向量
uset edom;
// 两头的基本块
struct block *succ;
struct block *pred;
// 入边兄弟结点间的链接
struct edge *next; /* link list of incoming edges for a node */
};
基本块的结构:
struct block {
// 块ID,用于块映射
int id;
// 非分支指令序列
struct slist *stmts; /* side effect stmts */
// 分支指令
struct stmt s; /* branch stmt */
int mark;
u_int longjt; /* jt branch requires long jump */
u_int longjf; /* jf branch requires long jump */
// 在dag的底基层
int level;
// 在指令序列的偏移,用于代码转换
int offset;
// 分支语义
int sense;
// 真分支
struct edge et;
// 假分支
struct edge ef;
// 用于拉链回填
struct block *head;
// 由于层级链表
struct block *link; /* link field used by optimizer */
// 支配结点比特向量
uset dom;
// 传递闭包比特向量
uset closure;
// 入边
struct edge *in_edges;
// def和kill比特向量
atomset def, kill;
// 活跃信息比特向量
atomset in_use;
atomset out_use;
// 分支的value
int oval;
// 所有寄存器的value
bpf_u_int32 val[N_ATOMS];
};
在bpf_optimize中,第一步调用了opt_init执行内存初始化。首先计算所有基本块的数量,并给每个基本块分配一个id,然后申请了一个数组,建立id到基本块的映射。
/*
* First, count the blocks, so we can malloc an array to map
* block number to block. Then, put the blocks into the array.
*/
unMarkAll(ic);
// 计算所有块的数量
n = count_blocks(ic, ic->root);
opt_state->blocks = (struct block **)calloc(n, sizeof(*opt_state->blocks));
...
unMarkAll(ic);
opt_state->n_blocks = 0;
// 给基本块分配id,并通过opt_state->blocks数组来建立id到基本块的映射。
number_blks_r(opt_state, ic, ic->root);
给基本块之间的边建立同样的映射,前面说过所有的基本块都有两个分支,所以这里基本块×2就行:
opt_state->n_edges = 2 * opt_state->n_blocks;
opt_state->edges = (struct edge **)calloc(opt_state->n_edges, sizeof(*opt_state->edges));
然后建立dag的层链表数组:
/*
* The number of levels is bounded by the number of nodes.
*/
opt_state->levels = (struct block **)calloc(opt_state->n_blocks, sizeof(*opt_state->levels));
计算边的比特向量字数和块的比特向量字数,后面分配比特向量时就依照这个值来分配。
opt_state->edgewords = opt_state->n_edges / (8 * sizeof(bpf_u_int32)) + 1;
opt_state->nodewords = opt_state->n_blocks / (8 * sizeof(bpf_u_int32)) + 1;
分配所有需要用到的比特向量:
/* XXX */
// 这里将所有后面要用到的比特向量全部分配到一个内存中
opt_state->space = (bpf_u_int32 *)malloc(2 * opt_state->n_blocks * opt_state->nodewords * sizeof(*opt_state->space)
+ opt_state->n_edges * opt_state->edgewords * sizeof(*opt_state->space));
...
p = opt_state->space;
opt_state->all_dom_sets = p;
// 支配节点比特向量
for (i =