首页 > *nix技术, 内核技术, 跟踪调试 > Linux Kprobes介绍

Linux Kprobes介绍

2013年3月10日 发表评论 阅读评论 8,111 次浏览

不得不说printk()函数是一个伟大的调试工具,它几乎能帮助我们在Linux系统的任何位置把我们想要的数据打印出来,但同时这也是一个比较耗时的过程,因为我们得把printk()函数加入到Linux内核源码的对应位置,重新编译模块,如果是内核修改,那么也许还得重启机器。

动态工具Kprobe的出现,使得我们可以在Linux系统的大部分地方都能通过更简便、快速的方法获得更多的内核信息,在前面文章里曾介绍的SystemTap也就是以Kprobe为基石而成的用户命令接口。

Kprobe,简单点说就是允许我们在内核执行到指定的地方时回调一个我们的自定义函数,在这个自定义函数内,我们就可以执行一些内核信息(类似于以前的printk())收集动作,完了之后内核再回去执行它原本的逻辑。

在这里,“内核执行到指定的地方”有一个专门的术语叫做probe(探针),目前有三种类型的probe,分别为:
kprobes:内核代码的任何指令处。
jprobes:内核函数的入口处,因此可以很方便的获取到对应内核函数的参数。
kretprobes:内核函数的退出点。

先看个Kprobe实例:

/*
 * NOTE: This example is works on x86 and powerpc.
 * Here's a sample kernel module showing the use of kprobes to dump a
 * stack trace and selected registers when do_fork() is called.
 *
 * For more information on theory of operation of kprobes, see
 * Documentation/kprobes.txt
 *
 * You will see the trace data in /var/log/messages and on the console
 * whenever do_fork() is invoked to create a new process.
 */

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

/* For each probe you need to allocate a kprobe structure */
static struct kprobe kp = {
	.symbol_name	= "do_fork",
};

/* kprobe pre_handler: called just before the probed instruction is executed */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx,"
			" flags = 0x%lx\n",
		p->addr, regs->ip, regs->flags);
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, nip = 0x%lx,"
			" msr = 0x%lx\n",
		p->addr, regs->nip, regs->msr);
#endif

	/* A dump_stack() here will give a stack backtrace */
	return 0;
}

/* kprobe post_handler: called after the probed instruction is executed */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
				unsigned long flags)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n",
		p->addr, regs->flags);
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "post_handler: p->addr = 0x%p, msr = 0x%lx\n",
		p->addr, regs->msr);
#endif
}

/*
 * fault_handler: this is called if an exception is generated for any
 * instruction within the pre- or post-handler, or when Kprobes
 * single-steps the probed instruction.
 */
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
	printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
		p->addr, trapnr);
	/* Return 0 because we don't handle the fault. */
	return 0;
}

static int __init kprobe_init(void)
{
	int ret;
	kp.pre_handler = handler_pre;
	kp.post_handler = handler_post;
	kp.fault_handler = handler_fault;

	ret = register_kprobe(&kp);
	if (ret < 0) {
		printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
		return ret;
	}
	printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
	return 0;
}

static void __exit kprobe_exit(void)
{
	unregister_kprobe(&kp);
	printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
}

module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

上面实例来之:http://lxr.linux.no/#linux+v2.6.30.8/samples/kprobes/kprobe_example.c,即是Linux内核源码自带的例子,要编译它可以直接利用内核的Makefile(要打开对应的CONFIG_SAMPLES选项),也可以另外自写一个Makefile:

# Makefile
 
MDIR = $(shell pwd)
 
ifeq (, $(KSRC))
    KSRC := /usr/src/linux-2.6.30.8
endif
 
ifeq (, $(PROJECT_DIR))
	PROJECT_DIR := $(PWD)/../
endif
 
module := kprobe_example
 
obj-m := $(module).o
 
srcs =  $(wildcard, *.c)
 
$(module)-objs := $(addsuffix .o, $(basename $(srcs)))
 
EXTRA_CFLAGS += $(FLAG) -I$(PROJECT_DIR)/inc -I${SHAREDHDR} -I$(KERNELHDR) -O2 -D__KERNEL__ -DMODULE $(INCLUDE) -DEXPORT_SYMTAB
 
TARGET = $(module).ko
 
all:
	make -C $(KSRC) M=$(MDIR) modules
 
debug:
	make EXTRA_FLAGS="${EXTRA_CFLAGS} -DDEBUG" -C $(KSRC) M=$(MDIR) modules
 
clean:
	make -C $(KSRC) M=$(MDIR) clean
 
install: all
	cp -f $(TARGET) $(INSTALL_DIR)

编译OK:

[root@localhost kprobe]# ls
kprobe_example.c  Makefile
[root@localhost kprobe]# make
make -C /usr/src/linux-2.6.30.8 M=/root/kprobe modules
make[1]: Entering directory `/usr/src/linux-2.6.30.8'
  CC [M]  /root/kprobe/kprobe_example.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /root/kprobe/kprobe_example.mod.o
  LD [M]  /root/kprobe/kprobe_example.ko
make[1]: Leaving directory `/usr/src/linux-2.6.30.8'

