首页 > 其他分享 >tracer ftrace笔记(13)—— kprobe

tracer ftrace笔记(13)—— kprobe

时间:2023-02-04 22:36:11浏览次数:43  
标签:kernel 13 ftrace trace kprobes sys kprobe handler

基于 Linux-5.15

一、kprobe 简介

1. kprobes 是为了便于跟踪内核函数执行状态的一种轻量级内核调试技术。可以在内核的绝大多数函数(非inline、非trace自身函数)中动态的插入探测点来收集所需的调试状态信息,包括函数有无被调用、何时被调用、
执行是否正确以及函数的入参和返回值是什么等等。

2. kprobes技术包括的3种探测手段分别时kprobe、jprobe(5.10上已废弃) 和 kretprobe。kprobe 是最基本的探测方式,是实现后两种的基础,它可以在任意的位置放置探测点(就连函数内部的某条指令处也可以),
它提供了探测点的调用前、调用后和内存访问出错3种回调方式,分别是 pre_handler、post_handler 和 fault_handler,其中 pre_handler 函数将在被探测指令被执行前回调,post_handler 会在被探测指令执行
完毕后回调(注意不是被探测函数),fault_handler 会在内存访问出错时被调用;jprobe 基于 kprobe 实现,它用于获取被探测函数的入参值;kretprobe 同样基于kprobe实现,用于获取被探测函数的返回值。

3. kprobes 的技术需要硬件异常处理和单步调试技术的支持,前者用于让程序的执行流程陷入到用户注册的回调函数中去,而后者则用于单步执行被探测点指令。

4. kprobes 的特点与使用限制
(1) kprobes 允许在同一个被被探测位置注册多个kprobe.
(2) kprobes 几乎可以探测内核中的任何函数,包括中断处理函数。单 kprobes.c 中用于实现 kprobes 自身的函数是不允许被探测的,另外还有 inline 函数 do_page_fault 和 notifier_call_chain;
(3) kprobes 会避免在处理探测点函数时再次调用另一个探测点的回调函数,例如在 printk()函数上注册了探测点,则在它的回调函数中可能再次调用 printk 函数,此时将不再触发printk探测点的回调,仅仅时增
加了 kprobe 结构体中 nmissed 字段的数值;
(4) kprobes 回调函数的运行期间是关闭抢占的,因此不论在何种情况下,在回调函数中不要调用会放弃CPU的函数。
(5) kretprobe 通过替换返回地址为预定义的 trampoline 的地址来实现,因此栈回溯和gcc内嵌函数 __builtin_return_address() 调用将返回 trampoline 的地址而不是真正的被探测函数的返回地址;
(6) 如果一个函数的调用次数和返回次数不相等,则在类似这样的函数上注册 kretprobe 将可能不会达到预期的效果,例如 do_exit() 函数会存在问题,而 do_execve() 函数和 do_fork() 函数不会;

5. 使用 kprobe 有两种方式,第一种是开发人员自行编写内核模块,向内核注册探测点,探测函数可根据需要自行定制,使用灵活方便。可参考samples/kprobes目录下的 kprobe_example.c 和 kretprobe_example.c;
第二种方式是使用kprobes on ftrace,这种方式是kprobe和ftrace结合使用,即可以通过kprobe来优化ftrace来跟踪函数的调用,下面通过实验进行介绍。

 

二、kprobe 相关数据结构

1. struct kprobe

struct kprobe {
    struct hlist_node hlist;
    /* list of kprobes for multi-handler support */
    struct list_head list;
    /*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;
    /* Called before addr is executed. */
    kprobe_pre_handler_t pre_handler;
    /* Called after addr is executed, unless... */
    kprobe_post_handler_t post_handler;
    /* Saved opcode (which has been replaced with breakpoint) */
    kprobe_opcode_t opcode;
    /* copy of the original instruction */
    struct arch_specific_insn ainsn;
    /* Indicates various status flags. Protected by kprobe_mutex after this kprobe is registered. */
    u32 flags;
};

成员说明:

