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

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

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

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

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

Linux内核调试技术——kprobe使用与实现(五)-触发kprobe探测和回调

前文中,从register_kprobe函数注册kprobe的流程已经看到,用户指定的被探测函数入口地址处的指令已经被替换成架构相关的BREAKPOINT_INSTRUCTION指令,若是正常的代码流程执行到该指令,将会触发异常,进入架构相关的异常处理函数,kprobe注册的回调函数及被探测函数的单步执行流程均在该流程中执行。由于不同架构实现存在差别,下面来分析x86架构的实现:

x86_64架构下,执行到前文中替换的BREAKPOINT_INSTRUCTION指令后将触发INT3中断,进而调用到do_int3函数。do_int3函数做的事情比较多,但是和kprobe相关的仅代码中列出的这1处,下面来看kprobe_int3_handler函数,这个函数比较长,分段来分析:

本地中断在处理kprobe期间依然被禁止,同时调用user_mode函数确保本处理函数处理的int3中断是在内核态执行流程期间被触发的(因为kprobe不会从用户态触发),这里之所以要做这么一个判断是因为同arm定义特殊未处理指令回调函数不同,这里的do_int3要通用的多,并不是单独为kprobe所设计的。然后获取被探测指令的地址保存到addr中(对于int3中断,其被Intel定义为trap,那么异常发生时EIP寄存器内指向的为异常指令的后一条指令),同时会禁用内核抢占,注释中说明在reenter_kprobe和单步执行时会有选择的重新开启内核抢占。接下来获取当前cpu的kprobe_ctlblk控制结构体和本次要处理的kprobe实例p,然后根据不同的情况进行不同分支的处理。在继续分析前先来看一下x86_64架构kprobe_ctlblk结构体的定义

kprobe_old_flags和kprobe_saved_flags字段用于保存寄存器pt_regs的flag标识。

下面回到函数中根据不同的情况分别分析:

1、p存在且curent_kprobe存在

对于kprobe重入的情况,调用reenter_kprobe函数单独处理:

这个流程对于KPROBE_HIT_SS KPROBE_HIT_SSDONE和KPROBE_HIT_ACTIVE,递增nmissed值并调用setup_singlestep函数进入单步处理流程(该函数最后一个入参此时设置为1,针对reenter的情况会将kprobe_status状态设置为KPROBE_REENTER并调用save_previous_kprobe执行保存当前kprobe的操作)。对于KPROBE_REENTER阶段还是直接报BUG。注意最后函数会返回1,do_int3也会直接返回,表示该中断已被kprobe截取并处理,无需再处理其他分支。

2、p存在但curent_kprobe不存在

这是一般最通用的kprobe执行流程,首先调用set_current_kprobe绑定p为当前正在处理的kprobe:

这里在设置current_kprobe全局变量的同时,还会同时设置kprobe_saved_flags和kprobe_old_flags的flag值,它们用于具体的架构指令相关处理。接下来处理pre_handler回调函数,有注册的话就调用执行,然后调用setup_singlestep启动单步执行。在调试完成后直接返回1。

3、p不存在且被探测地址的指令也不是BREAKPOINT_INSTRUCTION

这种情况表示kprobe可能已经被其他CPU注销了,则让他执行原始指令即可,因此这里设置regs->ip值为addr并重新开启内核抢占返回1。

4、p不存在但curent_kprobe存在

这种情况一般用于实现jprobe,因此会调用curent_kprobe的break_handler回调函数,然后在break_handler返回非0的情况下执行单步执行,最后返回1。具体在jprobe实现中再详细分析。

单步执行

单步执行其实就是执行被探测点的原始指令,涉及的主要函数即前文中分析kprobe触发及处理流程时遗留的singlestep函数(arm)和setup_singlestep函数(x86),它们的实现原理完全不同,其中会涉及许多cpu架构相关的知识,因此会比较晦涩。下面从原理角度逐一分析,并不涉及太多架构相关的细节:

x86_64架构的单步执行函数其主要原理是:当程序执行到某条想要单独执行CPU指令时,在执行之前产生一次CPU异常,此时把异常返回时的CPU的EFLAGS寄存器的TF(调试位)位置为1,把IF(中断屏蔽位)标志位置为0,然后把EIP指向单步执行的指令。当单步指令执行完成后,CPU会自动产生一次调试异常(由于TF被置位)。此时,Kprobes会利用debug异常,执行post_handler()。下面来简单看一下:

首先在前文中已经介绍了,函数的最后一个入参reenter表示是否重入,对于重入的情况那就调用save_previous_kprobe函数保存当前正在运行的kprobe,然后绑定p和current_kprobe并设置kprobe_status为KPROBE_REENTER;对于非重入的情况则设置kprobe_status为KPROBE_HIT_SS。

接下来考试准备单步执行,首先设置regs->flags中的TF位并清空IF位,同时把int3异常返回的指令寄存器地址改为前面保存的被探测指令,当int3异常返回时这些设置就会生效,即立即执行保存的原始指令(注意这里是在触发int3之前原来的上下文中执行,因此直接执行原始指令即可,无需特别的模拟操作)。该函数返回后do_int3函数立即返回,由于cpu的标识寄存器被设置,在单步执行完被探测指令后立即触发debug异常,进入debug异常处理函数do_debug。

首先调用resume_execution函数将debug异常返回的下一条指令设置为被探测之后的指令,这样异常返回后程序的流程就会按正常的流程继续执行;然后恢复kprobe执行前保存的flags标识;接下来如果kprobe不是重入的并且设置了post_handler回调函数,就设置kprobe_status状态为KPROBE_HIT_SSDONE并调用post_handler函数;如果是重入的kprobe则调用restore_previous_kprobe函数恢复之前保存的kprobe。最后调用reset_current_kprobe函数解除本kprobe和current_kprobe的绑定,如果本kprobe由单步执行触发,则说明do_debug异常处理还有其他流程带处理,返回0,否则返回1。

至此,kprobe的一般处理流程就分析完了,最后分析一下剩下的最后一个回调函数fault_handler。

出错回调

出错会调函数fault_handler会在执行pre_handler、single_step和post_handler期间触发内存异常时被调用,对应的调用函数为kprobe_fault_handler,它同样时架构相关的,下面来看一下(x86_64):

1、do_page_fault->__do_page_fault->kprobes_fault

可见在触发缺页异常之后,若当前正在处理kprobe流程期间,会调用kprobe_fault_handler进行处理。

2、do_general_protection->notify_die->kprobe_exceptions_notify

前文中init_kprobes初始化时会注册die内核通知链kprobe_exceptions_nb,它的回调函数为kprobe_exceptions_notify,在内核触发DIE_GPF类型的notify_die时,该函数会调用kprobe_fault_handler进行处理。下面来简单看一下x86_64架构的kprobe_fault_handler函数实现:

kprobe_fault_handler函数会找到当前正在处理的kprobe,然后根据处理状态的不同本别处理。首先若是单步执行或是重入的情况,则说明单步执行是发生了内存错误,则复位当前正在处理的kprobe,同时设置PC指针为异常触发指令地址,就好像它是一个普通的缺页异常,由内核后续的处理流程处理;若是执行pre_handler和post_handler回调函数期间出错,则递增kprobe的nmiss字段值,然后调用fault_handler回调函数执行用户指定的操作,fault_handler函数返回0,即没有修复内存异常,则会直接调用fixup_exception函数尝试修复。。

以上fault_handler回调函数分析完毕。

原文发布于微信公众号 - Linux知识积累(LinuxLearning365)

原文发表时间:2019-06-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券