文章目录

  • 前言
  • 一、demo
    • 1.1 demo演示
    • 1.2 struct jprobe
  • 二、jprobe 原理
    • 2.1 原理简介
    • 1.2 原理详解
  • 三、源码解析
    • 3.1 struct jprobe
    • 3.2 register_jprobe
    • 3.3 kprobe_handler
    • 3.4 setjmp_pre_handler
    • 3.5 jprobe_return
    • 3.6 longjmp_break_handler
  • 四、Deprecated Features
  • 五、使用 perf-probe 获取函数参数
  • 总结
  • 参考资料

前言

现在介绍jprobe。kprobe提供三种探测手段:kprobe、kretprobe和jprobe,其中kretprobe和jprobe基于kprobe实现,分别应用于不同探测场景中。

kretprobe基于kprobe实现,用于获取被探测函数的返回值。
jprobe基于kprobe实现,它用于获取被探测函数的入参值。

高版本的内核已经不支持 jprobe,我使用的是centos 7进行代码测试和 3.10.0 内核源码分析。

一、demo

1.1 demo演示

仍然拿_do_fork() 举例,_do_fork() 是Linux内核用来创建新任务的函数接口。

由于jprobe使用来获取探测函数的入参值,主要看一下_do_fork函数的参数:

long _do_fork(unsigned long clone_flags,unsigned long stack_start,unsigned long stack_size,int __user *parent_tidptr,int __user *child_tidptr,unsigned long tls)
/** Here's a sample kernel module showing the use of jprobes to dump* the arguments of _do_fork().** For more information on theory of operation of jprobes, see* Documentation/kprobes.txt** Build and insert the kernel module as done in the kprobe example.* You will see the trace data in /var/log/messages and on the* console whenever _do_fork() is invoked to create a new process.* (Some messages may be suppressed if syslogd is configured to* eliminate duplicate messages.)*/#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>/** Jumper probe for _do_fork.* Mirror principle enables access to arguments of the probed routine* from the probe handler.*//* Proxy routine having the same arguments as actual _do_fork() routine */
static long j_do_fork(unsigned long clone_flags, unsigned long stack_start,unsigned long stack_size, int __user *parent_tidptr,int __user *child_tidptr, unsigned long tls)
{pr_info("jprobe: clone_flags = 0x%lx, stack_start = 0x%lx ""stack_size = 0x%lx\n", clone_flags, stack_start, stack_size);/* Always end with a call to jprobe_return(). */jprobe_return();return 0;
}static struct jprobe my_jprobe = {.entry          = j_do_fork,.kp = {.symbol_name   = "_do_fork",},
};static int __init jprobe_init(void)
{int ret;ret = register_jprobe(&my_jprobe);if (ret < 0) {pr_err("register_jprobe failed, returned %d\n", ret);return -1;}pr_info("Planted jprobe at %p, handler addr %p\n",my_jprobe.kp.addr, my_jprobe.entry);return 0;
}static void __exit jprobe_exit(void)
{unregister_jprobe(&my_jprobe);pr_info("jprobe at %p unregistered\n", my_jprobe.kp.addr);
}module_init(jprobe_init)
module_exit(jprobe_exit)
MODULE_LICENSE("GPL");

结果:

(1)jprobe处理程序例程 j_do_fork 要和 待探测的内核函数 _do_fork()例程具有相同类型的参数和返回值类型。
(2)jprobe处理程序例程 j_do_fork执行完毕以后,必须调用jprobe_return函数结尾。

1.2 struct jprobe

/** Special probe type that uses setjmp-longjmp type tricks to resume* execution at a specified entry with a matching prototype corresponding* to the probed function - a trick to enable arguments to become* accessible seamlessly by probe handling logic.* Note:* Because of the way compilers allocate stack space for local variables* etc upfront, regardless of sub-scopes within a function, this mirroring* principle currently works only for probes placed on function entry points.*/
struct jprobe {struct kprobe kp;void *entry;    /* probe handling code to jump to */
};/* For backward compatibility with old code using JPROBE_ENTRY() */
#define JPROBE_ENTRY(handler)   (handler)

由于jprobe基于kprobe实现的,可以看到struct jprobe结构体中内嵌了struct kprobe结构体。

