kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

Linux内核kprobe机制

  • 探测点处理函数在运行时是失效抢占的,依赖于特定的架构,探测点处理函数运行时也可能是中断失效的。
  • 因此,对于任何探测点处理函数,不要使用导致睡眠或进程调度的任何内核函数(如尝试获得semaphore)。

    Kprobe机制是内核提供的一种调试机制,它提供了一种方法,能够在不修改现有代码的基础上,灵活的跟踪内核函数的执行。它的基本工作原理是:用户指定一个探测点,并把一个用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。

    Kprobe提供了三种形式的探测点,一种是最基本的kprobe,能够在指定代码执行前、执行后进行探测,但此时不能访问被探测函数内的相关变量信 息;一种是jprobe,用于探测某一函数的入口,并且能够访问对应的函数参数;一种是kretprobe,用于完成指定函数返回值的探测功能。其中最基 本的就是kprobe机制,jprobe以及kretprobe的实现都依赖于kprobe,但其代码的实现都很巧妙,强烈建议每一个内核爱好者阅读。

代码:

首先是struct kprobe结构,每一个探测点的基本结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
structkprobe {
	/*用于保存kprobe的全局hash表,以被探测的addr为key*/
	structhlist_node hlist;

	/* list of kprobes for multi-handler support */
	/*当对同一个探测点存在多个探测函数时,所有的函数挂在这条链上*/
	structlist_head list;

	/*count the number of times this probe was temporarily disarmed */
	unsigned longnmissed;

	/* location of the probe point */
	/*被探测的目标地址*/
	kprobe_opcode_t *addr;

	/* Allow user to indicate symbol name of the probe point */
	/*symblo_name的存在,允许用户指定函数名而非确定的地址*/
	constchar*symbol_name;

	/* Offset into the symbol */
	/*如果被探测点为函数内部某个指令,需要使用addr + offset的方式*/
	unsigned intoffset;

	/* Called before addr is executed. */
	/*探测函数,在目标探测点执行之前调用*/
	kprobe_pre_handler_t pre_handler;

	/* Called after addr is executed, unless... */
	/*探测函数,在目标探测点执行之后调用*/
	kprobe_post_handler_t post_handler;

	/*
	 * ... called if executing addr causes a fault (eg. page fault).
	 * Return 1 if it handled fault, otherwise kernel will see it.
	 */
	kprobe_fault_handler_t fault_handler;

	/*
	 * ... called if breakpoint trap occurs in probe handler.
	 * Return 1 if it handled break, otherwise kernel will see it.
	 */
	kprobe_break_handler_t break_handler;

	/*opcode 以及 ainsn 用于保存被替换的指令码*/

	/* Saved opcode (which has been replaced with breakpoint) */
	kprobe_opcode_t opcode;

	/* copy of the original instruction */
	structarch_specific_insn ainsn;

	/*
	 * Indicates various status flags.
	 * Protected by kprobe_mutex after this kprobe is registered.
	 */
	u32 flags;
};

对于kprobe功能的实现主要利用了内核中的两个功能特性:异常(尤其是int 3),单步执行(EFLAGS中的TF标志)。

大概的流程:
1
2
3
4
5
6
 1)在注册探测点的时候,对被探测函数的指令码进行替换,替换为int 3的指令码;
 2)在执行int 3的异常执行中,通过通知链的方式调用kprobe的异常处理函数;
 3)在kprobe的异常出来函数中,判断是否存在pre_handler钩子,存在则执行;
 4)执行完后,准备进入单步调试,通过设置EFLAGS中的TF标志位,并且把异常返回的地址修改为保存的原指令码;
 5)代码返回,执行原有指令,执行结束后触发单步异常;
 6)在单步异常的处理中,清除单步标志,执行post_handler流程,并最终返回;

下面又进入代码时间,首先看一下kprobe模块的初始化代码,初始化代码主要做了两件事:标记出哪些代码是不能被探测的,这些代码属于kprobe实现的关键代码;注册通知链到die_notifier,用于接收异常通知。

