Kprobe in linux with aarch64

Table of Contents

Overview

kprobe是Linux内核自带的调试机制,可以在内核的函数执行前后添加钩子来执行自定义函数来达到修改,调试内核的目的。
具体简介可以参考内核官方文档,这个文档已经有比较详细的记录.
https://www.kernel.org/doc/html/latest/trace/kprobes.html

basic usage

先来看一下这个功能的基础使用方法, 官方的两个例子很简单

Kernel config

CONFIG_KPROBES=y
CONFIG_SAMPLES=y
CONFIG_SAMPLE_KPROBES=y
CONFIG_CONFIG_SAMPLE_KRETPROBES=y

kprobe

insmod kprobe_example.ko symbol=cmdline_proc_open
root@j5dvb:/system/lib/modules/4.14.74# cat /proc/cmdline
[  277.486723] <cmdline_proc_open> pre_handler: p->addr = 0xffffff8008205f10, pc = 0xffffff8008205f10, pstate = 0x60400149
[  277.488094] <cmdline_proc_open> post_handler: p->addr = 0xffffff8008205f10, pstate = 0x60400149
console=uart8250,mmio32,0x43B90000,921600n8 root=/dev/ram0 rdinit=/init rw rootwait loglevel=14 hobotboot.hardware=j5dvb hobotboot.reason=freboot hobotboot.fchm_fault=0x00000001 hobotboot.fault_timestamp=1637559008 hobotboot.ipaddr=10.106.32.44 hobotboot.slot_suffix=_a hobotboot.bpu_slot_suffix=_-1 hobotboot.mode=normal module_blacklist=isp_hw1 dyndbg="module module +p;"
root@j5dvb:/system/lib/modules/4.14.74#

上述命令可以在打开/proc/cmdline这个命令时运行自己注册的钩子函数

kretprobe

insmod kretprobe_example.ko func=meminfo_proc_show
root@j5dvb:/system/lib/modules/4.14.74# cat /proc/meminfo
[  561.094342] meminfo_proc_show returned 0 and took 27667 ns to execute
MemTotal:        3771188 kB
MemFree:         2865632 kB
MemAvailable:    3013684 kB
Buffers:            1688 kB
Cached:           156948 kB
SwapCached:            0 kB
Active:           327792 kB

kprobe VS kretprobe

kretprobe底层通过kprobe来实现,与kprobe的不同在于实现了一系列辅助的函数来支持instance的实例化,这样在每次运行到对应的函数/地址时会自动生成一个新的private data,从而实现并发和多实例.

从官方的例子就可以看到,kretprobe可以实现统计函数运行时间的功能,但是简单的kprobe就不行(因为没有private data)。

typedef int (*kprobe_pre_handler_t) (struct kprobe *, struct pt_regs *);
typedef void (*kprobe_post_handler_t) (struct kprobe *, struct pt_regs *,
                unsigned long flags);
typedef int (*kretprobe_handler_t) (struct kretprobe_instance *,
                struct pt_regs *);

kprobe的回调中,kprobe是全局共享的,所以没有简单的办法去追踪每次的函数调用。而kretprobe的回调会有instance的概念,并且可以可以携带private在kretprobe_instance中.

简而言之,kretprobe会处理好多实例的问题,并且对每次的函数进入,退出都有对应的private data可供保存对应的数据。而kprobe只能探测单条指令,在指令运行前后执行注册进取的callback.

底层实现

kprobe

kprobe_addr 用来获取需要patch的指令地址,目前可以通过指定add/symbol + offset的方法来指定.
check_kprobe_rereg 用来检查kprobe对象是否已经被注册过,如果已经被注册过,就返回-EINVAL的错误
prepare_kprobe 用来记录原始的指令, 计算新的指令,并且注册到系统的insn的处理中
arm_kprobe 用来给text代码段打patch,将需要trap的指令替换成debug指令

kprobe 相关关键代码

在注册kprobe时,系统将对应地址的指令替换成brk的指令,当系统运行到对应的地址时,系统会触发debug的异常

/* arm kprobe: install breakpoint in text */
void __kprobes arch_arm_kprobe(struct kprobe *p)
{
    void *addr = p->addr;
    u32 insn = BRK64_OPCODE_KPROBES;

    aarch64_insn_patch_text(&addr, &insn, 1);
}