static struct jprobe my_jprobe = {.entry            = j_do_fork,.kp = {.symbol_name   = "_do_fork",},
};
struct kprobe {....../* Allow user to indicate symbol name of the probe point */const char *symbol_name;......
};

二、jprobe 原理

2.1 原理简介

jprobe是使用放置在函数入口点上的kprobe实现的,它采用简单的镜像原理,允许无缝访问被探测函数的参数。jprobe处理程序例程应该与被探测的函数具有相同的签名(参数列表和返回类型),并且必须始终以调用Kprobes函数jprobe_return()结束。

当探针(probe)被击中时,Kprobes会复制保存的寄存器和堆栈的一部分,然后,Kprobes将保存的指令指针指向jprobe的处理程序例程,并从陷阱(trap)中返回。因此,控制权传递给处理程序,处理程序与被探测函数具有相同的寄存器和堆栈内容。完成后,处理程序调用jprobe_return(),再次捕获以恢复原始堆栈内容和处理器状态,并切换到被探测的函数。

按照惯例,被调用者拥有自己的参数,因此gcc可能会生成意外修改堆栈中该部分的代码。这就是Kprobes保存堆栈副本并在jprobe处理程序运行后恢复堆栈的原因。最多复制MAX_STACK_SIZE字节。

注意,被探测函数的参数可以在堆栈或寄存器中传递。无论哪种情况,只要处理程序的原型与被探测函数的原型匹配,jprobe都可以工作。

1.2 原理详解

JProbe必须将控制权转移到与放置探针的功能具有相同原型的另一个功能,然后将控制权以与执行JProbe之前相同的状态返回到原始功能。JProbe利用KProbe使用的机制。JProbe不调用用户定义的 pre-handler,而是指定自己的 pre-handler 函数setjmp_pre_handler(),并使用另一个处理函数break_handler。分为三个过程:

(1)第一步:当断点被命中时,控制到达kprobe_handler(),它调用jprobe pre-handler(setjmp_pre_handler())。这将在将rip(指令寄存器:指向下一条即将执行指令的地址)更改为用户定义函数的地址之前保存堆栈内容和寄存器。然后它返回1,告诉kprobe_handler() 简单地返回,而不是像kprobe那样设置单步执行。返回时,control 到达用户定义的函数以访问原始函数的参数。当用户定义的函数完成时,它调用jprobe_return() 而不是执行正常返回。

(2)第二步:jprobe_return() 截断当前堆栈帧并生成一个断点,该断点通过do_int3() 将控制权传递给kprobe_handler()。kprobe_handler() 发现生成的断点地址(jprobe_handller() 中int3指令的地址)没有注册的探测点,但是KProbes在当前CPU上处于活动状态。它假定断点必须是由 jprobes 生成的,因此调用了先前保存的current_kprobe的break_handler。break_handler还原在将控制权传递给用户定义函数之前保存的堆栈内容和寄存器,并返回。

(3)第三步:kprobe_handler() 然后设置jprobe指令的单步执行,后续与kprobe原理相同。

备注:jprobe 的 pre-handler函数:setjmp_pre_handler() 和 break_handler函数:longjmp_break_handler(),这两个函数不是用户定义的函数,而是在内核中已经设置好了,setjmp_pre_handler()负责在遇到探测点保存原始调用上下文,longjmp_break_handler()负责在离开探测点时恢复原始调用上下文。

上下文就是指探测函数点的一系列寄存器和堆栈内容,通过上下文信息返回原来的探测点正常执行路径。
jprobe需要保存探测点上下文信息,而kprobe不需要。

三、源码解析

3.1 struct jprobe

/** Special probe type that uses setjmp-longjmp type tricks to resume* execution at a specified entry with a matching prototype corresponding* to the probed function - a trick to enable arguments to become* accessible seamlessly by probe handling logic.* Note:* Because of the way compilers allocate stack space for local variables* etc upfront, regardless of sub-scopes within a function, this mirroring* principle currently works only for probes placed on function entry points.*/
struct jprobe {struct kprobe kp;void *entry;    /* probe handling code to jump to */
};/* For backward compatibility with old code using JPROBE_ENTRY() */
#define JPROBE_ENTRY(handler)   (handler)

