kprobe钩子详细介绍

Kprobe钩子介绍

kprobe是一个动态地收集调试和性能信息的工具,它从Dprobe项目派生而来,是一种非破坏性工具,用户用它几乎可以跟踪任何函数或被执行的指令以及一些异步事件(如timer)。它的基本工作机制是:用户指定一个探测点,并把一个用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。

kprobe实现了三种类型的探测点:kprobes, jprobes和kretprobes (也叫返回探测点)。 kprobes是可以被插入到内核的任何指令位置的探测点,jprobes则只能被插入到一个内核函数的入口,而kretprobes则是在指定的内核函数返回时才被执行。

一、Kprobes

1、原理介绍

当安装一个kprobes探测点时,kprobe首先备份被探测的指令,然后使用断点指令(即在i386和x86_64的int3指令)来取代被探测指令的头一个或几个字节。当CPU执行到探测点时,将因运行断点指令而执行trap操作,那将导致保存CPU的寄存器,调用相应的trap处理函数,而trap处理函数将调用相应的notifier_call_chain(内核中一种异步工作机制)中注册的所有notifier函数,kprobe正是通过向trap对应的notifier_call_chain注册关联到探测点的处理函数来实现探测处理的。当kprobe注册的notifier被执行时,它首先执行关联到探测点的pre_handler函数,并把相应的kprobe struct和保存的寄存器作为该函数的参数,接着,kprobe单步执行被探测指令的备份,最后,kprobe执行post_handler。等所有这些运行完毕后,紧跟在被探测指令后的指令流将被正常执行。

2、相关接口

2.1、kprobe结构体

structkprobe {

    /*用于保存kprobe的全局hash表,以被探测的addr为key*/

    struct hlist_node hlist;

 

    /* list of kprobes for multi-handlersupport */

    /*当对同一个探测点存在多个探测函数时,所有的函数挂在这条链上*/

    struct list_head list;

 

    /*count the number of times this probe wastemporarily disarmed */

    unsigned long nmissed;

 

    /* location of the probe point */

    /*被探测的目标地址*/

    kprobe_opcode_t *addr;

 

    /* Allow user to indicate symbol name ofthe probe point */

    /*symblo_name的存在,允许用户指定函数名而非确定的地址*/

    const char *symbol_name;

 

    /* Offset into the symbol */

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

    unsigned int offset;

 

    /* 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 afault (eg. page fault).

     * Return 1 if it handled fault, otherwisekernel will see it.

     */

    kprobe_fault_handler_t fault_handler;

 

    /*

     * ... called if breakpoint trap occurs inprobe handler.

     * Return 1 if it handled break, otherwisekernel will see it.

     */

    kprobe_break_handler_t break_handler;

 

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

   

    /* Saved opcode (which has been replacedwith breakpoint) */

    kprobe_opcode_t opcode;

 

    /*copy of the original instruction */

    struct arch_specific_insn ainsn;

 

    /*

     * Indicates various status flags.

     * Protected by kprobe_mutex after thiskprobe is registered.

     */

    u32 flags;

};

2.2、注册

int register_kprobe(struct kprobe *kp);

为了使用该函数,用户需要在源文件中包含头文件linux/kprobes.h。

该函数的参数是structkprobe类型的指针,struct kprobe包含了字段addr、pre_handler、post_handler和fault_handler,addr指定探测点的位置,pre_handler指定执行到探测点时执行的处理函数,post_handler指定执行完探测点后执行的处理函数,fault_handler指定错误处理函数,当在执行pre_handler、post_handler以及被探测函数期间发生错误时,它会被调用。在调用该注册函数前,用户必须先设置好struct kprobe的这些字段,用户可以指定任何处理函数为NULL。

该注册函数会在kp->addr地址处注册一个kprobes类型的探测点,当执行到该探测点时,将调用函数kp->pre_handler,执行完被探测函数后,将调用kp->post_handler。如果在执行kp->pre_handler或kp->post_handler时或在单步跟踪被探测函数期间发生错误,将调用kp->fault_handler。