/* disarm kprobe: remove breakpoint from text */
void __kprobes arch_disarm_kprobe(struct kprobe *p)
{
    void *addr = p->addr;

    aarch64_insn_patch_text(&addr, &p->opcode, 1);
}
static int brk_handler(unsigned long unused, unsigned int esr,
                    struct pt_regs *regs)
{
    if (call_break_hook(regs, esr) == DBG_HOOK_HANDLED)
        return 0;

    if (user_mode(regs)) {
        send_user_sigtrap(TRAP_BRKPT);
    } else {
        pr_warn("Unexpected kernel BRK exception at EL1\n");
        return -EFAULT;
    }

    return 0;
}
NOKPROBE_SYMBOL(brk_handler);

在brk handler中,kprobe通过设置单部调试来实现调试指令的运行, 代码如下所示

static void __kprobes kprobe_handler(struct pt_regs *regs)
{
    struct kprobe *p, *cur_kprobe;
    struct kprobe_ctlblk *kcb;
    unsigned long addr = instruction_pointer(regs);

    kcb = get_kprobe_ctlblk();
    cur_kprobe = kprobe_running();

    p = get_kprobe((kprobe_opcode_t *) addr);

    if (p) {
        if (cur_kprobe) {
            if (reenter_kprobe(p, regs, kcb))
                return;
        } else {
            /* Probe hit */
            set_current_kprobe(p);
            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 will
             * modify the execution path and no need to single
             * stepping. Let's just reset current kprobe and exit.
             */
            if (!p->pre_handler || !p->pre_handler(p, regs)) {
                setup_singlestep(p, regs, kcb, 0);
            } else
                reset_current_kprobe();
        }
    }
    /*
     * 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.
     * Return back to original instruction, and continue.
     */
}

在被hook的指令单步执行之后,系统通过 kprobe_breakpoint_ss_handler 来处理接下来的任务, 并在此处清理单步调试的设置,并调用post_handler

kprobe执行流程

流程图大致如下:

.kprobe_call_sequence.png
上图大概画出了aarch64架构下kprobe的执行流程,从图中可以看到,kprobe利用修改指令,并且CPU单步调试的功能完成整个kprobe的功能。

注:为了方便,图中并没有画出需要模拟指令的运行路径,对于需要模拟运行的指令,如b,bl等并不会使用单步调试的功能,而是通过软件来模拟,见代码singlestep for insn simulation, 并且在模拟完成之后直接调用 post_handler回调。

kretprobe

注册

kretprobe在kprobe的基础上实现,注册函数很简单,参数检查,分配对象,以及注册kprobe
kernel source for register_kretprobe

  1. 检查参数
    kretprobe要求探测的函数位于行数开端,在注册时系统会检查地址是否在函数的开始位置 kprobe_on_func_entry
  2. 根据maxactive来分配kretprobe_instance对象
  3. 注册kprobe

pre_handler

kernel source for pre_handler_kretprobe

  1. 从freelist中取出一个未使用的kretprobe_instance对象
  2. 如果注册有entry_handler,则运行entry_handler
  3. 如果有entry_handler并且运行返回非0,释放kretprobe_instance对象,并结束
  4. 运行arch_prepare_kretprobe,保存并修改函数运行返回地址为kretprobe_trampoline
  5. 被探测的函数继续运行,返回后将会直接返回kretprobe_trampoline

kretprobe_trampoline

kretprobe_trampoline
__kretprobe_trampoline_handler

  1. 获取被探测函数的真实返回地址 (该地址在arch_prepare_kretprobe 中被保存)
  2. 将kretprobe_instance的ret_addr设置为真实的返回地址 (针对一个函数注册了多了kretprobe的情况)
  3. 调用handler的回调, 并且回收kretprobe_instance对象

kretprobe summary

根据上述的分析,kretprobe通过在函数的首地址放置kprobe探针,并通过修改lr指针的方式完成捕获整个函数的调用开始以及结束。

Summary

以上分析了kprobe和kretprobe的基本实现,由分析可得结论,kprobe只能分析单条指令,其两个callback函数会在指令执行的前后被调用。
而kretprobe能分析整个函数的执行,其两个callback会在函数调用前,以及函数运行返回后执行。

Contact me via :)
虚怀乃若谷,水深则流缓。