struct jprobe 是一种特殊的探测类型,它使用setjmp-longjmp类型技巧,以与被探测函数对应的匹配原型在指定的条目处恢复执行,这是一种技巧,使参数能够通过探测处理逻辑无缝访问。
由于编译器为本地变量等预先分配堆栈空间的方式,无论函数内的子作用域如何,这种镜像原则目前只适用于放置在函数入口点上的探测。

struct jprobe 用到的struct kprobe 的相关成员如下:

struct kprobe {....../* Allow user to indicate symbol name of the probe point */const char *symbol_name;....../* Called before addr is executed. */kprobe_pre_handler_t pre_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;......
};

3.2 register_jprobe

int __kprobes register_jprobe(struct jprobe *jp)
{return register_jprobes(&jp, 1);
}
EXPORT_SYMBOL_GPL(register_jprobe);
unsigned long __weak arch_deref_entry_point(void *entry)
{return (unsigned long)entry;
}int __kprobes register_jprobes(struct jprobe **jps, int num)
{struct jprobe *jp;int ret = 0, i;if (num <= 0)return -EINVAL;for (i = 0; i < num; i++) {unsigned long addr, offset;jp = jps[i];addr = arch_deref_entry_point(jp->entry);/* Verify probepoint is a function entry point */if (kallsyms_lookup_size_offset(addr, NULL, &offset) &&offset == 0) {jp->kp.pre_handler = setjmp_pre_handler;jp->kp.break_handler = longjmp_break_handler;ret = register_kprobe(&jp->kp);} elseret = -EINVAL;if (ret < 0) {if (i > 0)unregister_jprobes(jps, i);break;}}return ret;
}
EXPORT_SYMBOL_GPL(register_jprobes);

(1)从jp->entry中取出探测回调函数的地址,调用 kallsyms_lookup_size_offset 验证 probe point是否为函数入口点。kallsyms_lookup_size_offset函数的作用是从内核或者模块的符号表中找到addr地址所在的符号,找到后会通过offset值返回addr与符号起始的偏移,这偏移值必须为0,即必须为一个函数的入口。

(2)如果探测回调函数是函数的入口,则设置 struct jprobe 的 struct kprobe成员变量 pre_handler 为 setjmp_pre_handler,break_handler为 longjmp_break_handler,调用 register_kprobe 注册 kprobe点。

3.3 kprobe_handler

当断点被命中时,即执行了int 3指令,将会调用 do_int3 函数:

/* May run on IST stack. */
dotraplinkage void __kprobes notrace do_int3(struct pt_regs *regs, long error_code)
{enum ctx_state prev_state;......//通知链机制,执行注册的通知链:kprobe_exceptions_notify函数if (notify_die(DIE_INT3, "int3", regs, error_code, X86_TRAP_BP,SIGTRAP) == NOTIFY_STOP)goto exit;......
exit:exception_exit(prev_state);
}

jprobe用到主要是 int3 指令,执行int3 指令时,会调用kprobe_exceptions_notify函数,进而调用 kprobe_handler 函数:

// linux-3.10/arch/x86/kernel/kprobes/core.c/** Interrupts are disabled on entry as trap3 is an interrupt gate and they* remain disabled throughout this function.*/
static int __kprobes kprobe_handler(struct pt_regs *regs)
{kprobe_opcode_t *addr;struct kprobe *p;struct kprobe_ctlblk *kcb;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();p = get_kprobe(addr);if (p) {if (kprobe_running()) {if (reenter_kprobe(p, regs, kcb))return 1;} 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.*/if (!p->pre_handler || !p->pre_handler(p, regs))setup_singlestep(p, regs, kcb, 0);return 1;}} else if (*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();return 1;} else if (kprobe_running()) {p = __this_cpu_read(current_kprobe);if (p->break_handler && p->break_handler(p, regs)) {if (!skip_singlestep(p, regs, kcb))setup_singlestep(p, regs, kcb, 0);return 1;}} /* else: not a kprobe fault; let the kernel handle it */preempt_enable_no_resched();return 0;
}/** Wrapper routine for handling exceptions.*/
int __kprobes
kprobe_exceptions_notify(struct notifier_block *self, unsigned long val, void *data)
{struct die_args *args = data;int ret = NOTIFY_DONE;if (args->regs && user_mode_vm(args->regs))return ret;switch (val) {case DIE_INT3:if (kprobe_handler(args->regs))ret = NOTIFY_STOP;break;case DIE_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;case DIE_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;}return ret;
}