hlist: 被用于kprobe全局hash,索引值为被探测点的地址;
list: 用于链接同一被探测点的不同探测kprobe
addr:被探测点的地址,注册过程中由 kprobe_addr()将探测的函数名转换为地址,赋值到这里.
symbol_name:被探测函数的名字,一般是驱动中直接指向内核函数。
offset:被探测点在函数内部的偏移,用于探测函数内部的指令,如果该值为0表示函数的入口;
pre_handler:在被探测点指令执行之前调用的回调函数,在 kprobe_handler() 中调用;
post_handler:在被探测指令执行之后调用的回调函数;
opcode:保存的被探测点原始指令,在 arch_prepare_kprobe() 中执行保存动作;
ainsn:被复制的被探测点的原始指令,用于单步执行,架构强相关(可能包含指令模拟函数);
flags:状态标记

 

2. struct kretprobe

struct kretprobe {
    struct kprobe kp;
    kretprobe_handler_t handler;
    kretprobe_handler_t entry_handler;
    int maxactive;
    int nmissed;
    size_t data_size;
    struct freelist_head freelist;
    struct kretprobe_holder *rph;
};

成员说明:

kp: kretprobe 是基于 kprobe 实现的;
handler: 在被探测函数返回后被调用(一般在这个函数中打印被探测函数的返回值);
entry_handler: 会在被探测函数执行之前被调用;
maxactive: 表示同时支持并行探测的上限。因为 kretprobe 会跟踪一个函数从开始到结束,因此对于一些调用比较频繁的被探测函数,在探测的时间段内重入的概率比较高,这个 maxactive 字段值表示在重入情况发生时,
支持同时检测的进程数(执行流数)的上限,若并行触发的数量超过了这个上限,则 kretprobe 不会进行跟踪探测,仅仅增加 nmissed 字段的值以作提示;
data_size: 表示kretprobe私有数据的大小,在注册kretprobe时会根据该大小预留空间;
freelist: 表示空闲的 kretprobe 运行实例链表,它链接了本 kretprobe 的空闲实例 struct kretprobe_instance 结构体表示。

 

三、通过trace使用kprobe分析

1. trace方式使用的handler函数

struct file_operations dyn_event_write //trace_dynevent.c 对应 /sys/kernel/tracing/dynamic_events 文件
    create_dyn_event //trace_dynevent.c 
        trace_kprobe_ops.create //trace_kprobe.c alloc_trace_kprobe 中进行的赋值
struct file_operations kprobe_events_ops.write //trace_kprobe.c 对应 /sys/kernel/tracing/kprobe_events 文件
    probes_write
        create_or_delete_trace_kprobe
            trace_kprobe_create //trace_kprobe.c
                __trace_kprobe_create //trace_kprobe.c 这个函数中会解析echo的参数,有语法注释
            perf_kprobe_init //trace_event_perf.c 非主要调用路径
                create_local_trace_kprobe //trace_kprobe.c 使能 CONFIG_PERF_EVENTS 才调用,默认使能的
                    alloc_trace_kprobe
                        if (is_return)
                            tk->rp.handler = kretprobe_dispatcher;
                        else
                            tk->rp.kp.pre_handler = kprobe_dispatcher;
                    register_trace_kprobe // __trace_kprobe_create 中继续执行这个函数
                        __register_trace_kprobe
                            if (trace_kprobe_is_return(tk))
                                ret = register_kretprobe(&tk->rp);
                            else
                                ret = register_kprobe(&tk->rp.kp);

通过向 kprobe_events 文件写入命令的方式使用 kprobe,可以看到kprobe使用的是 p->pre_handler = kprobe_dispatcher. kretprobe使用的是 kretprobe_dispatcher().

 

四、通过trace使用kprobe实验

1. trace sysfs文件节点的内核调用路径

ssize_t pl_store(struct gov_attr_set *attr_set, const char *buf, size_t count) //要探测的内核函数原型

/sys/kernel/tracing # echo 'p:myprobe pl_store buf=+0(%x1):string' > kprobe_events //设置要trace pl_store(),并打印其参数1用户传入buf中的内容。
/sys/kernel/tracing # echo 1 > options/stacktrace //使能打印函数调用栈(只能打印内核空间的)
/sys/kernel/tracing # echo 1 > events/kprobes/myprobe/enable //使能自己定义的 myprobe 节点

