linux内核kprobe分析

来源:互联网 发布:逆战老是网络波动异常 编辑:程序博客网 时间:2024/04/30 08:34
转自博客:[kprobe原理解析(二)](http://www.cnblogs.com/honpey/p/4575902.html) [关于kprobe的几种使用](http://blog.csdn.net/cybertan/article/details/8045396)

kprobe的工作过程大致如下:

1)注册kprobe。注册的每个kprobe对应一个kprobe结构体,该结构中记录着插入点(位置),以及该插入点本来对应的指令original_opcode;

2)替换原有指令。使能kprobe的时候,将插入点位置的指令替换为一条异常(BRK)指令,这样当CPU执行到插入点位置时会陷入到异常态;

3)执行pre_handler。进入异常态后,首先执行pre_handler,然后利用CPU提供的单步调试(single-step)功能,设置好相应的寄存器,将

                                    下一条指令设置为插入点处本来的指令,从异常态返回;

4)再次陷入异常态。上一步骤中设置了single-step相关的寄存器,所以originnal_opcode刚一执行,便会二进宫:再次陷入异常态,此时将single-step

                                  清除,并且执行post_handler,然后从异常态安全返回。

步骤2),3),4)便是一次kprobe工作的过程,它的一个基本思路就是将本来执行一条指令扩展成执行kprobe->pre_handler —> 指令 —> kprobe–>post_hander这样三个过程。下面详细解释每个过程:

指令替换过程:

 

上图中蓝色区域表示内存,红色标明了地址,绿色部分代表一条指令,上图的意思是,内存0xfffffc000162914处存放一条指令是0xa9bd7bfd。那么,现在我注册了一个kprobe,探测点是sys_write函数,该函数的起始位置就是0xffffffc000162914,现在我要使能kprobe了,那么我要做的就是把0xffffffc000162914处原来的指令0xa9bd7bfd替换成一条BRK指令,即上图所表示的一个移花接木过程。你可能会好奇原来的指令0xa9bd7bfd存在哪里?存在kprobe结构体的opcode域!这样当不再使能kprobe的时候,我再恢复回去。

触发BRK指令:

上面把人家指令给改了,那么CPU执行到BRK必然会引发内核陷入BRK异常状态:

蓝色部分依旧表示内存,绿色部分表示指令,红色表示CPU,上图表示CPU执行到0xffffffc000162914(sys_write)处,该处指令为BRK,于是内核陷入异常态。在异常态中,内核通过BRK指令的错误码判断这是一个kprobe异常,于是进入了kprobe处理函数。kprobe异常处理函数会根据发生异常的地址来找到对应的kprobe(kprobe的addr域记录着地址),执行kprobe的pre_handler函数,然后设置single-step相关的寄存器,为下一步执行原指令时发生single-step异常作准备。那么紧接着就是设置原指令的地址了,我们知道0xffffffc000162914处已经被替换成了BRK指令,原指令保存在kprobe结构体中,怎么保证下一步执行到原指令呢?最简单的做法是申请一块内存,然后将原指令复制到这块内存开始处,设置PC寄存器为该内存的首地址,这样当代码从异常态返回时,执行的第一条指令便是原指令了!

原指令得到执行,二进宫

经过上面一个步骤,pre_handler得到了执行,从异常态返回之后,原指令也得到了执行,但是由于设置了single-step模式,所以执行完原指令,马上又陷入了异常态,二进宫:

这次进入异常态后,先清一下single-step相关的寄存器,确保下次从异常返回时的指令不会由于single-step发生三进宫,然后执行post_handler,最后将地址0xfffffc000162918写入到PC寄存器,为什么是这个数值呢?它正是紧接着0xffffffc000162914的下一条指令的地址,有没有发现,至此我们已经完成了pre_handler->原指令->post_handler这样三个阶段,也就是说kprobe要做的事情都做完了,此时的工作就是收拾下残局,返回到正常的指令流程,我们的探测点在0xffffffc000162914处,下一条指令应该就是0xffffffc000162918了,所以把此值写入PC寄存器,让一切恢复正轨!

kprobe工作结束,走上正轨

上面把PC设置成了0xffffffc000162918,所以从异常态返回时,CPU就走上了正轨接着朝下面执行了,一个BRK指令引发的反应在此就告一段落了,但是每次当CPU执行到0xffffffc000162914处,都会触发上面的一连串操作,kprobe的机制也就是从一个BRK指令开始了。

