kk Blog —— 通用基础

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

linux内核文件读取

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
// test_file.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

#include <linux/types.h>

#include <linux/fs.h>
#include <linux/string.h>
#include <asm/uaccess.h> /* get_fs(),set_fs(),get_ds() */


static int __init file_test_init(void)
{
	char *FILE_DIR = "/root/test.txt";
	char *buff = "module read/write test";
	char tmp[100];
	struct file *filp = NULL;
	mm_segment_t old_fs;
	ssize_t ret;
   
	filp = filp_open(FILE_DIR, O_RDWR | O_CREAT, 0644);
   
	if(IS_ERR(filp)) {
		printk("open error...\n");
		return -2;
	}
   
	old_fs = get_fs();
	set_fs(get_ds());

	filp->f_op->write(filp, buff, strlen(buff), &filp->f_pos);
	filp->f_op->llseek(filp, 0, 0);
	ret = filp->f_op->read(filp, tmp, strlen(buff), &filp->f_pos);

	set_fs(old_fs);
	   
	if(ret > 0)
		printk("%s\n", tmp);
	else if(ret == 0)
		printk("read nothing.............\n");
	else {
		printk("read error\n");
		return -1;
	}

	filp_close(filp, NULL);
	return 0;
}

static void __exit file_test_exit(void)
{
	printk("file test exit\n");
}

module_init(file_test_init);
module_exit(file_test_exit);

MODULE_LICENSE("GPL");
1
2
3
4
5
6
7
8
9
10
11
// Makefile

obj-m := test_file.o

KDIR := /lib/modules/$(uname -r)/build/
PWD := $(shellpwd)

all:
      make -C $(KDIR) M=$(PWD) modules
clean:
      make -C $(KDIR) M=$(PWD) clean

注意:

在调用filp->f_op->read和filp->f_op->write等对文件的操作之前,应该先设置FS。
默认情况下,filp->f_op->read或者filp->f_op->write会对传进来的参数buff进行指针检查。如果不是在用户空间会拒绝访问。因为是在内核模块中,所以buff肯定不在用户空间,所以要增大其寻址范围。

拿filp->f_op->write为例来说明:
filp->f_op->write最终会调用access_ok ==> range_ok.
而range_ok会判断访问的地址是否在0 ~ addr_limit之间。如果在,则ok,继续。如果不在,则禁止访问。而内核空间传过来的buff肯定大于addr_limit。所以要set_fs(get_ds())。
这些函数在asm/uaccess.h中定义。以下是这个头文件中的部分内容:

1
2
3
4
5
6
7
8
9
10
#define MAKE_MM_SEG(s)   ((mm_segment_t) { (s) })

#define KERNEL_DS MAKE_MM_SEG(-1UL)
#define USER_DS       MAKE_MM_SEG(PAGE_OFFSET)

#define get_ds()  (KERNEL_DS)
#define get_fs()  (current_thread_info()->addr_limit)
#define set_fs(x) (current_thread_info()->addr_limit = (x))

#define segment_eq(a, b)  ((a).seg == (b).seg)

可以看到set_fs(get_ds())改变了addr_limit的值。这样就使得从模块中传递进去的参数也可以正常使用了。

在写测试模块的时候,要实现的功能是写进去什么,然后读出来放在tmp数组中。但写完了以后filp->f_ops已经在末尾了,这个时候读是什么也 读不到的,如果想要读到数据,则应该改变filp->f-ops的值,这就要用到filp->f_op->llseek函数了。其中的参数需要记下笔记:
系统调用:
off_t sys_lseek(unsigned int fd, off_t offset, unsigned int origin)
offset是偏移量。
若origin是SEEK_SET(0),则将该文件的位移量设置为距文件开始处offset 个字节。
若origin是SEEK_CUR(1),则将该文件的位移量设置为其当前值加offset, offset可为正或负。
若origin是SEEK_END(2),则将该文件的位移量设置为文件长度加offset, offset可为正或负。

