前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux内核调试技术——kprobe使用与实现(四)

Linux内核调试技术——kprobe使用与实现(四)

作者头像
用户5807183
发布2019-07-15 16:14:25
2.3K0
发布2019-07-15 16:14:25
举报
文章被收录于专栏:Linux知识积累Linux知识积累

Linux内核调试技术——kprobe使用与实现(一)

Linux内核调试技术——kprobe使用与实现(二)

Linux内核调试技术——kprobe使用与实现(三

Linux内核调试技术——kprobe使用与实现(四)--kprobe内核注册过程

kprobe探测模块调用register_kprobe向kprobe子系统注册一个kprobe探测点实例,代码路径kernel/kprobes.c

函数首先调用kprobe_addr函数初始化被探测点的地址p->addr。因为一般的探测模块并不会指定想要探测的addr地址,同kprobe_example例程一样通过传入函数名来指定要探测的函数,kprobe_addr函数的作用就是将函数名转换为最终的被探测地址:

kprobe_addr首先对入参进行检查,不允许函数名和地址同时设置或同时为空的情况;如果用户指定被探测函数名则调用kallsyms_lookup_name函数根据函数名查找其运行的虚拟地址;最后加上指定的探测偏移值作为最终的被探测地址。当然在绝大多数的情况下,offset值被用户设置为0,即用户探测指定函数的入口,但是也不排除用户想要探测某一函数内部的某一条指令。

kprobe_addr首先对入参进行检查,不允许函数名和地址同时设置或同时为空的情况;如果用户指定被探测函数名则调用kallsyms_lookup_name函数根据函数名查找其运行的虚拟地址;最后加上指定的探测偏移值作为最终的被探测地址。当然在绝大多数的情况下,offset值被用户设置为0,即用户探测指定函数的入口,但是也不排除用户想要探测某一函数内部的某一条指令。

回到register_kprobe函数中,下面调用check_kprobe_rereg函数防止同一个kprobe实例被重复注册,其中check_kprobe_rereg->__get_valid_kprobe调用流程将根据addr地址值搜索全局hash表并查看是否有同样的kprobe实例已经在表中了。

随后register_kprobe函数继续初始化kprobe的flags、nmissed字段和list链表(flag只允许用户传递KPROBE_FLAG_DISABLED,表示注册的kprobe默认是不启用的),然后调用check_kprobe_address_safe函数检测被探测地址是否可探测:

首先调用arch_check_ftrace_location确认是否探测地址已经被ftrace跟踪,若是且在开启了CONFIG_KPROBES_ON_FTRACE内核选项的情况下在该kprobe实例的flags上置位KPROBE_FLAG_FTRACE符号,表示本kprobe已使用ftrace。

然后上锁并竟用内核抢占,开始进入地址有效性检测流程,首先判断以下3个条件,必须全部满足:1、被探测地址在内核的地址段中;2、地址不在kprobe的黑名单之中;3、不在jump lable保留的地址空间中(内核jump lable特性使用?)。其中第一点比较好理解,函数实现如下:

被探测的函数当然要在内核的text(_stext~ _etext)段中,由于非内核启动时刻,不包括init text段;然后模块的text段和init text段也都可以,最后如果在ftrace动态分配的trampoline地址空间中也是满足的。

其中第二点中的blacklist黑名单指的是实现kprobes的关键代码路径,只有不在该黑名单中的函数才可以被探测:

主要包含两个方面,一是架构相关的kprobe关键代码路径,他们被保存在__kprobes_text_start~__kprobes_text_end段中,二是kprobe_blacklist链表,该链表前面在kprobe初始化过程中已经看到了。

首先__kprobes_text_start和__kprobes_text_end被定义在include/asm-generic/Vmlinux.lds.h中,使用宏__kprobes标记的函数被归入该.kprobes.text段:

简单的总结一下:即使用宏NOKPROBE_SYMBOL和宏__kprobes标记的内核函数不可以被探测kprobe。

回到check_kprobe_address_safe函数中,若满足了以上三点,接下来判断被探测地址是否属于某一个内核模块的init_text段或core_text段:

判断若属于某一个模块的话则增加这个模块的引用计数以防止模块被意外动态卸载,同时不允许在已经完成加载模块的init_text段中的函数注册kprobe(因为在模块加载完成后init_text段的内存已经被free掉了)。最后若模块获取成功,它将通过probed_mod参数返回给register_kprobe用于错误处理流程。

以上判断都通过之后重新打开内核抢占并解锁,回到register_kprobe函数继续注册流程。接下来尝试从全局hash表中查找是否之前已经为同一个被探测地址注册了kprobe探测点,若已注册则调用register_aggr_kprobe函数继续注册流程,该流程稍后再分析。现假设是初次注册,则调用prepare_kprobe函数,该函数会根据被探测地址是否已经被ftrace了而进入不同的流程,这里假设没有启用ftrace,则直接调用arch_prepare_kprobe函数进入架构相关的注册流程,看一下x86架构的实现:

首先对于smp系统,被探测地址不能被用于smp-alternatives,非smp无此要求;然后判断该被探测地址的指令有效并调用get_insn_slot函数申请用于拷贝原始指令的指令slot内存空间,最后调用arch_copy_kprobe函数执行指令复制动作。

函数首先调用__copy_instruction将kprobe->addr被探测地址的指令拷贝到kprobe->ainsn.insn保存起来(可能会对指令做适当的修改),然后初始化kprobe->ainsn结构体,最后将指令的第一个字节保存到kprobe->opcode字段中(x86架构的kprobe_opcode_t是u8类型的)。

如此被探测点指令就被拷贝保存起来了。架构相关的初始化完成以后,接下来register_kprobe函数初始化kprobe的hlist字段并将它添加到全局的hash表中。然后判断如果kprobes_all_disarmed为false并且kprobe没有被disable(在kprobe的初始化函数中该kprobes_all_disarmed值默认为false),则调用arm_kprobe函数,它会把触发trap的指令写到被探测点处替换原始指令。

这里假设不适用ftrace和optimize kprobe特性,将直接调用架构相关的函数arch_arm_kprobe,其中x86的实现如下:

直接调用text_poke函数将addr地址处的指令替换为BREAKPOINT_INSTRUCTION指令(机器码是0xCC),当正常执行流程执行到这条指令后就会触发int3中断,进而进入探测回调流程。

至此kprobe的注册流程分析完毕,再回头分析对一个已经被注册过kprobe的探测点注册新的kprobe的执行流程,即register_aggr_kprobe函数:

在前文中看到,该函数会在对同一个被探测地址注册多个kprobe实例时会被调用到,该函数会引入一个kprobe aggregator的概念,即由一个统一的kprobe实例接管所有注册到该地址的kprobe。这个函数的注释非常详细,并不难理解,来简单分析一下:

函数的第一个入参orig_p是在全局hash表中找到的已经注册的kprobe实例,第二个入参是本次需要注册的kprobe实例。首先在完成了必要的上锁操作后就调用kprobe_aggrprobe函数检查orig_p是否是一个aggregator。

它通过kprobe的pre_handler回调判断,如果是aggregator则它的pre_handler回调函数会被替换成aggr_pre_handler函数。一般对于第二次注册kprobe的情况显然是不会满足条件的,会调用alloc_aggr_kprobe函数创建一个,对于没有开启CONFIG_OPTPROBES选项的情况,alloc_aggr_kprobe仅仅是分配了一块内存空间,然后调用init_aggr_kprobe函数初始化这个aggr kprobe。

可以看到,这个aggr kprobe中的各个字段基本就是从orig_p中拷贝过来的,包括opcode和ainsn这两个备份指令的字段以及addr和flags字段,但是其中的4个回调函数会被初始化为aggr kprobe所特有的addr_xxx_handler,这几个函数后面会具体分析。接下来函数会初始化aggr kprobe的两个链表头,然后将自己添加到链表中去,并替换掉orig_p。

回到register_aggr_kprobe函数中,如果本次是第二次以上向同一地址注册kprobe实例,则此时的orig_p已经是aggr kprobe了,则会调用kprobe_unused函数判断该kprobe是否为被使用,若是则调用reuse_unused_kprobe函数重新启用,但是对于没有开启CONFIG_OPTPROBES选项的情况,逻辑上是不存在这种情况的,因此reuse_unused_kprobe函数的实现仅仅是一段打印后就立即触发BUG_ON。

继续往下分析,下面来讨论aggr kprobe被kill掉的情况,显然只有在第三次及以上注册同一地址可能会出现这样的情况。针对这一种情况,这里同初次注册kprobe的调用流程类似,首先调用arch_prepare_kprobe做架构相关初始化,保存被探测地址的机器指令,然后调用prepare_optimized_kprobe启用optimized_kprobe,最后清除KPROBE_FLAG_GONE的标记。

接下来调用再次copy_kprobe将aggr kprobe中保存的指令opcode和ainsn字段拷贝到本次要注册的kprobe的对应字段中,然后调用add_new_kprobe函数将新注册的kprobe链入到aggr kprobe的list链表中:

注意最主要的就是add list,只是如果新注册的kprobe设定了break_handler回调函数,会将其插入链表的末尾并为aggr kprobe设定break handler回调函数aggr_break_handler;与此同时若新注册的kprobe设定了post_handler,也同样为aggr kprobe设定post handler回调函数aggr_post_handler。

回到register_aggr_kprobe函数,在out标号处继续执行,下面会进入if条件判断,启用aggr kprobe,然后调用前文中分析过的arm_kprobe函数替换被探测地址的机器指令为BREAKPOINT_INSTRUCTION指令。

至此整个kprobe注册流程分析结束,下面来分析以上注册的探测回调函数是如何被执行的以及被探测指令是如何被单步执行的。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linux知识积累 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Linux内核调试技术——kprobe使用与实现(一)
  • Linux内核调试技术——kprobe使用与实现(二)
  • Linux内核调试技术——kprobe使用与实现(三)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档