注册内核通知链:kprobe_exceptions_nb,注释标明了该通知链最高,最先被调用,执行被探测指令期间若发生了内存异常,比如执行了int3指令, 将最优先调用kprobe_exceptions_notify函数。

// linux-3.10/kernel/kprobes.cstatic struct notifier_block kprobe_exceptions_nb = {.notifier_call = kprobe_exceptions_notify,.priority = 0x7fffffff /* we need to be notified first */
};static int __init init_kprobes(void)
{......//注册kprobe_exceptions_nb函数register_die_notifier(&kprobe_exceptions_nb);......
}//kprobes作为一个模块,其初始化函数为init_kprobes
module_init(init_kprobes);
// linux-3.10/kernel/notifier.c/*** notifier_call_chain - Informs the registered notifiers about an event.*  @nl:       Pointer to head of the blocking notifier chain* @val:      Value passed unmodified to notifier function*   @v:        Pointer passed unmodified to notifier function* @nr_to_call:   Number of notifier functions to be called. Don't care*         value of this parameter is -1.* @nr_calls: Records the number of notifications sent. Don't care*          value of this field is NULL.*   @returns:  notifier_call_chain returns the value returned by the*          last notifier function called.*/
static int __kprobes notifier_call_chain(struct notifier_block **nl,unsigned long val, void *v,int nr_to_call,  int *nr_calls)
{int ret = NOTIFY_DONE;struct notifier_block *nb, *next_nb;nb = rcu_dereference_raw(*nl);while (nb && nr_to_call) {next_nb = rcu_dereference_raw(nb->next);#ifdef CONFIG_DEBUG_NOTIFIERSif (unlikely(!func_ptr_is_kernel_text(nb->notifier_call))) {WARN(1, "Invalid notifier called!");nb = next_nb;continue;}
#endifret = nb->notifier_call(nb, val, v);if (nr_calls)(*nr_calls)++;if ((ret & NOTIFY_STOP_MASK) == NOTIFY_STOP_MASK)break;nb = next_nb;nr_to_call--;}return ret;
}/***   __atomic_notifier_call_chain - Call functions in an atomic notifier chain*  @nh: Pointer to head of the atomic notifier chain* @val: Value passed unmodified to notifier function*    @v: Pointer passed unmodified to notifier function*    @nr_to_call: See the comment for notifier_call_chain.* @nr_calls: See the comment for notifier_call_chain.**  Calls each function in a notifier chain in turn.  The functions*    run in an atomic context, so they must not block.*  This routine uses RCU to synchronize with changes to the chain.**   If the return value of the notifier can be and'ed* with %NOTIFY_STOP_MASK then atomic_notifier_call_chain()*   will return immediately, with the return value of*  the notifier function which halted execution.*  Otherwise the return value is the return value* of the last notifier function called.*/
int __kprobes __atomic_notifier_call_chain(struct atomic_notifier_head *nh,unsigned long val, void *v,int nr_to_call, int *nr_calls)
{int ret;rcu_read_lock();ret = notifier_call_chain(&nh->head, val, v, nr_to_call, nr_calls);rcu_read_unlock();return ret;
}
EXPORT_SYMBOL_GPL(__atomic_notifier_call_chain);int __kprobes atomic_notifier_call_chain(struct atomic_notifier_head *nh,unsigned long val, void *v)
{return __atomic_notifier_call_chain(nh, val, v, -1, NULL);
}
EXPORT_SYMBOL_GPL(atomic_notifier_call_chain);int notrace __kprobes notify_die(enum die_val val, const char *str,struct pt_regs *regs, long err, int trap, int sig)
{struct die_args args = {.regs = regs,.str    = str,.err = err,.trapnr  = trap,.signr  = sig,};return atomic_notifier_call_chain(&die_chain, val, &args);
}
int3-->do_int3-->notify_die(DIE_INT3, "int3", regs, error_code, X86_TRAP_BP,SIGTRAP) == NOTIFY_STOP)-->kprobe_exceptions_notify(){case DIE_INT3:if (kprobe_handler(args->regs))ret = NOTIFY_STOP;break;}