http://blog.csdn.net/yf210yf/article/details/8997007

RedHat/CentOS发行版本号及内核版本号对照表

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
Redhat 9.0———————————————2.4.20-8
RHEL 3 Update 8————————————2.4.21-47
RHEL 4 ————————————————2.6.9-5
RHEL 4 Update 1————————————2.6.9-11
RHEL 4 Update 2————————————2.6.9-22
RHEL 4 Update 3————————————2.6.9-34
RHEL 4 Update 4————————————2.6.9-42
RHEL 4 Update 5————————————2.6.9-55
RHEL 4 Update 6————————————2.6.9-67
RHEL 4 Update 7————————————2.6.9-78

CENTOS 5/RHEL 5 ———————————2.6.18-8
CENTOS 5.1/RHEL 5 Update 1——————2.6.18-53
CENTOS 5.2/RHEL 5 Update 2——————2.6.18-92
CENTOS 5.3/RHEL 5 Update 3——————2.6.18-128
CENTOS 5.4/RHEL 5 Update 4——————2.6.18-164
CENTOS 5.5/RHEL 5 Update 5——————2.6.18-194
CENTOS 5.6/RHEL 5 Update 6——————2.6.18-238
CENTOS 5.7/RHEL 5 Update 7——————2.6.18-274
CENTOS 5.8/RHEL 5 Update 8——————2.6.18-308

CENTOS 6.0/RHEL 6 Update 0——————2.6.32-71
CENTOS 6.1/RHEL 6 Update 1——————2.6.32-131
CENTOS 6.2/RHEL 6 Update 2——————2.6.32-220
CENTOS 6.3/RHEL 6 Update 3——————2.6.32-279
CENTOS 6.4/RHEL 6 Update 4——————2.6.32-358
CENTOS 6.5/RHEL 6 Update 5——————2.6.32-431
CENTOS 6.6/RHEL 6 Update 6——————2.6.32-504

CENTOS 7.0/RHEL 7 Update 0——————3.10.0-123

Linux内核kretprobe机制

http://www.ibm.com/developerworks/cn/linux/l-cn-systemtap1/index.html

kretprobe也使用了kprobes来实现,当用户调用register_kretprobe()时,kprobe在被探测函数的入口建立 了一个探测点,当执行到探测点时,kprobe保存了被探测函数的返回地址并取代返回地址为一个trampoline的地址,kprobe在初始化时定义 了该trampoline并且为该trampoline注册了一个kprobe,当被探测函数执行它的返回指令时,控制传递到该trampoline,因 此kprobe已经注册的对应于trampoline的处理函数将被执行,而该处理函数会调用用户关联到该kretprobe上的处理函数,处理完毕后, 设置指令寄存器指向已经备份的函数返回地址,因而原来的函数返回被正常执行。

被探测函数的返回地址保存在类型为 kretprobe_instance的变量中,结构kretprobe的maxactive字段指定了被探测函数可以被同时探测的实例数,函数 register_kretprobe()将预分配指定数量的kretprobe_instance。如果被探测函数是非递归的并且调用时已经保持了自旋 锁(spinlock),那么maxactive为1就足够了; 如果被探测函数是非递归的且运行时是抢占失效的,那么maxactive为NR_CPUS就可以了;如果maxactive被设置为小于等于0, 它被设置到缺省值(如果抢占使能, 即配置了 CONFIG_PREEMPT,缺省值为10和2*NR_CPUS中的最大值,否则缺省值为NR_CPUS)。

如果 maxactive被设置的太小了,一些探测点的执行可能被丢失,但是不影响系统的正常运行,在结构kretprobe中nmissed字段将记录被丢失 的探测点执行数,它在返回探测点被注册时设置为0,每次当执行探测函数而没有kretprobe_instance可用时,它就加1。

http://hi.baidu.com/lixiang1988/item/8884bc286c9920ceddf69acd