该函数成功时返回0,否则返回负的错误码。

2.3、探测点前处理函数

intpre_handler(struct kprobe *p, struct pt_regs *regs);

用户必须按照该原型参数格式定义自己的pre_handler,当然函数名取决于用户自己。参数p就是指向该处理函数关联到的kprobes探测点的指针,可以在该函数内部引用该结构的任何字段,就如同在使用调用register_kprobe时传递的那个参数。参数regs指向运行到探测点时保存的寄存器内容。kprobe负责在调用pre_handler时传递这些参数,用户不必关心,只是要知道在该函数内你能访问这些内容。

一般地,它应当始终返回0,除非用户知道自己在做什么。

2.4、探测点后处理函数

voidpost_handler(struct kprobe *p, struct pt_regs *regs, unsigned long flags);

函数名可以自定义,但原型必须一致。前两个参数与pre_handler相同,最后一个参数flags总是0。

2.5、错误处理函数

intfault_handler(struct kprobe *p, struct pt_regs *regs, int trapnr);

函数名可以自定义,但原型必须一致。前两个参数与pre_handler相同,第三个参数trapnr是与错误处理相关的架构依赖的trap号(例如,对于i386,通常的保护错误是13,而页失效错误是14)。

如果成功地处理了异常,它应当返回1。

2.6、卸载

voidunregister_kprobe(struct kprobe *kp);

2.7、pt_regs结构体

structpt_regs {

unsigned long r15;

unsigned long r14;

unsigned long r13;

unsigned long r12;

unsigned long bp;

unsigned long bx;

/*arguments: non interrupts/non tracing syscalls only save up to here*/

unsigned long r11;

unsigned long r10;

unsigned long r9;

unsigned long r8;

unsigned long ax;

unsigned long cx;

unsigned long dx;

unsigned long si;

unsigned long di;

unsigned long orig_ax;

/* endof arguments */

/* cpuexception frame or undefined */

unsigned long ip;

unsigned long cs;

unsigned long flags;

unsigned long sp;

unsigned long ss;

/* topof stack page */

};

 

3、简单例子

功能:探测schedule()函数,在探测点执行前后分别输出当前正在运行的进程、所在的CPU以及preempt_count(),当卸载该模块时将输出该模块运行时间以及发生的调度次数。

代码:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/kallsyms.h>
#include <linux/sched.h>
#include <linux/time.h>

static struct kprobe kp;
static struct timeval start, end;
static int schedule_counter = 0;

int handler_pre(struct kprobe *p, struct pt_regs*regs)
{
    printk("current task onCPU#%d: %s (before scheduling), preempt_count = %d\n", smp_processor_id(),current->comm, preempt_count());
    schedule_counter++;
    return 0;
}

void handler_post(struct kprobe *p, structpt_regs *regs, unsigned long flags)
{
    printk("current task onCPU#%d: %s (after scheduling), preempt_count = %d\n", smp_processor_id(),current->comm, preempt_count());
}

int handler_fault(struct kprobe *p, structpt_regs *regs, int trapnr)
{
    printk("A fault happenedduring probing.\n");
    return 0;
}

int init_module(void)
{
    int ret;

    kp.pre_handler = handler_pre;
    kp.post_handler =handler_post;
    kp.fault_handler =handler_fault;
    kp.addr = (kprobe_opcode_t*)kallsyms_lookup_name("schedule");
    
    if (!kp.addr) {
       printk("Couldn't get the address of schedule.\n");
        return -1;
    }

    if ((ret =register_kprobe(&kp) < 0)) {
       printk("register_kprobe failed, returned %d\n", ret);
        return -1;
    }

    do_gettimeofday(&start);

    printk("kproberegistered\n");
    return 0;
}