3.4 setjmp_pre_handler

// linux-3.10/arch/x86/include/asm/ptrace.hstruct pt_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;
/* end of arguments */
/* cpu exception frame or undefined */unsigned long ip;unsigned long cs;unsigned long flags;unsigned long sp;unsigned long ss;
/* top of stack page */
};
// linux-3.10/arch/x86/kernel/kprobes/core.cDEFINE_PER_CPU(struct kprobe_ctlblk, kprobe_ctlblk);
// linux-3.10/arch/x86/include/asm/kprobes.h#ifdef CONFIG_KPROBES
DECLARE_PER_CPU(struct kprobe_ctlblk, kprobe_ctlblk);/* per-cpu kprobe control block */
struct kprobe_ctlblk {......unsigned long *jprobe_saved_sp;struct pt_regs jprobe_saved_regs;kprobe_opcode_t jprobes_stack[MAX_STACK_SIZE];......
};

jprobe_saved_sp用于保存sp寄存器信息,用来存储栈顶地址。
jprobe_saved_regs用于保存寄存器信息。
jprobes_stack则用于保存堆栈信息。

struct kprobe_ctlblk 是一个 per-cpu变量:

[root@localhost ~]# cat /proc/kallsyms | grep __per_cpu_start
0000000000000000 A __per_cpu_start
[root@localhost ~]# cat /proc/kallsyms | grep kprobe_ctlblk
0000000000013360 A kprobe_ctlblk
[root@localhost ~]# cat /proc/kallsyms | grep __per_cpu_end
000000000001d000 A __per_cpu_end
// linux-3.10/include/linux/kprobes.hstatic inline struct kprobe_ctlblk *get_kprobe_ctlblk(void)
{return (&__get_cpu_var(kprobe_ctlblk));
}
// linux-3.10/arch/x86/kernel/kprobes/core.cint __kprobes setjmp_pre_handler(struct kprobe *p, struct pt_regs *regs)
{struct jprobe *jp = container_of(p, struct jprobe, kp);unsigned long addr;struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();//保存探测点上下文信息:寄存器和堆栈内容kcb->jprobe_saved_regs = *regs;kcb->jprobe_saved_sp = stack_addr(regs);//获取栈顶地址addr = (unsigned long)(kcb->jprobe_saved_sp);/** As Linus pointed out, gcc assumes that the callee* owns the argument space and could overwrite it, e.g.* tailcall optimization. So, to be absolutely safe* we also save and restore enough stack bytes to cover* the argument area.*/memcpy(kcb->jprobes_stack, (kprobe_opcode_t *)addr,MIN_STACK_SIZE(addr));regs->flags &= ~X86_EFLAGS_IF;//关闭中断trace_hardirqs_off();regs->ip = (unsigned long)(jp->entry);return 1;
}

当断点被命中时,即执行了int 3指令,控制到达kprobe_handler(),它将会调用jprobe pre-handler即 setjmp_pre_handler() 函数,此函数在用户预定义的函数之前先执行,对于我们的示例中就是 j_do_fork() ,setjmp_pre_handler() 函数在 j_do_fork() 之前执行,setjmp_pre_handler保存了探测点上下文信息后,就将指令寄存去 ip 指向用户态回调函数 jp->entry,然后返回1,这里返回1,在kprobe_handler中会跳过单步模式,在kprobe_handler函数中不会执行setup_singlestep函数了。在jprobe 执行 break_handler :longjmp_break_handler函数 之前 跳过单步模式。之后再执行用户态回调函数 jp->entry :j_do_fork,获取相应的参数值。

备注:这里探测函数(j_do_fork)是在kprobe_handler流程执行完成后跳转执行的,跳过了single_step流程,这也就说它不能利用原有kprobe的机制回到原始执行流程中去执行。

kprobe不需要保存原上下文信息。

// linux-3.10/arch/x86/kernel/kprobes/core.c/** Interrupts are disabled on entry as trap3 is an interrupt gate and they* remain disabled throughout this function.*/
static int __kprobes kprobe_handler(struct pt_regs *regs)
{....../** 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.*/if (!p->pre_handler || !p->pre_handler(p, regs))//在jprobe 执行 break_handler 之前 跳过单步模式setup_singlestep(p, regs, kcb, 0);return 1;......
}