kretprobe的实现

相关数据结构与函数分析

1) struct kretprobe结构

该结构是kretprobe实现的基础数据结构,以下是该结构的成员:

1
2
3
4
5
6
7
struct kprobe kp; //该成员是kretprobe内嵌的struct kprobe结构。
kretprobe_handler_t handler;//该成员是调试者定义的回调函数。
int maxactive;//该成员是最多支持的返回地址实例数。
int nmissed;//该成员记录有多少次该函数返回没有被回调函数处理。
struct hlist_head free_instances;
用于链接未使用的返回地址实例,在注册时初始化。
struct hlist_head used_instances;//该成员是正在被使用的返回地址实例链表。
2) struct kretprobe_instance结构

该结构表示一个返回地址实例。因为函数每次被调用的地方不同,这造成了返回地址不同,因此需要为每一次发生的调用记录在这样一个结构里面。以下是该结构的成员:

1
2
3
4
5
struct hlist_node uflist;
该成员被链接入kretprobe的used_instances或是free_instances链表。
struct kretprobe *rp;//该成员指向所属的kretprobe结构。
kprobe_opcode_t *ret_addr;//该成员用于记录被探测函数正真的返回地址。
struct task_struct *task;//该成员记录当时运行的进程。
3) pre_handler_kretprobe()函数

该函数在kretprobe探测点被执行到后,用于修改被探测函数的返回地址。

4) trampoline_handler()函数

该函数用于执行调试者定义的回调函数以及把被探测函数的返回地址修改回原来的返回地址。

kretprobe处理流程分析

kretprobe探测方式是基于kprobe实现的又一种内核探测方式,该探测方式主要用于在函数返回时进行探测,获得内核函数的返回值,还可以用于计算函数执行时间等方面。

1) kretprobe的注册过程

调试者要进行kretprobe调试首先要注册处理,这需要在调试模块中调用register_kretprobe(),下文中称该函数为 kretprobe 注册器。kretprobe注册器对传入的kretprobe结构的中kprobe.pre_handler赋值为 pre_handler_kretprobe()函数,用于在探测点被触发时被调用。接着,kretprobe注册器还会初始化kretprobe的一些 成员,比如分配返回地址实例的空间等操作。最后,kretprobe注册器会利用 kretprobe内嵌的structkprobe结构进行kropbe的注册。自此,kretprobe注册过程就完成了。

2) kretprobe探测点的触发过程

kretprobe触发是在刚进入被探测函数的第一条汇编指令时发生的,因为 kretprobe注册时把该地址修改位int3指令。
此时发生了一次CPU异常处理,这与kprobe探测点被触发相同。但与kprobe处理不同的是,这里并不是运行用户定义的pre_handler函 数,而是执行pre_handler_kretprobe()函数,该函数又会调用arch_prepare_kretprobe()函数。 arch_prepare_kretprobe()函数的主要功能是把被探测函数的返回地址变换为&kretprobe_trampoline所 在的地址,这是一个汇编地址标签。这个标签的地址在kretprobe_trampoline_holder()中用汇编伪指令定义。替换函数返回地址是 kretprobe实现的关键。当被探测函数返回时,返回到&kretprobe_trampoline地址处开始运行。

接着,在 一些保护现场的处理后,又去调用trampoline_handler()函数。该函数的主要有两个作用,一是根据当前的实例去运行用户定义的调试函数, 也就是kretprobe结构中的handler所指向的函数,二是把返回值设成被探测函数正真的返回地址。最后,在进行一些堆栈的处理后,被探测函数又 返回到了正常执行流程中去。

以上讨论的就是kretprobe的执行过程,可以看出,该调试方式的关键点在于修改被探测函数的返回地址到kprobes的控制流程中,之后再把返回地址修改到原来的返回地址并使得该函数继续正常执行。

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自动就能把源码中的出错位置找出来.