linux kprobe

  • 了解linux kprobe

1.What is kprobes?

  kprobe是一个轻量级的内核调试工具,也是其他更高级的内核调试(如perf和systemtap的基础)。kprobes 主要用来对内核进行调试追踪, 属于比较轻量级的机制, 本质上是在指定的探测点(比如函数的某行, 函数的入口地址和出口地址, 或者内核的指定地址处)插入一组处理程序. 内核执行到这组处理程序的时候就可以获取到当前正在执行的上下文信息, 比如当前的函数名, 函数处理的参数以及函数的返回值, 也可以获取到寄存器甚至全局数据结构的信息.

  kprobe可以在运行的内核中动态插入探测点,执行你预定义的操作。用户指定一个探测点,并把一个用户定义的处理函数关联到该探测点, 当内核执行到该探测点时, 相应的关联函数被执行,然后继续执行正常的代码路径。

  kprobe 实现了三种类型的探测点:

  • kprobes 是可以被插入到内核的任何指令位置的探测点
  • jprobes 则只能被插入到一个内核函数的入口
  • kretprobes 则是在指定的内核函数返回时才被执行。kretprobes 在 kprobes 的机制上实现, 主要用于返回点(比如内核函数或者系统调用的返回值)的探测以及函数执行耗时的计算.

在这里插入图片描述

Note:
  uprobes 机制类似 kprobes, 不过主要用户空间的追踪调试. 另外 uprobes 应该主要是由 systemtap 实现并完善.

1.1.kprobes技术背景

  开发人员在内核或者模块的调试过程中,往往会需要要知道其中的一些函数有无被调用、何时被调用、执行是否正确以及函数的入参和返回值是什么等等。比较简单的做法是在内核代码对应的函数中添加日志打印信息,但这种方式往往需要重新编译内核或模块,重新启动设备之类的,操作较为复杂甚至可能会破坏原有的代码执行过程。

  而利用kprobes技术,用户可以定义自己的回调函数,然后在内核或者模块中几乎所有的函数中(有些函数是不可探测的,例如kprobes自身的相关实现函数,后文会有详细说明)动态的插入探测点,当内核执行流程执行到指定的探测函数时,会调用该回调函数,用户即可收集所需的信息了,同时内核最后还会回到原本的正常执行流程。如果用户已经收集足够的信息,不再需要继续探测,则同样可以动态的移除探测点。因此kprobes技术具有对内核执行流程影响小和操作方便的优点。