执行用户自定义函数后,调用 jprobe_return 函数。

从setjmp_pre_handler的实现可以看出,该函数仅仅修改了kprobe的返回地址,并没有修改栈和其他的寄存器值,因此在CPU跳转到用户自定义函数:j_do_fork执行时,它的寄存器和栈中的内容同原本调用_do_fork函数时几乎是一模一样的(仅仅是禁用了中断而已),因此不论是通过寄存器传参还是通过压栈的方式传参,用户在定义jdo_fork函数时只需要将函数入参定义的同do_fork一样就可以轻轻松松的获取到原有的入参值了

3.5 jprobe_return

// linux-3.10/arch/x86/kernel/kprobes/core.cvoid __kprobes jprobe_return(void)
{struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();asm volatile (
#ifdef CONFIG_X86_64"       xchg   %%rbx,%%rsp \n"
#else"       xchgl   %%ebx,%%esp   \n"
#endif"       int3         \n""       .globl jprobe_return_end\n""       jprobe_return_end:    \n""       nop            \n"::"b"(kcb->jprobe_saved_sp):"memory");
}

在 jprobe_return 执行 int 3指令:

/* May run on IST stack. */
dotraplinkage void __kprobes notrace do_int3(struct pt_regs *regs, long error_code)
{......if (notify_die(DIE_INT3, "int3", regs, error_code, X86_TRAP_BP,SIGTRAP) == NOTIFY_STOP)goto exit;......
}
int3-->do_int3-->notify_die(DIE_INT3, "int3", regs, error_code, X86_TRAP_BP,SIGTRAP) == NOTIFY_STOP)-->kprobe_exceptions_notify(){case DIE_INT3:if (kprobe_handler(args->regs))ret = NOTIFY_STOP;break;}
/** Interrupts are disabled on entry as trap3 is an interrupt gate and they* remain disabled throughout this function.*/
static int __kprobes kprobe_handler(struct pt_regs *regs)
{......} else if (kprobe_running()) {p = __this_cpu_read(current_kprobe);if (p->break_handler && p->break_handler(p, regs)) {if (!skip_singlestep(p, regs, kcb))setup_singlestep(p, regs, kcb, 0);return 1;}} /* else: not a kprobe fault; let the kernel handle it */......
}

执行 break_handler 函数:longjmp_break_handler,之后在设置为单步执行模式,与kprobe后续一致。

3.6 longjmp_break_handler

int __kprobes longjmp_break_handler(struct kprobe *p, struct pt_regs *regs)
{struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();u8 *addr = (u8 *) (regs->ip - 1);struct jprobe *jp = container_of(p, struct jprobe, kp);if ((addr > (u8 *) jprobe_return) &&(addr < (u8 *) jprobe_return_end)) {if (stack_addr(regs) != kcb->jprobe_saved_sp) {struct pt_regs *saved_regs = &kcb->jprobe_saved_regs;printk(KERN_ERR"current sp %p does not match saved sp %p\n",stack_addr(regs), kcb->jprobe_saved_sp);printk(KERN_ERR "Saved registers for jprobe %p\n", jp);show_regs(saved_regs);printk(KERN_ERR "Current registers\n");show_regs(regs);BUG();}*regs = kcb->jprobe_saved_regs;memcpy((kprobe_opcode_t *)(kcb->jprobe_saved_sp),kcb->jprobes_stack,MIN_STACK_SIZE(kcb->jprobe_saved_sp));preempt_enable_no_resched();return 1;}return 0;
}

恢复探测点上下文:寄存器和堆栈内容,打开内核抢占。然后后面开始执行单步模式,恢复 kprobe 原有执行路径。

四、Deprecated Features

Jprobes现在已被弃用。依赖它的人应该迁移到其他跟踪功能或使用较旧的内核。请考虑将工具迁移到以下选项之一:

(1) 使用 trace-event 跟踪带有参数的目标函数
trace-event 是一个低开销的静态定义的事件接口(如果关闭,几乎没有可见的开销)。您可以定义新事件,并通过ftrace或任何其他跟踪工具对其进行跟踪。
请参考:
https://lwn.net/Articles/379903/
https://lwn.net/Articles/381064/
https://lwn.net/Articles/383362/