void cleanup_module(void)
{
    unregister_kprobe(&kp);
    do_gettimeofday(&end);
    printk("Scheduling timesis %d during of %ld milliseconds.\n", schedule_counter, ((end.tv_sec -start.tv_sec)*1000000 + (end.tv_usec - start.tv_usec))/1000);
    printk("kprobeunregistered\n");
}

MODULE_LICENSE("GPL");

 

二、jprobes

1、原理介绍

jprobe通过注册kprobes在被探测函数入口的来实现,它能无缝地访问被探测函数的参数。jprobe处理函数应当和被探测函数有同样的原型,而且该处理函数在函数末必须调用kprobe提供的函数jprobe_return()。当执行到该探测点时,kprobe备份CPU寄存器和栈的一些部分,然后修改指令寄存器指向jprobe处理函数,当执行该jprobe处理函数时,寄存器和栈内容与执行真正的被探测函数一模一样,因此它不需要任何特别的处理就能访问函数参数, 在该处理函数执行到最后时,它调用jprobe_return(),那导致寄存器和栈恢复到执行探测点时的状态,因此被探测函数能被正常运行。需要注意,被探测函数的参数可能通过栈传递,也可能通过寄存器传递,但是jprobe对于两种情况都能工作,因为它既备份了栈,又备份了寄存器,当然,前提是jprobe处理函数原型必须与被探测函数完全一样。

2、相关接口

2.1、jprobe结构体

structjprobe {

    struct kprobe kp;

    void *entry;    /* probe handling code to jump to */

};

Kprobe在第一节kprobes中已介绍。

2.2、注册

intregister_jprobe(struct jprobe *jp);

为了使用该函数,用户需要在源文件中包含头文件linux/kprobes.h。

用户在调用该注册函数前需要定义一个struct jprobe类型的变量并设置它的kp.addr和entry字段,kp.addr指定探测点的位置,它必须是被探测函数的第一条指令的地址,entry指定探测点的处理函数,该处理函数的参数表和返回类型应当与被探测函数完全相同,而且它必须正好在返回前调用jprobe_return()。如果被探测函数被声明为asmlinkage、fastcall或影响参数传递的任何其他形式,那么相应的处理函数也必须声明为相应的形式。

该注册函数在jp->kp.addr注册一个jprobes类型的探测点,当内核运行到该探测点时,jp->entry指定的函数会被执行。

如果成功,该函数返回0,否则返回负的错误码。

2.3、卸载

voidunregister_jprobe(struct jprobe *jp);

3、简单例子

功能:使用jprobes打印do_execve函数的参数的例子,每次调用do_execve之前,都会打印相应的参数。

代码:

#include<linux/module.h>

#include<linux/kprobes.h>

#include<linux/kallsyms.h>

 

structjprobe exec_jp;

intjp_do_execve(const char * filename,

    const char __user *const __user *argv,

    const char __user *const __user *envp,

    struct pt_regs * regs)

{

    int cnt = 0;

    printk("filename = %s\n",filename);

    for(; *argv != NULL;argv++,cnt++)

        printk("argv[%d] = %s\n", cnt,*argv);

 

    jprobe_return();

    return 0;

}

        

static__init int jprobes_exec_init(void)

{   

    exec_jp.kp.symbol_name ="do_execve";

    exec_jp.entry = JPROBE_ENTRY(jp_do_execve);

    /*注册jprobes*/

    register_jprobe(&exec_jp);

   

    return 0;

}

        

static__exit void jprobes_exec_cleanup(void)

{

    /*撤销jprobes注册*/

    unregister_jprobe(&exec_jp);

}

 

module_init(jprobes_exec_init);

module_exit(jprobes_exec_cleanup);

MODULE_LICENSE("GPL");

三、kretprobes

1、原理介绍

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。

2、相关接口

2.1、kretprobe结构体

内核2.6.25之前:

structkretprobe {

        struct kprobe kp;

        kretprobe_handler_t handler;

        int maxactive;

        int nmissed;

        struct hlist_head free_instances;

        struct hlist_head used_instances;

};