然后执行 P=/sys/kernel/tracing; > $P/trace; echo 1 > $P/tracing_on; cat $P/trace_pipe 抓trace,可以看到打印:

/sys/devices/system/cpu/cpufreq/policy0/walt # echo 112233 > pl

sh 23682-23682   (  23682) [000] d..1 259562.002535: myprobe: (pl_store+0x0/0x48 [sched_walt]) buf="112233 //buf内容打印出来
"
              sh-23682   (  23682) [000] d..1 259562.002542: <stack trace>
 => pl_store
 => sysfs_kf_write
 => kernfs_fop_write_iter
 => vfs_write
 => ksys_write
 => __arm64_sys_write
 => el0_svc_common
 => el0_svc
 => el0_sync_handler
 => el0_sync

结束kprobe trace执行:

/sys/kernel/tracing # echo 0 > events/kprobes/myprobe/enable //禁用自己定义的 myprobe 节点
/sys/kernel/tracing # echo '-:myprobe' > kprobe_events //移除自己要probe的函数

 

2. trace procfs文件节点的内核调用路径 

ssize_t proc_test_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) //函数原型

/sys/kernel/tracing # echo 'p:myprobe proc_test_write buf=+0(%x1):ustring' > kprobe_events //其它步骤同上

然后执行 P=/sys/kernel/tracing; > $P/trace; echo 1 > $P/tracing_on; cat $P/trace_pipe 抓trace,可以看到打印:

RenderThread-9557    (   4185) [005] d..1   201.357916: myprobe: (proc_test_write+0x0/0x32c [proc_test]) buf="p 9557" //buf内容打印出来
           <...>-9557    (   4185) [005] d..1   201.357917: <stack trace>
 => proc_test_write
 => vfs_write
 => ksys_write
 => __arm64_sys_write
 => el0_svc_common
 => el0_svc
 => el0_sync_handler
 => el0_sync

注:若不需要栈回溯可以不使能,也可以通过向 trace_options 文件中echo stacktrace/nostacktrace 来使能/禁止打印栈回溯。不使能栈回溯trace打印如下:

//配置kprobe:
/sys/kernel/tracing # echo 'p:myprobe proc_test_write buf=+0(%x1):ustring' > kprobe_events
/sys/kernel/tracing # echo 1 > events/kprobes/myprobe/enable
//抓trace:
/sys/kernel/tracing # echo 1 > tracing_on
/sys/kernel/tracing # cat trace_pipe | grep proc_test_write
//trace打印:
RenderThread-8950    [007] d..1.   202.148271: myprobe: (proc_test_write+0x0/0x278 [proc_test]) buf="p 8950"
RenderThread-8964    [007] d..1.   204.906870: myprobe: (proc_test_write+0x0/0x278 [proc_test]) buf="p 8964"
...

 

3. trace函数的所有参数

int select_task_rq_fair(struct task_struct *p, int prev_cpu, int wake_flags) //函数原型

/sys/kernel/tracing # echo 'p:myprobe select_task_rq_fair p=%x0:x64 cpu=%x1 wake_flags=%x2:x32' > kprobe_events
/sys/kernel/tracing # echo 1 > events/kprobes/myprobe/enable
/sys/kernel/tracing # echo 1 > tracing_on
/sys/kernel/tracing # cat trace_pipe | grep select_task_rq_fair

 cat-30355   [006] d..4. 12923.640081: myprobe: (select_task_rq_fair+0x0/0x360) p=0xffffff8312294800 cpu=0x6 wake_flags=0x18
adbd-1241    [000] d.h5. 12923.640128: myprobe: (select_task_rq_fair+0x0/0x360) p=0xffffff838a9a3600 cpu=0x6 wake_flags=0x8
grep-30356   [006] d..4. 12923.640135: myprobe: (select_task_rq_fair+0x0/0x360) p=0xffffff83c6101200 cpu=0x5 wake_flags=0x8
...

 

4. trace函数的返回值