1.2.kprobes的特点与使用限制:

  • 1、kprobes允许在同一个被被探测位置注册多个kprobe,但是目前jprobe却不可以;同时也不允许以其他的jprobe回掉函数和kprobe的post_handler回调函数作为被探测点。

  • 2、一般情况下,可以探测内核中的任何函数,包括中断处理函数。不过在kernel/kprobes.c和arch/*/kernel/kprobes.c程序中用于实现kprobes自身的函数是不允许被探测的,另外还有do_page_fault和notifier_call_chain;

  • 3、如果以一个内联函数为探测点,则kprobes可能无法保证对该函数的所有实例都注册探测点。由于gcc可能会自动将某些函数优化为内联函数,因此可能无法达到用户预期的探测效果;

  • 4、一个探测点的回调函数可能会修改被探测函数运行的上下文,例如通过修改内核的数据结构或者保存与struct pt_regs结构体中的触发探测之前寄存器信息。因此kprobes可以被用来安装bug修复代码或者注入故障测试代码;

  • 5、kprobes会避免在处理探测点函数时再次调用另一个探测点的回调函数,例如在printk()函数上注册了探测点,则在它的回调函数中可能再次调用printk函数,此时将不再触发printk探测点的回调,仅仅时增加了kprobe结构体中nmissed字段的数值;

  • 6、在kprobes的注册和注销过程中不会使用mutex锁和动态的申请内存;

  • 7、kprobes回调函数的运行期间是关闭内核抢占的,同时也可能在关闭中断的情况下执行,具体要视CPU架构而定。因此不论在何种情况下,在回调函数中不要调用会放弃CPU的函数(如信号量、mutex锁等);

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

  • 9、如果一个函数的调用此处和返回次数不相等,则在类似这样的函数上注册kretprobe将可能不会达到预期的效果,例如do_exit()函数会存在问题,而do_execve()函数和do_fork()函数不会;

  • 10、如果当在进入和退出一个函数时,CPU运行在非当前任务所有的栈上,那么往该函数上注册kretprobe可能会导致不可预料的后果,因此,kprobes不支持在X86_64的结构下为__switch_to()函数注册kretprobe,将直接返回-EINVAL。

2.kprobe原理

  具体流程见下图:
在这里插入图片描述

  • 1.当用户注册一个探测点后,kprobe首先备份被探测点的对应指令,然后将原始指令的入口点替换为断点指令,该指令是CPU架构相关的,如i386和x86_64是int3,arm是设置一个未定义指令(目前的x86_64架构支持一种跳转优化方案Jump Optimization,内核需开启CONFIG_OPTPROBES选项,该种方案使用跳转指令来代替断点指令);

  • 2.当CPU流程执行到探测点的断点指令时,就触发了一个trap,在trap处理流程中会保存当前CPU的寄存器信息并调用对应的trap处理函数,该处理函数会设置kprobe的调用状态并调用用户注册的pre_handler回调函数,kprobe会向该函数传递注册的struct kprobe结构地址以及保存的CPU寄存器信息;

  • 3.随后kprobe单步执行前面所拷贝的被探测指令,具体执行方式各个架构不尽相同,arm会在异常处理流程中使用模拟函数执行,而x86_64架构则会设置单步调试flag并回到异常触发前的流程中执行;

  • 4.在单步执行完成后,kprobe执行用户注册的post_handler回调函数;

  • 5.最后执行流程回到被探测指令之后的正常流程继续执行。

2.KProbes Interface
在这里插入图片描述

2.1.struct kprobe

struct kprobe {
    struct hlist_node hlist;-----------------------------------------------被用于kprobe全局hash,索引值为被探测点的地址。
    /* list of kprobes for multi-handler support */
    struct list_head list;-------------------------------------------------用于链接同一被探测点的不同探测kprobe。
    /*count the number of times this probe was temporarily disarmed */
    unsigned long nmissed;
    /* location of the probe point */
    kprobe_opcode_t *addr;-------------------------------------------------被探测点的地址。
    /* Allow user to indicate symbol name of the probe point */
    const char *symbol_name;-----------------------------------------------被探测函数的名称。
    /* Offset into the symbol */
    unsigned int offset;---------------------------------------------------被探测点在函数内部的偏移,用于探测函数内核的指令,如果该值为0表示函数的入口。
    /* Called before addr is executed. */
    kprobe_pre_handler_t pre_handler;--------------------------------------被探测点指令执行之前调用的回调函数。
    /* Called after addr is executed, unless... */
    kprobe_post_handler_t post_handler;------------------------------------被探测点指令执行之后调用的回调函数。
    kprobe_fault_handler_t fault_handler;----------------------------------在执行pre_handler、post_handler或单步执行被探测指令时出现内存异常则会调用该回调函数。
    kprobe_break_handler_t break_handler;----------------------------------在执行某一kprobe过程中出发了断点指令后会调用该函数,用于实现jprobe。
    kprobe_opcode_t opcode;------------------------------------------------保存的被探测点原始指令。
    struct arch_specific_insn ainsn;---------------------------------------被复制的被探测点的原始指令,用于单步执行,架构强相关。
    u32 flags;-------------------------------------------------------------状态标记。
};

其中各个字段的含义如下:

  • struct hlist_node hlist:被用于kprobe全局hash,索引值为被探测点的地址;

  • struct list_head list:用于链接同一被探测点的不同探测kprobe;

  • kprobe_opcode_t *addr:被探测点的地址;

  • const char *symbol_name:被探测函数的名字;

  • unsigned int offset:被探测点在函数内部的偏移,用于探测函数内部的指令,如果该值为0表示函数的入口;

  • kprobe_pre_handler_t pre_handler:在被探测点指令执行之前调用的回调函数;

  • kprobe_post_handler_t post_handler:在被探测指令执行之后调用的回调函数;

  • kprobe_fault_handler_t fault_handler:在执行pre_handler、post_handler或单步执行被探测指令时出现内存异常则会调用该回调函数;

  • kprobe_break_handler_t break_handler:在执行某一kprobe过程中触发了断点指令后会调用该函数,用于实现jprobe;

  • kprobe_opcode_t opcode:保存的被探测点原始指令;

  • struct arch_specific_insn ainsn:被复制的被探测点的原始指令,用于单步执行,架构强相关(可能包含指令模拟函数);

  • u32 flags:状态标记