内核2.6.25及其后:

structkretprobe {

struct kprobe kp;

//注册的回调函数,handler指定探测点的处理函数

kretprobe_handler_t handler;

//注册的预处理回调函数,类似于kprobes中的pre_handler()

kretprobe_handler_t entry_handler;

int maxactive;

int nmissed;

//指示kretprobe需要为回调监控预留多少内存空间

size_t data_size;

struct hlist_head free_instances;

raw_spinlock_t lock;

};

2.2、注册

intregister_kretprobe(struct kretprobe *rp);

为了使用该函数,用户需要在源文件中包含头文件linux/kprobes.h。

该注册函数的参数为struct kretprobe类型的指针,用户在调用该函数前必须定义一个struct kretprobe的变量并设置它的kp.addr、handler以及maxactive字段,kp.addr指定探测点的位置,handler指定探测点的处理函数,maxactive指定可以同时运行的最大处理函数实例数,它应当被恰当设置,否则可能丢失探测点的某些运行。

该注册函数在地址rp->kp.addr注册一个kretprobe类型的探测点,当被探测函数返回时,rp->handler会被调用。

如果成功,它返回0,否则返回负的错误码。

2.3、回调处理函数

intkretprobe_handler(struct kretprobe_instance *ri, struct pt_regs *regs);

函数名可以自定义,但原型必须一致。参数regs指向保存的寄存器,ri指向类型为struct kretprobe_instance的变量,该结构的ret_addr字段表示返回地址,rp指向相应的kretprobe_instance变量,task字段指向相应的task_struct。结构structkretprobe_instance是注册函数register_kretprobe根据用户指定的maxactive值来分配的,kprobe负责在调用kretprobe处理函数时传递相应的kretprobe_instance。

2.4、获取返回值

intretval = regs_return_value(regs);

使用kretprobe时,我们通常更关注被探测函数的返回值,所以大多数情况下都会用到regs_return_value函数。

2.5、卸载

voidunregister_kretprobe(struct kretprobe *rp);

3、简单例子

功能:获取返回值并计算函数运行时间。

代码:

/*

 * kretprobe_example.c

 *

 * Here's a sample kernel module showing theuse of return probes to

 * report the return value and total time takenfor probed function

 * to run.

 *

 * usage: insmod kretprobe_example.kofunc=<func_name>

 *

 * If no func_name is specified, do_fork is instrumented

 *

 * For more information on theory of operationof kretprobes, see

 * Documentation/kprobes.txt

 *

 * Build and insert the kernel module as donein the kprobe example.

 * You will see the trace data in/var/log/messages and on the console

 * whenever the probed function returns. (Somemessages may be suppressed

 * if syslogd is configured to eliminateduplicate messages.)

 */

 

#include<linux/kernel.h>

#include<linux/module.h>

#include<linux/kprobes.h>

#include<linux/ktime.h>

#include<linux/limits.h>

#include<linux/sched.h>

 

staticchar func_name[NAME_MAX] = "do_fork";

module_param_string(func,func_name, NAME_MAX, S_IRUGO);

MODULE_PARM_DESC(func,"Function to kretprobe; this module will report the"

                    " function's executiontime");

 

/* per-instanceprivate data */

structmy_data {

ktime_t entry_stamp;

};

 

/* Herewe use the entry_hanlder to timestamp function entry */

staticint entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs)

{

struct my_data *data;

 

if (!current->mm)

         return1;    /* Skip kernel threads */

 

data = (struct my_data *)ri->data;

data->entry_stamp = ktime_get();

return 0;

}

 

/*

 * Return-probe handler: Log the return valueand duration. Duration may turn

 * out to be zero consistently, depending uponthe granularity of time

 * accounting on the platform.

 */

staticint ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)