/sys/kernel/tracing # echo 'r:myprobe select_task_rq_fair ret_cpu=$retval' > kprobe_events
/sys/kernel/tracing # echo 1 > events/kprobes/myprobe/enable
/sys/kernel/tracing # echo 1 > tracing_on
/sys/kernel/tracing # cat trace_pipe | grep select_task_rq_fair

 cat-21169   [007] d..4.  5803.107173: myprobe: (try_to_wake_up+0x2d0/0x800 <- select_task_rq_fair) ret_cpu=0x7
adbd-1236    [003] d..4.  5803.107522: myprobe: (try_to_wake_up+0x2d0/0x800 <- select_task_rq_fair) ret_cpu=0x5
...

注:使用kretprobe,echo的首字符是'r'而不是使用kprobe的'p',因此参数和返回值不能同时trace. ==>加2个trace点呢?

 

5. 同时trace函数所有参数和返回值

/sys/kernel/tracing # echo 'p:myprobe select_task_rq_fair p=%x0:x64 cpu=%x1 wake_flags=%x2:x32' > kprobe_events
/sys/kernel/tracing # echo 'r:myretprobe select_task_rq_fair ret_cpu=$retval' >> kprobe_events //追加,否则只有一个,必须要改为使用 myretprobe,否则不成功
/sys/kernel/tracing # echo 1 > events/kprobes/myprobe/enable
/sys/kernel/tracing # echo 1 > events/kprobes/myretprobe/enable
/sys/kernel/tracing # echo 1 > tracing_on
/sys/kernel/tracing # cat trace_pipe | grep select_task_rq_fair

 grep-30836   [007] d..4. 13444.897214: myprobe: (select_task_rq_fair+0x0/0x360) p=0xffffff83a0398000 cpu=0x5 wake_flags=0x8
 adbd-1241    [004] d..4. 13444.897216: myprobe: (select_task_rq_fair+0x0/0x360) p=0xffffff83e0fcb600 cpu=0x5 wake_flags=0x8
 grep-30836   [007] d..4. 13444.897219: myretprobe: (try_to_wake_up+0x2d0/0x800 <- select_task_rq_fair) ret_cpu=0x3
<...>-30699   [005] d..4. 13444.897221: myprobe: (select_task_rq_fair+0x0/0x360) p=0xffffff80bf4a3600 cpu=0x5 wake_flags=0x8
 adbd-1241    [004] d..4. 13444.897230: myretprobe: (try_to_wake_up+0x2d0/0x800 <- select_task_rq_fair) ret_cpu=0x1
<...>-30699   [005] d..4. 13444.897230: myretprobe: (try_to_wake_up+0x2d0/0x800 <- select_task_rq_fair) ret_cpu=0x1
...

可以看到入参时的探测和返回值的探测在trace的显示上并不是一一对应的,展示效果并不好。


6. 使用filter过滤功能

(1) 通过PID过滤

/sys/kernel/tracing # echo 'p:myprobe proc_task_write buf=+0(%x1):ustring' > kprobe_events
/sys/kernel/tracing # echo 1 > events/kprobes/myprobe/enable
/sys/kernel/tracing # echo "common_pid!=4083" > events/kprobes/myprobe/filter //过滤TID=4083线程上下文的打印,common_pid 来自 events/kprobes/myprobe/format
/sys/kernel/tracing # cat events/kprobes/myprobe/filter
common_pid!=4083
/sys/kernel/tracing # echo "common_pid==4083" > events/kprobes/myprobe/filter //只保留TID=4083线程上下文的打印
/sys/kernel/tracing # cat events/kprobes/myprobe/filter
common_pid==4083

//只过滤"common_pid==4083"后只有4083这一个线程打印了:
ndroid.launcher-4083    [007] d..1.  2655.036948: myprobe: (proc_task_write+0x0/0x3e4 [proc_test]) buf="p 2827 115"
ndroid.launcher-4083    [007] d..1.  2655.036955: myprobe: (proc_task_write+0x0/0x3e4 [proc_test]) buf="p 3070 124"
...