注:

——————————————————————————————————————————————————————————

由于kprobe涉及到程序指令的修改,这部分和体系结构相关,我选择的体系结构ARM64,如本文的BRK指令等均是ARM64中的概念,

x86中INT3与之对应。

——————————————————————————————————————————————————————————

###**使用方法**

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


/* kprobe-exam.c */

#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 on CPU#%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, struct pt_regs *regs, unsigned long flags)
{
    printk(“current task on CPU#%d: %s (after scheduling), preempt_count = %d\n”, smp_processor_id(), current->comm, preempt_count());
}

int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
    printk(“A fault happened during 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(“kprobe registered\n”);
    return 0;
}

void cleanup_module(void)
{
    unregister_kprobe(&kp);
    do_gettimeofday(&end);
    printk(“Scheduling times is %d during of %ld milliseconds.\n”, schedule_counter, ((end.tv_sec - start.tv_sec)*1000000 + (end.tv_usec - start.tv_usec))/1000);
    printk(“kprobe unregistered\n”);
}

MODULE_LICENSE(“GPL”);

2:,它示例了获取系统调用open的参数,但读者不要试图在实际的应用中这么使用,因为copy_from_user可能导致睡眠,而kprobe并不允许在探测点处理函数中这么做

/* jprobe-exam.c */
/* jprobe-exam.c */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/kallsyms.h>
#include <linux/fs.h>
#include <asm/uaccess.h>


static struct jprobe jp;

asmlinkage long jprobe_sys_open(const char __user *filename, int flags, int mode)
{
    int len = PATH_MAX;
    char * tmpfilename = NULL;

    if (TASK_SIZE - (unsigned long) filename < PATH_MAX) {
        len = TASK_SIZE - (unsigned long) filename;
    }

    tmpfilename = kmalloc(len, GFP_ATOMIC);
    if (tmpfilename == NULL) return 0;

    if (copy_from_user(tmpfilename, filename, len)) return 0;

    printk(“process ‘%s’ call open(‘%s’, %d, %d)\n”, current->comm, tmpfilename, flags, mode);
    jprobe_return();
    return 0;
}

int init_module(void)
{
    int ret;

    jp.entry = (kprobe_opcode_t *) jprobe_sys_open;
    jp.kp.addr = (kprobe_opcode_t *)kallsyms_lookup_name(“sys_open”);
    if (!jp.kp.addr) {
        printk(“Couldn’t find the address of sys_open\n”);
        return -1;
    }

    if ((ret = register_jprobe(&jp)) <0) {
        printk(“register_jprobe failed, returned %d\n”, ret);
        return -1;
    }
    printk(“Registered a jprobe.\n”);
    return 0;
}

void cleanup_module(void)
{
    unregister_jprobe(&jp);
    printk(“jprobe unregistered\n”);
}

MODULE_LICENSE(“GPL”);

3:kretprobe-exam.c是一个返回探测例子,它探测系统调用open并输出返回值小于0的情况。它也有意设置maxactive为1,以便示例丢失探测运行的情况,当然,只有系统并发运行多个sys_open才可能导致这种情况,因此,读者需要有SMP的系统或者有超线程支持才能看到这种情况。

/*kretprobe-exam.c*/

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

static struct kretprobe kretp;

static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    // Substitute the appropriate register name for your architecture –
    // e.g., regs->rax for x86_64, regs->gpr[3] for ppc64.
    int retval = (int) regs->eax;
    if (retval < 0) {
        printk(“sys_open returns %d\n”, retval);
    }
    return 0;
}

int init_module(void)
{
    int ret;

    kretp.kp.addr = (kprobe_opcode_t *) kallsyms_lookup_name(“sys_open”);
    if (!kretp.kp.addr) {
        printk(“Couldn’t find sys_open.\n”);
        return -1;
    }

    kretp.handler = ret_handler,
    kretp.maxactive = 1;

    if ((ret = register_kretprobe(&kretp)) < 0) {
        printk(“register_kretprobe failed, returned %d\n”, ret);
        return -1;
    }
    printk(“Registered a return probe.\n”);
    return 0;
}

void cleanup_module(void)
{
    unregister_kretprobe(&kretp);
    printk(“kretprobe unregistered\n”);
    printk(“Missed %d sys_open probe instances.\n”, kretp.nmissed);
}

MODULE_LICENSE(“GPL”);



原创粉丝点击