涉及的API函数接口如下:

  • int register_kprobe(struct kprobe *kp) //向内核注册kprobe探测点
  • void unregister_kprobe(struct kprobe *kp) //卸载kprobe探测点
  • int register_kprobes(struct kprobe **kps, int num) //注册探测函数向量,包含多个探测点
  • void unregister_kprobes(struct kprobe **kps, int num) //卸载探测函数向量,包含多个探测点
  • int disable_kprobe(struct kprobe *kp) //临时暂停指定探测点的探测
  • int enable_kprobe(struct kprobe *kp) //恢复指定探测点的探测

2.2.KProbes Manager

  The KProbes Manager is responsible for registering and unregistering KProbes and JProbes. The file kernel/kprobes.c implements the KProbes manager. Each probe is described by the struct kprobe structure and stored in a hash table hashed by the address at which the probe is placed. Access to this hash table is serialized by the spinlock kprobe_lock. This spinlock is locked before a new probe is registered, an existing probe is unregistered or when a probe is hit. This prevents these operations from executing simultaneously on a SMP machine. Whenever a probe is hit, the probe handler is called with interrupts disabled. Interrupts are disabled because handling a probe is a multiple step process which involves breakpoint handling and single-step execution of the probed instruction. There is no easy way to save the state between these operations hence interrupts are kept disabled during probe handling.

  The manager is composed of these functions which are followed by a simplified description of what they do. These functions are architecture independent. A side-by-side reading of the code in kernel/kprobes.c and these steps will clarify the whole implementation.

  • void lock_kprobes(void)
    Locks KProbes and records the CPU on which it was locked
  • void unlock_kprobes(void)
    Resets the recorded CPU and unlocks KProbes
  • struct kprobe *get_kprobe(void *addr)
    Using the address of the probed instruction, returns the probe from hash table
  • int register_kprobe(struct kprobe *p)
    This function registers a probe at a given address. Registration involves copying the instruction at the probe address in a probe specific buffer. On x86 the maximum instruction size is 16 bytes hence 16 bytes are copied at the given address. Then it replaces the instruction at the probed address with the breakpoint instruction.
  • void unregister_kprobe(struct kprobe *p)
    This function unregisters a probe. It restores the original instruction at the address and removes the probe structure from the hash table.
  • int register_jprobe(struct jprobe *jp)
    This function registers a JProbe at a function address. JProbes use the KProbes mechanism. In the KProbe pre_handler it stores its own handler setjmp_pre_handler and in the break_handler stores the address of longjmp_break_handler. Then it registers struct kprobe jp->kp by calling - - register_kprobe()
  • void unregister_jprobe(struct jprobe *jp)
    Unregisters the struct kprobe used by this JProbe

2.3.Kprobe config

CONFIG_KPROBES=y
CONFIG_KALLSYMS=y or CONFIG_KALLSYMS_ALL=y

2.4.debugfs Interface

/sys/kernel/debug/kprobes/list: 列出内核中已经设置kprobe断点的函数
/sys/kernel/debug/kprobes/enabled: kprobe开启/关闭开关
/sys/kernel/debug/kprobes/blacklist: kprobe黑名单(无法设置断点函数)
/proc/sys/debug/kprobes-optimization: Turn kprobes optimization ON/OFF.

3.kprobe使用实例

  使用kprobe可以通过两种方式:

  • 第一种是开发人员自行编写内核模块,向内核注册探测点,探测函数可根据需要自行定制,使用灵活方便;
  • 第二种方式是使用kprobes on ftrace,这种方式是kprobe和ftrace结合使用,即可以通过kprobe来优化ftrace来跟踪函数的调用。

3.1. 基于ftrace使用kprobe

  kprobe和内核的ftrac结合使用,需要对内核进行配置,然后添加探测点、进行探测、查看结果。

  内核kprobe配置:

打开"General setup"->“Kprobes”,以及"Kernel hacking"->“Tracers”->“Enable kprobes-based dynamic events”。

3.2.kprobe trace events使用

  kprobe事件相关的节点有如下:

/sys/kernel/debug/tracing/kprobe_events-----------------------配置kprobe事件属性,增加事件之后会在kprobes下面生成对应目录。
/sys/kernel/debug/tracing/kprobe_profile----------------------kprobe事件统计属性文件。
/sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/enabled-------使能kprobe事件
/sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/filter--------过滤kprobe事件
/sys/kernel/debug/tracing/kprobes/<GRP>/<EVENT>/format--------查询kprobe事件显示格式

3.3.基于ftrace使用kprobe动态trace:

// 添加探针
echo 'p:myprobe do_sys_open' > /sys/kernel/debug/tracing/kprobe_events
echo 'r:myretprobe do_sys_open $retval' >> /sys/kernel/debug/tracing/kprobe_events

  执行如上命令生成:

root@uos-PC:/sys/kernel/debug/tracing/events/kprobes# ls
enable filter myprobe myretprobe

// 激活
echo 1 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
echo 1 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable

// 查看输出
cat /sys/kernel/debug/tracing/trace

// 关闭
echo 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enable
echo 0 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enable

// 移除探针
echo -:myprobe >> kprobe_events
echo > /sys/kernel/debug/tracing/kprobe_events

3.4.基于ftrace使用uprobe动态trace(kernel/trace/trace_uprobe.c)

// 添加探针
echo 'p: /bin/bash:0x4245c0' > /sys/kernel/debug/tracing/uprobe_events
echo 'r: /bin/bash:0x4245c0' > /sys/kernel/debug/tracing/uprobe_events

// 激活
echo 1 > events/uprobes/enable

// 查看输出
cat /sys/kernel/debug/tracing/trace

// 关闭
echo 0 > events/uprobes/enable

// 移除
echo '-:bash_0x4245c0' >> /sys/kernel/debug/tracing/uprobe_events
echo > /sys/kernel/debug/tracing/uprobe_events

3.5.基于ftrace使用tracepoints静态events(kernel/trace/trace_events.c)

  • 通常可以写内核模块给某个静态tracepoint添加探针
  • 基于ftrace events
// 添加event
echo sched_wakeup >> /sys/kernel/debug/tracing/set_event
echo *:* > /sys/kernel/debug/tracing/set_event
echo 'irq:*' > /sys/kernel/debug/tracing/set_event

// 激活event
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable

// 查看输出
cat /sys/kernel/debug/tracing/trace

// 移除event
echo '!sched_wakeup' >> /sys/kernel/debug/tracing/set_event
echo > /sys/kernel/debug/tracing/set_event

3.1.kprobe_example.c

  以内核中samples/kprobes/kprobe_example.c为例,介绍如何使用kprobe进行内核函数探测。该kprobe实例实现了_do_fork的探测,该函数会在fork系统调用或者kernel_kthread创建内核线程时被调用。对原%p修改为%pF后,可读性更强。可以显示函数名称以及偏移量。

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

#define MAX_SYMBOL_LEN    64
static char symbol[MAX_SYMBOL_LEN] = "_do_fork";
module_param_string(symbol, symbol, sizeof(symbol), 0644);

/* For each probe you need to allocate a kprobe structure */
static struct kprobe kp = {---------------------------------------------------------定义一个实例kp并初始化symbol_name为"_do_fork",将探测_do_fork函数。
    .symbol_name    = symbol,
};

/* 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
    pr_info("<%s> pre_handler: p->addr = %pF, ip = %lx, flags = 0x%lx\n",
        p->symbol_name, p->addr, regs->ip, regs->flags);
#endif
#ifdef CONFIG_ARM64
    pr_info("<%s> pre_handler: p->addr = %pF, pc = 0x%lx,"
            " pstate = 0x%lx\n",
        p->symbol_name, p->addr, (long)regs->pc, (long)regs->pstate);
#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
    pr_info("<%s> post_handler: p->addr = %pF, flags = 0x%lx\n",
        p->symbol_name, p->addr, regs->flags);
#endif
#ifdef CONFIG_ARM64
    pr_info("<%s> post_handler: p->addr = %pF, pstate = 0x%lx\n",
        p->symbol_name, p->addr, (long)regs->pstate);
#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)
{
    pr_info("fault_handler: p->addr = %pF, 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的三个回调函数。
    kp.post_handler = handler_post;
    kp.fault_handler = handler_fault;

    ret = register_kprobe(&kp);-----------------------------------------------------注册kp探测点到内核。
    if (ret < 0) {
        pr_err("register_kprobe failed, returned %d\n", ret);
        return ret;
    }
    pr_info("Planted kprobe at %pF\n", kp.addr);
    return 0;
}

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

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

refer to

  • Documentation/trace/kprobetrace.txt
  • https://lwn.net/Articles/132196/
  • https://lishiwen4.github.io/linux-kernel/linu-kprobe
  • https://blog.crazytaxii.com/posts/an_introduction_2_kprobes/
  • https://www.kernel.org/doc/Documentation/kprobes.txt
  • https://www.kernel.org/doc/ols/2006/slides/kprobes.html
  • https://blog.arstercz.com/introduction_to_linux_dynamic_tracing/
  • https://documentation.suse.com/sles/12-SP4/html/SLES-all/cha-tuning-kprobes.html
  • https://www.cnblogs.com/arnoldlu/p/9752061.html
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值