(2) 将ftrace动态事件(kprobe event)与 perf-probe 一起使用
如果使用调试信息(CONFIG_debug_info=y)构建内核,则可以使用 perf-probe 查找分配给哪个本地变量或参数的寄存器/堆栈,并设置新事件来跟踪它。
请参考:
https://static.lwn.net/kerneldoc/trace/kprobetrace.html
https://static.lwn.net/kerneldoc/trace/events.html
tools/perf/Documentation/perf-probe.txt

五、使用 perf-probe 获取函数参数

(1)从调试符号表中查询 do_sys_open 的所有参数:perf probe -V do_sys_open ,如果这个命令执行失败,就说明调试符号表还没有安装, yum --enablerepo=base-debuginfo install -y kernel-debuginfo-$(uname -r)。

[root@localhost ~]# perf probe -V do_sys_open
Available variables at do_sys_open@<do_sys_open+0>char*   filenameint     dfdint     flagsstruct open_flags       opumode_t mode

(2)找出参数名称和类型后,就可以把参数加到探针中了。
获取参数:filename:string

(3)添加带参数的探针:perf probe --add ‘do_sys_open filename:string’

[root@localhost ~]# perf probe --add 'do_sys_open filename:string'
Added new event:probe:do_sys_open    (on do_sys_open with filename:string)You can now use it in all perf tools, such as:perf record -e probe:do_sys_open -aR sleep 1

(4)采样记录

perf record -e probe:do_sys_open -aR ls
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.557 MB perf.data (12 samples) ]

(5)查看结果

[root@localhost ~]# perf scriptperf 17873 [003] 372605.184198: probe:do_sys_open: (ffffffff95c3fdf0) filename_string="/proc/17880/status"ls 17880 [001] 372605.184591: probe:do_sys_open: (ffffffff95c3fdf0) filename_string="/etc/ld.so.cache"ls 17880 [001] 372605.184609: probe:do_sys_open: (ffffffff95c3fdf0) filename_string="/lib64/libselinux.so.1"ls 17880 [001] 372605.184642: probe:do_sys_open: (ffffffff95c3fdf0) filename_string="/lib64/libcap.so.2"ls 17880 [001] 372605.184664: probe:do_sys_open: (ffffffff95c3fdf0) filename_string="/lib64/libacl.so.1"ls 17880 [001] 372605.184686: probe:do_sys_open: (ffffffff95c3fdf0) filename_string="/lib64/libc.so.6"ls 17880 [001] 372605.184713: probe:do_sys_open: (ffffffff95c3fdf0) filename_string="/lib64/libpcre.so.1"ls 17880 [001] 372605.184734: probe:do_sys_open: (ffffffff95c3fdf0) filename_string="/lib64/libdl.so.2"ls 17880 [001] 372605.184757: probe:do_sys_open: (ffffffff95c3fdf0) filename_string="/lib64/libattr.so.1"ls 17880 [001] 372605.184777: probe:do_sys_open: (ffffffff95c3fdf0) filename_string="/lib64/libpthread.so.0"ls 17880 [001] 372605.185151: probe:do_sys_open: (ffffffff95c3fdf0) filename_string=""ls 17880 [001] 372605.185203: probe:do_sys_open: (ffffffff95c3fdf0) filename_string="."

(6)删除探针

[root@localhost ~]# perf probe --del probe:do_sys_open
Removed event: probe:do_sys_open

总结

jprobe基于kprobe实现,不能在函数的任意位置插入探测点,只能在函数的入口处探测,一般用于获取函数的入参值,一个被探测函数点只能注册一个jprobe。

高版本已经不推荐使用jprobe。

参考资料

https://lwn.net/Articles/132196/
https://blog.csdn.net/luckyapple1028/article/details/54350410
https://blog.csdn.net/qq_34908601/article/details/123772569
https://cloud.tencent.com/developer/article/1462867