/sys/kernel/tracing # echo 0 > events/kprobes/myprobe/filter //取消过滤
/sys/kernel/tracing # cat events/kprobes/myprobe/filter
none

(2) 通过参数过滤

/sys/kernel/tracing # echo 'wake_flags==0x18' > events/kprobes/myprobe/filter //只看实验5中wake_flags==0x18的

(3) 通过返回值过滤

/sys/kernel/tracing # echo 'ret_cpu==1' > events/kprobes/myretprobe/filter //只看实验5中ret_cpu==1的

 

7. 实验总结

(1) 可以以追加的方式">>"的方式向 kprobe_events 文件中echo多个要跟踪的函数。
(2) 要做好清理才能正常使用下一次kprobe,比如一次测试完后echo 0 > enable 和 echo '-:myprobe' > kprobe_events。
(3) 需要先 echo '-:myprobe' 取消之前的探测,并且保证函数在 cat /proc/kallsyms(echo 0 > /proc/sys/kernel/kptr_restrict 后有地址) 中是有的。

 

五、通过代码使用kprobe分析

1. 内核中自带了 samples/kprobes/kprobe_example.c 文件用来展示如何通过代码使用kprobe。大致逻辑如下:

(1) 先定义一个 struct kprobe 结构变量(假设是 kp 指针),然后将要探测的函数名赋值给 kp->symbol_name 成员。
(2) 然后初始化 struct kprobe 结构变量的 kp->pre_handler / kp->post_handler 回调函数指针,前者会在探测指令执行前执行,后者则会在探测指令执行后执行。
(3) 然后通过 register_kprobe(kp) 注册这个 struct kprobe 结构变量p.
(4) 当不再使用时调用 unregister_kprobe(kp) 取消注册。


2. 内核中自带了 samples/kprobes/kretprobe_example.c 文件来展示如何通过代码使用kretprobe。kretprobe 是基于通过 kprobe 实现的,使用逻辑和kprobe基本一致,大致逻辑如下:

(1) 先定义一个 struct kretprobe 结构变量(假设是 kretp 指针),然后将要探测的函数名赋值给 kretp->kp.symbol_name 成员。
(2) 然后初始化 struct kprobe 结构变量的 kretp->handler / kretp->entry_handler 回调函数指针,前者会在函数退出时调用,后者则会在函数进入时调用。
(3) 然后通过 register_kretprobe(kretp) 注册这个 struct kretprobe 结构变量kretp.
(4) 当不再使用时调用 unregister_kretprobe(kretp) 取消注册。

 

六、通过代码使用kprobe实验

这里使用内核自带的 kprobe_example.c 和 kretprobe_example.c 两个文件进行实验。

1. 首先要使能配置

CONFIG_SAMPLES=y  //整个samples目录默认是不被编译的,使能这个才会被编译
CONFIG_SAMPLE_KPROBES=m

 

2. kprobe_example.ko 测试

/data/local/tmp # insmod kprobe_example.ko
/data/local/tmp # cat /sys/module/kprobe_example/parameters/symbol
kernel_clone
/data/local/tmp # dmesg -c | grep pstate
[17632.084434] handler_pre: <kernel_clone> p->addr = 0x0000000033079247, pc = 0xffffffd95a524eb4, pstate = 0x82400005
[17632.084612] handler_post: <kernel_clone> p->addr = 0x0000000033079247, pstate = 0x82400005
[17632.089308] handler_pre: <kernel_clone> p->addr = 0x0000000033079247, pc = 0xffffffd95a524eb4, pstate = 0x82400005
[17632.089415] handler_post: <kernel_clone> p->addr = 0x0000000033079247, pstate = 0x82400005
...

/data/local/tmp # rmmod kprobe_example.ko

symbol 文件是通过 module_param_string() 实现的,只有cat显示功能。可以改为 module_param_cb() 来实现,以便在echo新的函数名进去后能在回调函数中取消注册旧的,转而去探测新的函数。


3. kretprobe_example.ko 测试