看一下效果,执行的dmesg -c用于清空一下日志缓存,以便看得更清楚:

[root@localhost kprobe]# dmesg -c
[root@localhost kprobe]# insmod kprobe_example.ko 
[root@localhost kprobe]# dmesg 
Planted kprobe at ffffffff81065b30
pre_handler: p->addr = 0xffffffff81065b30, ip = ffffffff81065b31, flags = 0x246
post_handler: p->addr = 0xffffffff81065b30, flags = 0x246
[root@localhost kprobe]# rmmod kprobe_example.ko 
[root@localhost kprobe]# dmesg 
Planted kprobe at ffffffff81065b30
pre_handler: p->addr = 0xffffffff81065b30, ip = ffffffff81065b31, flags = 0x246
post_handler: p->addr = 0xffffffff81065b30, flags = 0x246
pre_handler: p->addr = 0xffffffff81065b30, ip = ffffffff81065b31, flags = 0x246
post_handler: p->addr = 0xffffffff81065b30, flags = 0x246
pre_handler: p->addr = 0xffffffff81065b30, ip = ffffffff81065b31, flags = 0x246
post_handler: p->addr = 0xffffffff81065b30, flags = 0x246
...
kprobe at ffffffff81065b30 unregistered

上面模块在内核符号do_fork处注册了一个探针,一旦系统内核执行到do_fork符号所对应的指令处,即会触发探针回调另外三个函数:handler_pre()、handler_post()、handler_fault(),其中函数handler_pre()和handler_post()分别在对应指令执行前和执行后被调用,而函数handler_fault()是在函数handler_pre()和handler_post()或对应指令单步执行发生异常(比如page fault)时被调用。

下来再来Kprobe机制的具体工作细节:
当需要在某指定的指令处(比如前面示例中符号do_fork地址所对应的指令,后续称为指令A)注册一个探针时,Kprobe会先将指令A拷贝出来并替换一条断点指令(比如i386和x86_64架构上的int3指令)进去(只占前8个字节)。这样,当cpu执行到do_fork地址时,实际执行的就是我们的断点指令,因此触发一个trap陷阱,此时将保存CPU的寄存器,并通过Linux内核的notifier_call_chain机制将cpu控制权限移交给Kprobe。

Kprobe先执行与当前探针相关的回调函数handler_pre(),该函数需要的参数,即kprobe结构体是已经知道的,而寄存器也在前面已经转存了,所以一切可以顺利进行。接下来,Kprobe将单步执行它所拷贝的指令A,再之后,Kprobe即执行post_handler()回调函数,最后,CPU继续执行指令A后面的指令。

Jprobe是基于Kprobe的实现,它特定在于函数的入口处,因此它能非常方便的访问到被探测函数的参数。Jprobe探针的使用有两个固定要求,首先,回调函数必须具有与被探测函数相同的参数列表和返回类型;其次,回调函数必须以jprobe_return()结束。

Jprobe的工作细节如下,当jprobe探针被触发时,Jprobe拷贝保存寄存器值和大部分(最大MAX_STACK_SIZE字节,比如i386为64字节)堆栈,然后将已保存的原指令(即指令A)位置指向jprobe的回调函数,因此,当从trap陷阱里返回来时,将由jprobe回调函数接管cpu的控制权限而被执行,其执行的环境也就是在前面保存的寄存器值和堆栈数据。当jprobe回调函数执行完毕,通过它调用的jprobe_return()函数,再次trap陷入并恢复到原本的堆栈上下文和处理器状态,进而开始执行被探测函数,进入到正常流程。

Return Probes同样是基于Kprobe的实现,它特定在于函数的返回处。当对函数(以函数f()为例)注册一个kretprobe时,Kprobe将在函数f()的入口点建立一个kprobe探针,所以一旦函数f()被执行,对应的探针将被触发,Kprobe先保存函数返回地址,然后把另外一个“trampoline地址”替换上去。这个trampoline地址处的指令没有什么特别,一般也就是nop指令,但在系统启动时,Kprobe会在这个trampoline地址指令处注册一个kprobe探针(假设名为探针R)。

一旦函数f()执行完毕进行返回后,cpu控制权限将被移交到trampoline地址处,因此探针R被触发。探针R的回调函数再执行我们给函数f()设置的kretprobe回调函数,然后再返回到之前保存的原返回地址处,进入到正常流程。

这里有一点值得注意,在具体的执行过程中,kretprobe探针有可能丢失,比如极端例子,f()为递归调用函数,那么如果递归的次数一多,就会因为返回地址无法全部保存下来导致后面f()函数执行时的kretprobe探针没有生效,不过Linux内核栈空间本身就比较少,所以一般也很少会在内核里用递归。