Linux jprobe的使用和原理相关推荐

  1. 超专业解析!10分钟带你搞懂Linux中直接I/O原理

    导语 | 本文主要以一张图为基础,向大家介绍Linux在I/O上做了哪些事情,即Linux中直接I/O原理,希望本文的经验和思路能为读者提供一些帮助和思考. 引言 我们先看一张图: 这张图大体上描述了 ...

  2. linux绑定盘符吗,Linux盘符绑定实现原理.PDF

    Linux盘符绑定实现原理 Linux盘符绑定实现原理 正一 2016.7.25 目 录  Linux盘符的分配  Linux内核IDR机制  Linux盘符绑定 Linux盘符的分配 sd_ ...

  3. Linux 写时复制机制原理

    在 Linux 系统中,调用 fork 系统调用创建子进程时,并不会把父进程所有占用的内存页复制一份,而是与父进程共用相同的内存页,而当子进程或者父进程对内存页进行修改时才会进行复制 -- 这就是著名 ...

  4. 【Linux内核】内存映射原理

    [Linux内核]内存映射原理 物理地址空间 物理地址是处理器在总线上能看到的地址,使用RISC(Reduced Instruction Set Computing精简指令集)的处理器通常只实现一个物 ...

  5. Linux select函数用法和原理

    select函数的用法和原理 Linux上的select函数 select函数用于检测一组socket中是否有事件就绪.这里的事件为以下三类: 读事件就绪 在socket内核中,接收缓冲区中的字节数大 ...

  6. linux shell 原理,linux下shell的工作原理

    linux下shell的工作原理 2009-12-8 10:19:53   出处:https://www.yqdown.com shell是用户和Linux操作系统之间的接口.Linux中有多种she ...

  7. linux下文件删除的原理精华讲解(考试题答案系列)

    说明:本文为老男孩linux培训某节课前考试试题及答案分享博文内容的一部分,也是独立成题的,你可以点下面地址查看全部的内容信息.http://oldboy.blog.51cto.com/2561410 ...

  8. linux设备驱动程序架构的研究,Linux设备驱动程序学习(12)-Linux设备模型(底层原理简介)...

    Linux设备驱动程序学习(12) -Linux设备模型(底层原理简介) 以<LDD3>的说法:Linux设备模型这部分内容可以认为是高级教材,对于多数程序作者来说是不必要的.但是我个人认 ...

  9. 用以促学——Linux进程后台运行的原理、方法、比较及其实现

    用以促学--Linux进程后台运行的原理.方法.比较及其实现 文章目录 用以促学--Linux进程后台运行的原理.方法.比较及其实现 前言 相关基础知识 应用场景 问题所在 linux概念说明 ses ...

最新文章

  1. NetBeans 时事通讯(刊号 # 103 - May 18, 2010)
  2. System Monitor ArcGIS系统监控利器
  3. mysql创建读写账号_mysql创建读写账号及服务相关优化配置
  4. MSSQL-SQL SERVER 分页原理
  5. hibernate 一对多_为什么很多人不愿意用hibernate了?
  6. java strtus2 DynamicMethodInvocation配置(二)
  7. 99se 设计4层板的设置方法
  8. html中div中文字如何上下居中,div中文字各种垂直居中的方法
  9. 《韩立刚计算机网络》第一章
  10. 多线程m3u8下载器 v1.0
  11. android eclipse三合一,创新巅峰之作全能型Orbitrap Eclipse三合一质谱仪
  12. 怎么看计算机的a卡右键找不到,右键没有显卡,右键没有amd显卡选项
  13. 自己DIY一个mp3播放器
  14. Aqara? 华为?智汀?要真的实现万物互联了吗?
  15. <<2020云南省青少年创意编程与智能设计大赛>>参赛作品之变身魔药实验编程设计说明
  16. 基于logistics回归的评分卡模型【相关理论】
  17. 基于Python,从零开始,裸写一套期权定价程序
  18. vue使用 moment.js 格式化时间(获取当前日期的周一和周日)
  19. 在linux下解压rar文件
  20. redis发布订阅与集群

热门文章

  1. 工作流引擎会签,加签,主持人,组长模式专题讲解
  2. video增加中间播放按钮
  3. 扒一扒 FM 算法的实现
  4. MUI在搜索框输入内容后,将手机软键盘右下角的换行变成搜索
  5. Python中list的内存分配
  6. 7个关键词带你看小米空净
  7. js打印分页、加标题、每页控制条数
  8. 托马斯和朋友儿童智能健康牙刷今日发布
  9. mysql to_day函数_mysql 日期函数to_days注意事项
  10. Java 反射机制快速入门及常见方法全归纳。