/data/local/tmp # insmod kretprobe_example.ko
/data/local/tmp # cat /sys/module/kretprobe_example/parameters/func
kernel_clone
/data/local/tmp # dmesg -c | grep returned
[18844.349421] kernel_clone returned 24524 and took 4076000 ns to execute //息屏状态
[18844.353517] kernel_clone returned 24525 and took 2896077 ns to execute
...
[18954.466383] kernel_clone returned 25389 and took 69846 ns to execute //频点被拉起后
[18954.466565] kernel_clone returned 25390 and took 56923 ns to execute

此驱动用于探测一个函数(本例是 kernel_clone)的执行时长。可以同上修改以便可以探测所有函数的执行时长。


五、kprobe 实现原理

1. 实现原理概述

首先kprobe会将被探测地址 kp->addr 处的指令保存在 kp->opcode 中,然后替换探测地址处的指令为 BRK64_OPCODE_KPROBES,当执行到这条指令时,就会触发异常跳转到BRK处理流程中,会调用注册的回调函数 kprobe_breakpoint_handler(),
此流程中会先调用 kp->pre_handler() 回调,然后单步执行被替换的指令。TODO: kp->post_handler() 回调什么时候执行?

指令替换函数:

/* 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);
}

 

2. 注册流程:

register_kprobe //kernel/kprobes.c

    kprobe_addr //kernel/kprobes.c
        _kprobe_addr //kernel/kprobes.c 将 p->symbol_name 转换为地址保存在 p->addr 中
        
    prepare_kprobe //kernel/kprobes.c
        arch_prepare_kprobe //arm64/kprobes.c
            p->opcode = le32_to_cpu(*p->addr); //将探测地址处的指令保存到 p->opcode 中
    
    arm_kprobe //kernel/kprobes.c
        __arm_kprobe //kernel/kprobes.c
            arch_arm_kprobe(p); //arm64/kprobes.c 将测试处指令替换为 BRK64_OPCODE_KPROBES

 

3. 触发指令后执行流程:

el1h_64_sync_handler //arm64/kernel/entry.S 中的异常向量表入口函数,判断 esr_el1 寄存器的值进行调用
    el1_dbg //entry-common.c
el0t_64_sync_handler //arm64/kernel/entry.S 中的异常向量表入口函数,判断 esr_el1 寄存器的值进行调用
    el0_dbg //entry-common.c
        do_debug_exception //mm/fault.c
            debug_fault_info[DBG_ESR_EVT_BRK].fn //mm/fault.c
                brk_handler //debug-monitors.c
                    call_break_hook //debug-monitors.c
                        kprobe_breakpoint_handler //arm64/kprobes.c struct break_hook kprobes_break_hook.fn() 在 arch_init_kprobes() 中注册
                            kprobe_handler //arm64/kprobes.c
                                p->pre_handler() //执行kprobe回调

注:el1h_64_sync_handler()/el0t_64_sync_handler 在 arm64/kernel/entry.S 中以异常向量表的形式定义的。

简化后的 kprobe_handler():

static void __kprobes kprobe_handler(struct pt_regs *regs)
{
    struct kprobe *p;
    unsigned long addr = instruction_pointer(regs); //return regs->pc;

    p = get_kprobe((kprobe_opcode_t *) addr);

    /* 先调用kprobe的 pre_handler 回调 */
    if (!p->pre_handler || !p->pre_handler(p, regs)) {
        /* 然后执行被替换的指令 */
        setup_singlestep(p, regs, kcb, 0);
    }
}

 

六、相关DEBUG方法

1. debugfs_kprobe_init() 中创建debugfs文件,显示不可以被kprobe探测函数黑名单和enable、diable文件。debug版本才有。

2. 通过 kprobe_profile 属性文件可以查看探测命中次数和丢失次数(probe hits and probe miss-hits)。

 


参考:
Linux内核调试技术Kprobe使用与实现:https://zhuanlan.zhihu.com/p/541499518

tracer ftrace笔记(12)—— trace文档翻译与实验——/sys/kernel/tracing/README:https://www.cnblogs.com/hellokitty2/p/17055175.html  //里面命令格式介绍

 

标签:kernel,13,ftrace,trace,kprobes,sys,kprobe,handler
From: https://www.cnblogs.com/hellokitty2/p/17092549.html

相关文章