wpcap过滤器后端代码解读

  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 = 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值