{

int retval = regs_return_value(regs);

struct my_data *data = (struct my_data*)ri->data;

s64 delta;

ktime_t now;

 

now = ktime_get();

delta = ktime_to_ns(ktime_sub(now,data->entry_stamp));

printk(KERN_INFO "%s returned %d and took%lld ns to execute\n",

                    func_name, retval, (longlong)delta);

return 0;

}

 

staticstruct kretprobe my_kretprobe = {

.handler             =ret_handler,

.entry_handler           =entry_handler,

.data_size                   =sizeof(struct my_data),

/* Probe up to 20 instances concurrently. */

.maxactive                  =20,

};

 

staticint __init kretprobe_init(void)

{

int ret;

 

my_kretprobe.kp.symbol_name = func_name;

ret = register_kretprobe(&my_kretprobe);

if (ret < 0) {

           printk(KERN_INFO"register_kretprobe failed, returned %d\n",

                             ret);

           return -1;

}

printk(KERN_INFO "Planted return probe at%s: %p\n",

                    my_kretprobe.kp.symbol_name,my_kretprobe.kp.addr);

return 0;

}

 

staticvoid __exit kretprobe_exit(void)

{

unregister_kretprobe(&my_kretprobe);

printk(KERN_INFO "kretprobe at %punregistered\n",

                    my_kretprobe.kp.addr);

 

/* nmissed > 0 suggests that maxactive wasset too low. */

printk(KERN_INFO "Missed probing %dinstances of %s\n",

           my_kretprobe.nmissed,my_kretprobe.kp.symbol_name);

}

 

module_init(kretprobe_init)

module_exit(kretprobe_exit)

MODULE_LICENSE("GPL");

 

四、kprobe的特点和限制

1、在Kprobe回调函数里打开某些文件会导致崩溃,如:/bin/login 。

2、 kprobe允许在同一地址注册多个kprobes,但是不能同时在该地址上有多个jprobes。

3、通常,用户可以在内核的任何位置注册探测点,特别是可以对中断处理函数注册探测点,但是也有一些例外。如果用户尝试在实现kprobe的代码(包括kernel/kprobes.c和arch/*/kernel/kprobes.c以及do_page_fault和notifier_call_chain)中注册探测点,register_*probe将返回-EINVAL。

4、如果为一个内联(inline)函数注册探测点,kprobe无法保证对该函数的所有实例都注册探测点,因为gcc可能隐式地内联一个函数。因此,要记住,用户可能看不到预期的探测点的执行。

5、一个探测点处理函数能够修改被探测函数的上下文,如修改内核数据结构,寄存器等。因此,kprobe可以用来安装bug解决代码或注入一些错误或测试代码。

6、如果一个探测处理函数调用了另一个探测点,该探测点的处理函数不将运行,但是它的nmissed数将加1。多个探测点处理函数或同一处理函数的多个实例能够在不同的CPU上同时运行。

7、除了注册和卸载,kprobe不会使用mutexe或分配内存。

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

9、kretprobe是通过取代返回地址为预定义的trampoline的地址来实现的,因此栈回溯和gcc内嵌函数__builtin_return_address()调用将返回trampoline的地址而不是真正的被探测函数的返回地址。

10、如果一个函数的调用次数与它的返回次数不相同,那么在该函数上注册的kretprobe探测点可能产生无法预料的结果(do_exit()就是一个典型的例子,但do_execve() 和 do_fork()没有问题)。

11、当进入或退出一个函数时,如果CPU正运行在一个非当前任务所有的栈上,那么该函数的kretprobe探测可能产生无法预料的结果,因此kprobe并不支持在x86_64上对__switch_to()的返回探测,如果用户对它注册探测点,注册函数将返回-EINVAL。

参考文档:

http://blog.chinaunix.net/uid-20321537-id-3944828.html

http://blog.csdn.net/sdulibh/article/details/42078681

https://www.kernel.org/doc/Documentation/kprobes.txt

https://lwn.net/Articles/132196/

http://lxr.cpsc.ucalgary.ca/lxr/linux 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值