初始化代码位于kernel/kprobes.c中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
staticint__init init_kprobes(void)
{
	inti,err =0;
		....

	 /*kprobe_blacklist中保存的是kprobe实现的关键代码路径,这些函数不应该被kprobe探测*/
	/*
	 * Lookup and populate the kprobe_blacklist.
	 *
	 * Unlike the kretprobe blacklist, we'll need to determine
	 * the range of addresses that belong to the said functions,
	 * since a kprobe need not necessarily be at the beginning
	 * of a function.
	 */
	for(kb =kprobe_blacklist;kb->name!=NULL;kb++){
		kprobe_lookup_name(kb->name,addr);
		if(!addr)
			continue;

		kb->start_addr =(unsigned long)addr;
		symbol_name =kallsyms_lookup(kb->start_addr,
				&size,&offset,&modname,namebuf);
		if(!symbol_name)
			kb->range =0;
		else
			kb->range =size;
	}
		....
	if(!err)
		/*注册通知链到die_notifier,用于接收int 3的异常信息*/
		err =register_die_notifier(&kprobe_exceptions_nb);
		 ....
}
其中的通知链:
1
2
3
4
5
staticstructnotifier_block kprobe_exceptions_nb ={
	.notifier_call =kprobe_exceptions_notify,
	/*优先级最高,保证最先执行*/
	.priority =0x7fffffff /* we need to be notified first */
};
kprobe的注册流程register_kprobe。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
int__kprobes register_kprobe(structkprobe *p)
{
	intret =0;
	structkprobe *old_p;
	structmodule *probed_mod;
	kprobe_opcode_t *addr;

	/*获取被探测点的地址,指定了symbol_name,则从kallsyms中获取;指定了offset,则返回addr + offset*/
	addr =kprobe_addr(p);
	if(!addr)
		return-EINVAL;
	p->addr =addr;

	/*判断同一个kprobe是否被重复注册*/
	ret =check_kprobe_rereg(p);
	if(ret)
		returnret;

	jump_label_lock();
	preempt_disable();
	/*判断被注册的函数是否位于内核的代码段内,或位于不能探测的kprobe实现路径中*/
	if(!kernel_text_address((unsigned long)p->addr)||
		in_kprobes_functions((unsigned long)p->addr)||
		ftrace_text_reserved(p->addr,p->addr)||
		jump_label_text_reserved(p->addr,p->addr))
		gotofail_with_jump_label;

	/* User can pass only KPROBE_FLAG_DISABLED to register_kprobe */
	p->flags&=KPROBE_FLAG_DISABLED;

	/*
	 * Check if are we probing a module.
	 */
	/*判断被探测的地址是否属于某一个模块,并且位于模块的text section内*/
	probed_mod =__module_text_address((unsigned long)p->addr);
	if(probed_mod){
		/*如果被探测的为模块地址,首先要增加模块的引用计数*/
		/*
		 * We must hold a refcount of the probed module while updating
		 * its code to prohibit unexpected unloading.
		 */
		if(unlikely(!try_module_get(probed_mod)))
			gotofail_with_jump_label;

		/*
		 * If the module freed .init.text, we couldn't insert
		 * kprobes in there.
		 */
		/*如果被探测的地址位于模块的init地址段内,但该段代码区间已被释放,则直接退出*/
		if(within_module_init((unsigned long)p->addr,probed_mod)&&
			probed_mod->state!=MODULE_STATE_COMING){
			module_put(probed_mod);
			gotofail_with_jump_label;
		}
	}
	preempt_enable();
	jump_label_unlock();

	p->nmissed =0;
	INIT_LIST_HEAD(&p->list);
	mutex_lock(&kprobe_mutex);

	jump_label_lock();/* needed to call jump_label_text_reserved() */

	get_online_cpus();    /* For avoiding text_mutex deadlock. */
	mutex_lock(&text_mutex);

	/*判断在同一个探测点是否已经注册了其他的探测函数*/
	old_p =get_kprobe(p->addr);
	if(old_p){
		/* Since this may unoptimize old_p, locking text_mutex. */
		/*如果已经存在注册过的kprobe,则将探测点的函数修改为aggr_pre_handler,并将所有的handler挂载到其链表上,由其负责所有handler函数的执行*/
		ret =register_aggr_kprobe(old_p,p);
		gotoout;
	}

	/* 分配特定的内存地址用于保存原有的指令
	 * 按照内核注释,被分配的地址必须must be on special executable page on x86.
	 * 该地址被保存在kprobe->ainsn.insn
	 */
	ret =arch_prepare_kprobe(p);
	if(ret)
		gotoout;

	/*将kprobe加入到相应的hash表内*/
	INIT_HLIST_NODE(&p->hlist);
	hlist_add_head_rcu(&p->hlist,
			   &kprobe_table[hash_ptr(p->addr,KPROBE_HASH_BITS)]);

	if(!kprobes_all_disarmed &&!kprobe_disabled(p))
/*将探测点的指令码修改为int 3指令*/
		__arm_kprobe(p);

	/* Try to optimize kprobe */
	try_to_optimize_kprobe(p);

out:
	mutex_unlock(&text_mutex);
	put_online_cpus();
	jump_label_unlock();
	mutex_unlock(&kprobe_mutex);

	if(probed_mod)
		module_put(probed_mod);

	returnret;

fail_with_jump_label:
	preempt_enable();
	jump_label_unlock();
	return-EINVAL;
注册完毕,就开始kprobe的执行流程了。对于该探测点,由于其起始指令已经被修改为int3,因此在执行到该地址时,必然会触发3号中断向量的处理流程do_int3.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* May run on IST stack. */
dotraplinkage void__kprobes do_int3(structpt_regs *regs,longerror_code)
{
#ifdef CONFIG_KGDB_LOW_LEVEL_TRAP
	if(kgdb_ll_trap(DIE_INT3,"int3",regs,error_code,3,SIGTRAP)
			==NOTIFY_STOP)
		return;
#endif /* CONFIG_KGDB_LOW_LEVEL_TRAP */
#ifdef CONFIG_KPROBES
	/*在这里以DIE_INT3,通知kprobe注册的通知链*/
	if(notify_die(DIE_INT3,"int3",regs,error_code,3,SIGTRAP)
			==NOTIFY_STOP)
		return;
#else
	if(notify_die(DIE_TRAP,"int3",regs,error_code,3,SIGTRAP)
			==NOTIFY_STOP)
		return;
#endif

	preempt_conditional_sti(regs);
	do_trap(3,SIGTRAP,"int3",regs,error_code,NULL);
	preempt_conditional_cli(regs);
}
在do_int3中触发kprobe注册的通知链函数,kprobe_exceptions_notify。由于kprobe以及jprobe等机制的处 理核心都在此函数内,这里只针对kprobe的流程进行分析:进入函数的原因是DIE_INT3,并且是第一次进入该函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
int__kprobes kprobe_exceptions_notify(structnotifier_block *self,
					   unsigned longval,void*data)
{
	structdie_args *args =data;
	intret =NOTIFY_DONE;

	if(args->regs &&user_mode_vm(args->regs))
		returnret;

	switch(val){
	caseDIE_INT3:
/*对于kprobe,进入kprobe_handle*/
		if(kprobe_handler(args->regs))
			ret =NOTIFY_STOP;
		break;
	caseDIE_DEBUG:
		if(post_kprobe_handler(args->regs)){
			/*
			 * Reset the BS bit in dr6 (pointed by args->err) to
			 * denote completion of processing
			 */
			(*(unsigned long*)ERR_PTR(args->err))&=~DR_STEP;
			ret =NOTIFY_STOP;
		}
		break;
	caseDIE_GPF:
		/*
		 * To be potentially processing a kprobe fault and to
		 * trust the result from kprobe_running(), we have
		 * be non-preemptible.
		 */
		if(!preemptible()&&kprobe_running()&&
			kprobe_fault_handler(args->regs,args->trapnr))
			ret =NOTIFY_STOP;
		break;
	default:
		break;
	}
	returnret;
}

staticint__kprobes kprobe_handler(structpt_regs *regs)
{
	kprobe_opcode_t *addr;
	structkprobe *p;
	structkprobe_ctlblk *kcb;

	/*对于int 3中断,其被Intel定义为Trap,那么异常发生时EIP寄存器内指向的为异常指令的后一条指令*/
	addr =(kprobe_opcode_t *)(regs->ip -sizeof(kprobe_opcode_t));
	/*
	 * We don't want to be preempted for the entire
	 * duration of kprobe processing. We conditionally
	 * re-enable preemption at the end of this function,
	 * and also in reenter_kprobe() and setup_singlestep().
	 */
	preempt_disable();

	kcb =get_kprobe_ctlblk();
	/*获取addr对应的kprobe*/
	p =get_kprobe(addr);

	if(p){
/*如果异常的进入是由kprobe导致,则进入reenter_kprobe(jprobe需要,到时候分析)*/
		if(kprobe_running()){
			if(reenter_kprobe(p,regs,kcb))
				return1;
		}else{
			set_current_kprobe(p,regs,kcb);
			kcb->kprobe_status =KPROBE_HIT_ACTIVE;

			/*
			 * If we have no pre-handler or it returned 0, we
			 * continue with normal processing.  If we have a
			 * pre-handler and it returned non-zero, it prepped
			 * for calling the break_handler below on re-entry
			 * for jprobe processing, so get out doing nothing
			 * more here.
			 */
	/*执行在此地址上挂载的pre_handle函数*/
			if(!p->pre_handler ||!p->pre_handler(p,regs))
/*设置单步调试模式,为post_handle函数的执行做准备*/
				setup_singlestep(p,regs,kcb,0);
			return1;
		}
	}elseif(*addr !=BREAKPOINT_INSTRUCTION){
		/*
		 * The breakpoint instruction was removed right
		 * after we hit it.  Another cpu has removed
		 * either a probepoint or a debugger breakpoint
		 * at this address.  In either case, no further
		 * handling of this interrupt is appropriate.
		 * Back up over the (now missing) int3 and run
		 * the original instruction.
		 */
		regs->ip =(unsigned long)addr;
		preempt_enable_no_resched();
		return1;
	}elseif(kprobe_running()){
		p =__this_cpu_read(current_kprobe);
		if(p->break_handler &&p->break_handler(p,regs)){
			setup_singlestep(p,regs,kcb,0);
			return1;
		}
	}/* else: not a kprobe fault; let the kernel handle it */

	preempt_enable_no_resched();
	return0;
}

staticvoid__kprobes setup_singlestep(structkprobe *p,structpt_regs *regs,
					   structkprobe_ctlblk *kcb,intreenter)
{
	if(setup_detour_execution(p,regs,reenter))
		return;

#if!defined(CONFIG_PREEMPT)
	if(p->ainsn.boostable ==1 &&!p->post_handler){
		/* Boost up -- we can execute copied instructions directly */
		if(!reenter)
			reset_current_kprobe();
		/*
		 * Reentering boosted probe doesn't reset current_kprobe,
		 * nor set current_kprobe, because it doesn't use single
		 * stepping.
		 */
		regs->ip =(unsigned long)p->ainsn.insn;
		preempt_enable_no_resched();
		return;
	}
#endif
	/*jprobe*/
	if(reenter){
		save_previous_kprobe(kcb);
		set_current_kprobe(p,regs,kcb);
		kcb->kprobe_status =KPROBE_REENTER;
	}else
		kcb->kprobe_status =KPROBE_HIT_SS;
	/* Prepare real single stepping */
	/*准备单步模式,设置EFLAGS的TF标志位,清楚IF标志位(禁止中断)*/
	clear_btf();
	regs->flags|=X86_EFLAGS_TF;
	regs->flags&=~X86_EFLAGS_IF;
	/* single step inline if the instruction is an int3 */
	if(p->opcode ==BREAKPOINT_INSTRUCTION)
		regs->ip =(unsigned long)p->addr;
	else
/*设置异常返回的指令为保存的被探测点的指令*/
		regs->ip =(unsigned long)p->ainsn.insn;
}
对应kprobe,pre_handle的执行就结束了,按照代码,程序开始执行保存的被探测点的指令,由于开启了单步调试模式,执行完指令后会继续触发异常,这次的是do_debug异常处理流程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
dotraplinkage void__kprobes do_debug(structpt_regs *regs,longerror_code)
{
	....

	/*在do_debug中,以DIE_DEBUG再一次触发kprobe的通知链*/
	if(notify_die(DIE_DEBUG,"debug",regs,PTR_ERR(&dr6),error_code,
							SIGTRAP)==NOTIFY_STOP)
		return;
   
	....
	return;
}

/*对于kprobe_exceptions_notify,其DIE_DEBUG处理流程*/
caseDIE_DEBUG:
		if(post_kprobe_handler(args->regs)){
			/*
			 * Reset the BS bit in dr6 (pointed by args->err) to
			 * denote completion of processing
			 */
			(*(unsigned long*)ERR_PTR(args->err))&=~DR_STEP;
			ret =NOTIFY_STOP;
		}
		break;

staticint__kprobes post_kprobe_handler(structpt_regs *regs)
{
	structkprobe *cur =kprobe_running();
	structkprobe_ctlblk *kcb =get_kprobe_ctlblk();

	if(!cur)
		return0;

	/*设置异常返回的EIP为下一条需要执行的指令*/
	resume_execution(cur,regs,kcb);
	/*恢复异常执行前的EFLAGS*/
	regs->flags|=kcb->kprobe_saved_flags;

	/*执行post_handler函数*/
	if((kcb->kprobe_status !=KPROBE_REENTER)&&cur->post_handler){
		kcb->kprobe_status =KPROBE_HIT_SSDONE;
		cur->post_handler(cur,regs,0);
	}

	/* Restore back the original saved kprobes variables and continue. */
	if(kcb->kprobe_status ==KPROBE_REENTER){
		restore_previous_kprobe(kcb);
		gotoout;
	}
	reset_current_kprobe();
out:
	preempt_enable_no_resched();

	/*
	 * if somebody else is singlestepping across a probe point, flags
	 * will have TF set, in which case, continue the remaining processing
	 * of do_debug, as if this is not a probe hit.
	 */
	if(regs->flags&X86_EFLAGS_TF)
		return0;

	return1;
}

至此,一个典型的kprobe的流程已经执行完毕了。

addr2line命令

这是一个示例程序,func函数返回参数a除以参数b的结果。这里使用0作为除数,结果就是程序因为除以0导致错误,直接中断了。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int func(int a, int b)
{
	return a / b;
}

int main()
{
	int x = 10;
	int y = 0;
	printf("%d / %d = %d\n", x, y, func(x, y));
	return 0;
}
1
$ gcc -o test1 -g test1.c  

编译程序,test1.c是程序文件名。执行程序,结果程序异常中断。查看系统dmesg信息,发现系统日志的错误信息:

1
[54106.016179] test1[8352] trap divide error ip:400506 sp:7fff2add87e0 error:0 in test1[400000+1000]

这条信息里的ip字段后面的数字就是test1程序出错时所程序执行的位置。使用addr2line就可以将400506转换成出错程序的位置:

1
2
$ addr2line -e test1 400506  
/home/hanfoo/code/test/addr2line/test1.c:5

这里的test1.c:5指的就是test1.c的第5行

1
return a / b;  

也正是这里出现的错误。addr2line帮助我们解决了问题。

addr2line如何找到的这一行呢。在可执行程序中都包含有调试信息, 其中很重要的一份数据就是程序源程序的行号和编译后的机器代码之间的对应关系Line Number Table。DWARF格式的Line Number Table是一种高度压缩的数据,存储的是表格前后两行的差值,在解析调试信息时,需要按照规则在内存里重建Line Number Table才能使用。

Line Number Table存储在可执行程序的.debug_line域,使用命令

1
$ readelf -w test1

可以输出DWARF的调试信息,其中有两行

1
2
Special opcode 146: advance Address by 10 to 0x4004fe and Line by 1 to 5  
Special opcode 160: advance Address by 11 to 0x400509 and Line by 1 to 6  

这里说明机器二进制编码的0x4004fe位置开始,对应于源码中的第5行,0x400509开始就对应与源码的第6行了,所以400506这个地址对应的是源码第5行位置。

addr2line通过分析调试信息中的Line Number Table自动就能把源码中的出错位置找出来.

抓包命令tcpdump

例:tcpdump host 172.16.29.40 and port 4600 -X -s 500

tcpdump采用命令行方式,它的命令格式为:

1
2
3
  tcpdump [ -adeflnNOpqStvx ] [ -c 数量 ] [ -F 文件名 ]
      [ -i 网络接口 ] [ -r 文件名] [ -s snaplen ]
      [ -T 类型 ] [ -w 文件名 ] [表达式 ]

1. tcpdump的选项介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	-a       将网络地址和广播地址转变成名字;
	-d        将匹配信息包的代码以人们能够理解的汇编格式给出;
	-dd       将匹配信息包的代码以c语言程序段的格式给出;
	-ddd  将匹配信息包的代码以十进制的形式给出;
	-e        在输出行打印出数据链路层的头部信息;
	-f        将外部的Internet地址以数字的形式打印出来;
	-l        使标准输出变为缓冲行形式;
	-n        不把网络地址转换成名字;
	-t        在输出的每一行不打印时间戳;
	-v        输出一个稍微详细的信息,例如在ip包中可以包括ttl和服务类型的信息;
	-vv       输出详细的报文信息;
	-c        在收到指定的包的数目后,tcpdump就会停止;
	-F        从指定的文件中读取表达式,忽略其它的表达式;
	-i        指定监听的网络接口;
	-r        从指定的文件中读取包(这些包一般通过-w选项产生);
	-w        直接将包写入文件中,并不分析和打印出来;
	-T        将监听到的包直接解释为指定的类型的报文,常见的类型有rpc (远程过程调用)和snmp(简单网络管理协议)

2. tcpdump的表达式介绍

表达式是一个正则表达式,tcpdump利用它作为过滤报文的条件,如果一个报文满足表 达式的条件,则这个报文将会被捕获。如果没有给出任何条件,则网络上所有的信息包将会 被截获。

在表达式中一般如下几种类型的关键字,一种是关于类型的关键字,主要包括host, net,port, 例如 host 210.27.48.2,指明 210.27.48.2是一台主机,net 202.0.0.0 指明 202.0.0.0是一个网络地址,port 23 指明端口号是23。如果没有指定类型,缺省的类型是 host.

第二种是确定传输方向的关键字,主要包括src , dst ,dst or src, dst and src , 这些关键字指明了传输的方向。举例说明,src 210.27.48.2 ,指明ip包中源地址是210.27. 48.2 , dst net 202.0.0.0 指明目的网络地址是202.0.0.0 。如果没有指明方向关键字,则 缺省是src or dst关键字。

第三种是协议的关键字,主要包括fddi,ip ,arp,rarp,tcp,udp等类型。Fddi指明是在 FDDI(分布式光纤数据接口网络)上的特定的网络协议,实际上它是"ether"的别名,fddi和e ther具有类似的源地址和目的地址,所以可以将fddi协议包当作ether的包进行处理和分析。 其他的几个关键字就是指明了监听的包的协议内容。如果没有指定任何协议,则tcpdump将会 监听所有协议的信息包。

除了这三种类型的关键字之外,其他重要的关键字如下:gateway, broadcast,less, greater,还有三种逻辑运算,取非运算是 ‘not ’ ‘! ’, 与运算是'and',‘&&’;或运算 是'o r' ,‘||';

这些关键字可以组合起来构成强大的组合条件来满足人们的需要,下面举几个例子来 说明。
(1)想要截获所有210.27.48.1 的主机收到的和发出的所有的数据包:
    #tcpdump host 210.27.48.1
(2) 想要截获主机210.27.48.1 和主机210.27.48.2 或210.27.48.3的通信:
    #tcpdump host 210.27.48.1 and ( 210.27.48.2 or 210.27.48.3 )
(3) 如果想要获取主机210.27.48.1除了和主机210.27.48.2之外所有主机通信的ip包:
    #tcpdump ip host 210.27.48.1 and ! 210.27.48.2
(4)如果想要获取主机210.27.48.1接收或发出的telnet包,使用如下命令:
    #tcpdump tcp port 23 host 210.27.48.1

3. tcpdump 的输出结果介绍

下面我们介绍几种典型的tcpdump命令的输出信息

(1) 数据链路层头信息

使用命令#tcpdump –e host ice
ice 是一台装有linux的主机,她的MAC地址是0:90:27:58:AF:1A H219是一台装有SOLARIC的SUN工作站,它的MAC地址是8:0:20:79:5B:46;上一条
命令的输出结果如下所示:
21:50:12.847509 eth0 < 8:0:20:79:5b:46 0:90:27:58:af:1a ip 60: h219.33357 > ice.
telne
t 0:0(0) ack 22535 win 8760 (DF)
分析:21:50:12是显示的时间, 847509是ID号,eth0 <表示从网络接口eth0 接受该
数据包,eth0 >表示从网络接口设备发送数据包, 8:0:20:79:5b:46是主机H219的MAC地址,它
表明是从源地址H219发来的数据包. 0:90:27:58:af:1a是主机ICE的MAC地址,表示该数据包的
目的地址是ICE . ip 是表明该数据包是IP数据包,60 是数据包的长度, h219.33357 > ice.
telnet 表明该数据包是从主机H219的33357端口发往主机ICE的TELNET(23)端口. ack 22535
表明对序列号是222535的包进行响应. win 8760表明发送窗口的大小是8760.

(2) ARP包的TCPDUMP输出信息

使用命令#tcpdump arp 得到的输出结果是:
22:32:42.802509 eth0 > arp who-has route tell ice (0:90:27:58:af:1a)
  22:32:42.802902 eth0 < arp reply route is-at 0:90:27:12:10:66 (0:90:27:58:af:1a)
分析: 22:32:42是时间戳, 802509是ID号, eth0 >表明从主机发出该数据包, arp表明是
ARP请求包, who-has route tell ice表明是主机ICE请求主机ROUTE的MAC地址。 0:90:27:58:af:1a是主机ICE的MAC地址。

(3) TCP包的输出信息

用TCPDUMP捕获的TCP包的一般输出信息是: src > dst: flags data-seqno ack window urgent options
src > dst:表明从源地址到目的地址, flags是TCP包中的标志信息,S 是SYN标志, F (FIN), P (PUSH) , R (RST) “.” (没有标记); data-seqno是数据包中的数据的顺序号, ack是下次期望的顺序号, window是接收缓存的窗口大小, urgent表明数据包中是否有紧急指针. Options是选项.

(4) UDP包的输出信息

用TCPDUMP捕获的UDP包的一般输出信息是:
route.port1 > ice.port2: udp lenth
UDP十分简单,上面的输出行表明从主机ROUTE的port1端口发出的一个UDP数据包到主机ICE的port2端口,类型是UDP, 包的长度是lenth

获取Linux内核未导出符号

从Linux内核的2.6某个版本开始,内核引入了导出符号的机制。只有在内核中使用EXPORT_SYMBOL或EXPORT_SYMBOL_GPL导出的符号才能在内核模块中直接使用。

然而,内核并没有导出所有的符号。例如,在3.8.0的内核中,do_page_fault就没有被导出。

而我的内核模块中需要使用do_page_fault,那么有那些方法呢?这些方法分别有什么优劣呢?

下面以do_page_fault为例,一一进行分析:
修改内核,添加EXPORT_SYMBOL(do_page_fault)或EXPORT_SYMBOL_GPL(do_page_fault)。
这种方法适用于可以修改内核的情形。在可以修改内核的情况下,这是最简单的方式。

使用kallsyms_lookup_name读取

kallsyms_lookup_name本身也是一个内核符号,如果这个符号被导出了,那么就可以在内核模块中调用kallsyms_lookup_name(“do_page_fault”)来获得do_page_fault的符号地址。
这种方法的局限性在于kallsyms_lookup_name本身不一定被导出。

读取/boot/System.map-,再使用内核模块参数传入内核模块

System.map-是编译内核时产生的,它里面记录了编译时内核符号的地址。如果能够保证当前使用的内核与 System.map-是一一对应的,那么从System.map-中读出的符号地址就是正确的。其中,kernel-version可以通过'uname -r'获得。
但是这种方法也有局限性,在模块运行的时候,System.map-文件不一定存在,即使存在也不能保证与当前内核是正确对应的。

读取/proc/kallsyms,再使用内核模块参数传入内核模块

/proc/kallsyms是一个特殊的文件,它并不是存储在磁盘上的文件。这个文件只有被读取的时候,才会由内核产生内容。因为这些内容是内核动态生成的,所以 可以保证其中读到的地址是正确的,不会有System.map-的问题。
需要注意的是,从内核 2.6.37开始,普通用户是没有办法从/proc/kallsyms中读到正确的值(需要内核指针的禁用/proc/sys/kernel/kptr_restrict设置为0)。在某些版本中,该文件为空,在较新的版本中,该文件中所有符号的地 址均为0。但是root用户是可以从/proc/kallsyms中读到正确的值的。好在加载模块也需要root权限,可以在加载模块时用脚本获取符号的 地址。命令:

1
#cat /proc/kallsyms | grep "\<do_page_fault\>" | awk '{print $1}'

内核符号表中,第一列为函数或变量的在内核中的地址,第二列为符号的类型,第三列为符号名,第四列为符号所属的模块。若第四列为空,则表示该符号属于内核代码。

1
2
3
4
5
6
7
8
9
10
11
符号属性    含义
b    符号在未初始化数据区(BSS)
c    普通符号,是未初始化区域
d    符号在初始化数据区
g    符号针对小object,在初始化数据区
i    非直接引用其他符号的符号
n    调试符号
r    符号在只读数据区
s    符号针对小object,在未初始化数据区
t    符号在代码段
u    符号未定义

若符号在内核中是全局性的,则属性为大写字母,如T、U等。

C语言输出缓冲区函数说明

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <unistd.h>

int main(void)
{
	int i = 0;
	while(1) {
		printf("sleeping %d", i++); //(1)
		fflush(stdout);
		sleep(1);
	}
	return 0;
}

1

printf将"sleeping %d"输出到标准输出文件的缓冲区中(缓冲区在内存上),fflush(stdout)将缓冲区中的内容强制刷新到,并将其中的内容输出到显示器上(“\n"回车换行 == fflush(stdout)+换行)

1
2
fflush()
buffer(In memroy) -----------> hard disk/monitor

2

有三个流(stream)是自动打开的, 相应的FILE结构指针为stdin、stdout、stderr,与之对应的文件描述符是:STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。

流缓冲的属性:

缓冲区类型有:全缓冲(大部分缓冲都是这类型)、行缓冲(例如stdio,stdout)、无缓冲(例如stderr)。
关于全缓冲,例如普通的文件操作,进行fputs、fprintf操作后,数据并没有立即写入磁盘文件中,当fflush或fclose文件时,数据才真正写入。
可以用以下函数设置流的缓冲类型:

1
2
3
4
void setvbuf()  
void setbuf()  
void setbuffer()  
void setlinebuf()

3

fflush() 是把 FILE *里的缓冲区(位于用户态进程空间)刷新到内核中
fsync() -是把内核中对应的缓冲(是在 vfs 层的缓冲)刷新到硬盘中

4

在Linux的标准函数库中,有一套称作“高级I/O”的函数,我们熟知的printf()、fopen()、fread()、fwrite()都在此 列,它们也被称作“缓冲I/O(buffered I/O)”,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(达到一定数量,或遇到特定字符,如换行符\n和文件结束符EOF),再 将缓冲区中的内容一次性写入文件,这样就大大增加了文件读写的速度。


The three types of buffering available are unbuffered, block buffered, and line buffered. When an output stream is unbuffered, information appears on the destination file or terminal as soon as written; when it is block buffered many characters are saved up and written as a block; when it is line buffered characters are saved up until a newline is output or input is read from any stream attached to a terminal device (typically stdin). The function fflush(3) may be used to force the block out early. (See fclose(3).) Normally all files are block buffered. When the first I/O operation occurs on a file, malloc(3) is called, and a buffer is obtained. If a stream refers to a terminal (as stdout normally does) it is line buffered. The standard error stream stderr is always unbuffered by default.

一般来说,block buffered的效率高些,将多次的操作合并成一次操作。先在标准库里缓存一部分,直到该缓冲区满了,或者程序显示的调用fflush时,将进行更新操作。而setbuf 则可以设置该缓冲区的大小。

setbuf()
1
2
#include <stdio.h>
void setbuf(FILE *stream, char *buf);

这个函数应该必须在如何输出被写到该文件之前调用。一般放在main里靠前面的语句!但是setbuf有个经典的错误,man手册上也提到了,c陷阱和缺陷上也提到了 You must make sure that both buf and the space it points to still exist by the time stream is closed, which also happens at program termination. For example, the following is illegal:

1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
	char buf[BUFSIZ];
	setbuf(stdin, buf);
	printf("Hello, world!\n");
	return 0;
}

这个程序是错误的。buf缓冲区最后一次清空应该在main函数结束之后,程序交回控制给操作系统之前C运行库所必须进行的清理工作的一部分,但是此时 buf字符数组已经释放。修改的方法是将buf设置为static,或者全局变量; 或者调用malloc来动态申请内存。

1
2
char * malloc();
setbuf(stdout,malloc(BUFSIZE));

这里不需要判断malloc的返回值,如果malloc调用失败,将返回一个null指针,setbuf的第二个参数可以是null,此时不进行缓冲!

fflush()

fflush函数则刷新缓冲区,将缓冲区上的内容更新到文件里。

1
2
#include <stdio.h>
int fflush(FILE *stream);

The function fflush forces a write of all user-space buffered data for the given output or update stream via the stream underlying write function. The open status of the stream is unaffected. If the stream argument is NULL, fflush flushes all open output streams.

但是fflush仅仅刷新C库里的缓冲。其他的一些数据的刷新需要调用fsync或者sync!

Note that fflush() only flushes the user space buffers provided by the C library. To ensure that the data is physically stored on disk the kernel buffers must be flushed too, e.g. with sync(2) or fsync(2).

fsync()和sync()

fsync和sync最终将缓冲的数据更新到文件里。

1
2
#include <unistd.h>
int fsync(int fd);

fsync copies all in-core parts of a file to disk, and waits until the device reports that all parts are on stable storage. It also updates metadata stat information. It does not necessarily ensure that the entry in the directory containing the file has also reached disk. For that an explicit fsync on the file descriptor of the directory is also needed.

同步命令sync就直接调用了sync函数来更新磁盘上的缓冲!