另外,用户可通过kretprobe的entry_handler字段设置一个入口回调函数,也就是在被探测函数的入口处执行,如果该函数返回0,那么对应的return回调将被照常执行,如果该函数返回非0,那么此次后续的kretprobe将不再生效,比如对应的return回调不会被执行。

Kprobe的API介绍如下:
注册kprobe:
#include int register_kprobe(struct kprobe *kp);
探针注册在kp->addr处,对应的回调为kp->pre_handler、kp->post_handler、kp->fault_handler。
可以把kp->flags设置为KPROBE_FLAG_DISABLED,则表示仅注册而不生效,后续可通过函数enable_kprobe(kp)进行启用。
可通过kp.symbol_name指定符号,比如:kp.symbol_name = “symbol_name”;
kp->addr和kp.symbol_name不能同时指定,否则将返回-EINVAL错误。

注册jprobe:
#include int register_jprobe(struct jprobe *jp)
在jp->kp.addr处注册一个探针,jp->kp.addr必须是一个函数的第一条指令。
对应的回调为jp->entry,它具有与被探测函数相同的参数和返回值,并且该函数末尾必须调用jprobe_return()。

注册kretprobe:
#include int register_kretprobe(struct kretprobe *rp);
在rp->kp.addr处注册一个探针,当对应函数返回时,rp->handler回调将被执行。
在调用register_kretprobe()之前必须恰当的设置rp->maxactive

启用禁用:
#include int disable_kprobe(struct kprobe *kp);
int disable_kretprobe(struct kretprobe *rp);
int disable_jprobe(struct jprobe *jp);

int enable_kprobe(struct kprobe *kp);
int enable_kretprobe(struct kretprobe *rp);
int enable_jprobe(struct jprobe *jp);

Kprobes的功能和限制:
1,Kprobes允许在同一地址注册多个探针,但在当前,不允许在同一个函数上同时注册多个jprobe。
2,一般而言,可以在Linux内核的任何位置注册探针,甚至是中断回调函数,但无法对实现Kprobes本身以及相关逻辑(比如do_page_fault、notifier_call_chain)的代码进行注册。
3,如果向一个可内联的函数进行探针注册,那么Kprobes没有尝试去追寻所有的展开点,所以某些展开点可能无法触发探针。
4,Kprobes探针回调函数可以修改被探测函数的上下文环境,因此可以进行错误注入等进行测试。
5,探针回调函数本身调用的函数不会触发探针,比如在函数printk()处注册一个探针,但在该探针的回调函数内又调用了函数printk(),那么在因内核执行函数printk()而触发探针,进而执行该探针的回调函数时调用到函数printk()不会再次触发该探针,不过对应的kprobe.nmissed会加1。
6,除了在Kprobes的注册和注销函数内以外,其它函数内不能使用互斥锁或进行内存分配。
7,探针回调函数在禁止抢占的状态下执行,根据架构和优化状态的不同,回调函数还可能在禁止中断的情况下执行。
8,其它,……

Kprobes性能开销:

完全参考:
http://lxr.linux.no/#linux+v3.8.2/Documentation/kprobes.txt

其它参考:
http://www-users.cs.umn.edu/~boutcher/kprobes/
http://www.linuxforu.com/2011/04/kernel-debugging-using-kprobe-and-jprobe/
https://www.ibm.com/developerworks/cn/linux/l-cn-systemtap1/
http://www.ibm.com/developerworks/cn/linux/l-kprobes.html
http://www.redhat.com/magazine/005mar05/features/kprobes/
http://lwn.net/Articles/132196/

转载请保留地址:http://www.lenky.info/archives/2013/03/2237http://lenky.info/?p=2237


备注:如无特殊说明,文章内容均出自Lenky个人的真实理解而并非存心妄自揣测来故意愚人耳目。由于个人水平有限,虽力求内容正确无误,但仍然难免出错,请勿见怪,如果可以则请留言告之,并欢迎来讨论。另外值得说明的是,Lenky的部分文章以及部分内容参考借鉴了网络上各位网友的热心分享,特别是一些带有完全参考的文章,其后附带的链接内容也许更直接、更丰富,而我只是做了一下归纳&转述,在此也一并表示感谢。关于本站的所有技术文章,欢迎转载,但请遵从CC创作共享协议,而一些私人性质较强的心情随笔,建议不要转载。

法律:根据最新颁布的《信息网络传播权保护条例》,如果您认为本文章的任何内容侵犯了您的权利,请以Email或书面等方式告知,本站将及时删除相关内容或链接。

  1. TonyL
    2016年8月27日20:48 | #1

    感谢分享,最近在看kprobes.txt,文章知识正好是我需要的,但我有一个问题,就是makefile文件里面的debug目标的内容是什么意思?还有INSTALL_DIR有值吗?

  2. 2014年8月19日09:55 | #2

    好文,感谢分享

  1. 本文目前尚无任何 trackbacks 和 pingbacks.