Linux内核中Kprobes调试技术的实现

内核开发者一直在试图寻找一种快捷高效的内核调试手段,用于内核开发之中。高效的调试技术有利于提高内核开发效率,缩短内核开发周期。    本文研究了一种新型的内核调试技术― Kprobes Kprobes 是一个轻量级的内核调试工具,利用 Kprobes 技术可以在运行的内核中动态的插入探测点,在探测点处执行用户预定义的操作。本文首先根据 Kprobes Linux 内核中的源码实现,针对 Linux  CPU异常技术,single-step技术,Loadable Kernel Module技术以及RCU同步技术在 Kprobes 中的应用进行了研究。其次,针对 Kprobes 目前所支持的kprobe,jprobe,kretprobe等三种调试手段的实现进行了详细的分析研究。

一、Kprobes调试技术

Kprobes调试技术概述
    一直以来,内核开发者一直在试图寻找一种快捷高效的内核调试手段,用于内核开发之中。从 2.6版本的Linux开始,一种新的内核调试技术出现了,这就是Kprobes技术。
    Kprobes最早是源于IBM的Dprobe项目发展起来的,Dprobe是一个IBM公司开发的内核调试工具。从2.6.9 Linux内核开始,Kprobes被加入内核源码,并处于不断完善之中,越来越多的功能被添加到 Kprobes 内核调试技术中来。Kprobes 目前已经能在 i386,x86_64, ppc64, ia64,sparc64等CPU平台上正常工作。

    目前,大多发行版本都包含一个内核调试工具 SystemTap。SystemTap可以通过编写脚本调试内核,该工具正是依托 Kprobes 来实现的。Kprobes是一个轻量级的内核调试工具,也就是说,Kprobes的运行基本不会影响到正常内核执行的流程。
    利用Kprobes技术可以在运行的内核中动态的插入探测点,当内核运行到该探测点后可以执行用户预定义的回调函数。当执行完用户函数后又会回到正常的内核执行流程,开始新一轮的调试工作。 

    Kprobes支持三种探测方式:第一种是最基本的探测方式称为 kprobe,该探测方式支持在内核的任意位置放置探测点(除了与 Kprobes实 现相关的代码)。第二种调试方式称之为 jprobe,该探测方式主要用于调试函数传入的参数。第三种调试方式称之为 kretprobe,该调试方式是在函数返回时执行用户回调函数,利用该方式可以调试内核函数返回值。以上讨论的后两种调试方式都是基于第一种 kprobe调试方式实现的。Kprobes还支持三种回调函数类型:第一种叫做 pre_handler,该回调函数用于在执行被探测指令前执行。第二种叫 post_handler,该回调函数用于在执行完被探测指令后执行。第三种叫fault_handler,此函数用于在出现内存访问错误时进行处理。
    目前Kprobes内核调试技术已经被很多内核开发者所采用,并使用在内核开发过程的各个阶段。
    Kprobes的开发还在不断的进行之中,目前的SystemTap社区负责对该调试技术的维护以及新功能的开发,主要由IBM,Intel,Redhat等公司维护。

Kprobes配置说明
    使用 Kprobes 进行内核调试前,先要对被调试内核进行相关配置。以 Linux 2.6.20.3 版本Linux内核为例:
    首先,需要把 Kprobes 相关代码编译进内核,进入内核目录运行 make menuconfig 命令,在Instrumentation Support项目中选择Kprobes。
    其次,选择 Configure standard kernel features 中的 Include all symbols in kallsyms项,该项用于启用kallsyms_lookup_name()函数,这个函数用于检索内核函数的地址。
    在最新版本的Kprobes实现中已经支持直接利用函数名来进行注册。
    第三,选择Loadable module support中的Enable Loadable module support项,该项用于启用内核的可插入模块功能。因为Kprobes的调试是通过模块插入实现的,调试者需要编写调试模块并插入内核方式进行内核调试,因此必须选择该选项。

Kprobes中的关键技术
    Kprobes不只是纯软件的实现方案,该技术与具体硬件紧密相连,用到了一些硬件的特性。因此Kprobes的实现框架分为两个部分实现:第一部分是Kprobes的 管理,这部分是与体系结构无关的代码。第二部分是与具体CPU体系结构相关的实现代码,比如准备单步执行环境等。这些代码与具体 CPU体系结构紧密相连,因此在不同 CPU 上各有不同的实现方式。本文以下所有的讨论都是基于 Intel IA32 CPU架构,2.6.20.3内核。下文讨论用于支持Kprobes实现的四种关键技术。

Linux CPU异常处理技术以及在Kprobes中的应用
    CPU异常是在CPU运行期间,由于外围硬件发出中断信号或是执行CPU异常指令等情况所引发的。
    CPU异常可分为硬件异常和软件异常。
    硬件异常也称为硬件中断,一般是有外围硬件设备发出中断信号引起。当外围设备发出一个中断信号,该信号被发往中断处理器仲裁,比较有名的是 8259中断处理芯片,目前Intel的CPU一般采用APIC(Advanced Programmable Interrupt Controllers)来实现。中断处理器仲裁后发往 CPU的中断引脚,这时CPU会开始一次中断处理流程。
    软件异常不是由外围设备发出的,而是由程序员写入程序里的一些 CPU 异常指令引起的,比如int,int3这类指令就会引起一次软件异常。在早期的 CPU上Linux系统调用的实现就是利用 int指令。当CPU执行到这些异常指令时,同样会开始一次异常处理流程。
    操作系统CPU异常处理的实现是和特定CPU体系结构紧密相关的。Intel IA32体系的CPU的每个中断或是异常都有一个向量号,这些向量号从 0到255。Linux内核中,每一个中断向量号都对应中断描述符表(IDT)中的一项,因此中断描述符表一共有 256项。IDT每一项包含8个字节,这8个字节的其中一部分是中断处理函数的地址。表项的其他字段的含义这里不再介绍,可以从 Intel IA32程序员手册中查到。Linux内核在初始化阶段用set_trap_gate()宏初始中断描述符表。
    Linux内核中,异常处理是通过两级跳转实现的。比如,如果当CPU运行过程中接收到一次异常信号,此时CPU会到根据中断向量号到中断描述符表中找到对应项,再跳转到表项中所指的地址去执行。
    这里跳转到的地址一般情况下对应源文件 linux/arch/i386/kernel/entry.S 中的某个汇编函数入口。汇编函数经过一定的初始化工作后再跳转到C函数中去执行。这里的C函数才是真正的异常处理函数,当从C函数返回到原来的汇编函数 后,汇编函数还会做一部分后续工作。最后汇编函数执行 iret指令从中断上下文中返回到被中断的代码中继续执行。这是 Linux内核处理异常一般过程,因此称Linux的异常处理过程是两次跳转实现的。
    在Kprobes的实现中同样也用到了CPU异常,当插入一个探测点的时候,Kprobes处理例程会把插入点处的指令保存起来,然后用int3指令代替。当CPU运行到插入的int3指令时,Linux内核就进入了异常处理流程,之后再运行调试者预定义的回调函数。利用 CPU异常来实现探测点的触发并处理是Kprobes实现的关键。

single-step技术及在Kprobes中应用

    调试器的单独执行功能是非常有用的,程序员可以通过单步执行代码来确定程序执行的流程,随时掌握变量变化情况,从而精确定位程序错误发生的位置。可以说单步执行功能是一个调试器必须具备的功能。single-step技术就是为了调试器的单步执行而设计的。
    single-step技术的主要思想是,当程序执行到某条想要单独执行 CPU指令时,在执行之前产生一次CPU异常,此时把异常返回时的CPU的EFLAGS寄存器的TF(调试位)位置为1,把IF(中断屏蔽位)标志位置为 0,然后把EIP指向单步执行的指令。当单步指令执行完成后,CPU会自动产生一次调试异常(由于TF被置位)。此时,调试器一般都会把控制权又交回调试 器,回到交互模式。
    在Kprobes实现中同样也用到了single-step技术,但目的不是为了回到交互模式,而是把控制权交回Kprobes控制流程。当Kprobes完成pre_handler()处理后,就会利用single-step技术执行被调试指令。此时,Kprobes会利用debug异常,执行post_handler()。这是single-step
技术在Kprobes的主要应用。

oadable kernel module技术及在Kprobes中的应用

    loadable kernel module(LKM)技术是Linux内核的又一强大特性。
    在LKM技术出现以前,添加内核代码只能通过修改内核源码,然后重新编译内核再重启后才能生效。
    当LKM技术出现后,内核编程变的简单许多,内核开发者只需要把代码写成内核模块形式,再插入内核就可以作为内核的一部分运行。确切来讲,LKM技术为内 核开发者提供了在内核运行过程中动态插入和卸载内核模块的功能。LKM技术在不用重新编译内核的情况下,可以扩展内核的功能。目前,大量的设备驱动程序利 用LKM技术实现。驱动开发者把驱动程序写成模块的形式,并在内核启动时自动加载入内核,当然这也可以通过手动加载方式实现。
    LKM技术的出现带来了两个好处:第一,LKM技术使得驱动程序的开发和调试变得异常简单,很大程度上提高了驱动开发的效率。第二,LKM技术的出现使得 内核镜像不至于过于庞大,因为大部分的内核代码可以写成模块形式,使用时才加载。这样不会使得内核在系统内存中占用太多空间而影响系统性能。
    在利用Kprobes进行内核调试时同样用到了LKM技术。调试者需要把调试代码写成模块形式并插入内核。当调试模块被插入内核后就进入调试阶段,而当调试模块从内核中卸载时,也就意味着调试过程的结束。可以说LKM技术是Kprobes技术进行内核调试基础。

RCU技术及在Kprobes中的应用

    Linux内 核有时会访问到一些全局的数据结构,如果此时内核被抢占并且数据被修改,又或者在SMP系统(即多处理器系统)上运行,可能会有多个 CPU同时访问同一块内存的情况,此时内核数据就可能产生不一致性。为了避免这种情况,内核使用了一些同步机制,比如利用信号量同步,利用自旋锁同步等方 法。在2.6版本的内核中又引入了一种新的内核同步机制RCU,这是Read Copy Update的缩写。RCU的主要思想是分为两个部分,第一部分是防止其他访问者对被保护对象写入,第二部分是真正展开写入行为。读者可以随时访问被 RCU保护的对象而不用获得任何锁。写者先对对象的副本修改,在所有读者都退出时再执行写入行为,不同的写者之间需要同步。RCU同步机制适用于存在大量 读操作而很少写操作的情况。因为这种情况下,读操作不用获得任何锁就可以对共享对象进行读操作,极大提高了效率。
    在Kprobes对探测点数据结构的操作中也是大量存在读操作而只有很少部分写操作。为了提高效率,Kprobes的 实现中也引入了RCU机制。当发生探测点注册或是注销时,或是在执行探测点的回调函数时,探测点数据结构struct kprobe就会被RCU机制保护起来。根据RCU的原理,写者的操作是被延迟的,而如果读者发生了阻塞,那写者的操作直到读者被唤醒后才进行,这会大大 降低 RCU的效率。因此在执行Kprobes回调函数时内核的抢占以及CPU中断都被禁用了。对于Kprobes来说,用RCU机制实现回调函数访问也极大的提高了SMP系统上多探测点探测的效率,因为可以并行的执行回调函数。但在这样的机制下,回调函数必须被设计成可重入的函数。

Kprobes调试技术体系结构分析

    根据Kprobes的源码实现,从逻辑上基本可以把 Kprobes分为3个部分:第一部分是注册探测点部分,第二部分是调试处理部分,第三部分是注销探测点部分。第一部分主要功能是进行一些安全性检查,并根据要求安装探测点等工作。第二部分是 Kprobes实现的关键,该部分根据探测类型执行预定的操作。这里的探测类型主要有三种:kprobe,jprobe和kretprobe。这部分还会完成被探测指令单步执行等操作。第三部分是当调试者撤销探测要求时,对探测点的注销操作,主要是恢复被探测指令等工作。


    下面分别介绍kprobe,jprobe和kretprobe的实现机制。
3.1 kprobe的实现
3.1.1 相关数据结构与函数分析 
1)  struct kprobe结构
    该数据结构是整个 Kprobes 体系的基础,所有 Kprobes 的行为都是围绕该结构展开。以下是
struct kprobe结构的主要成员:

 


2)  struct notifier_block 结构
    该结构用于注册异常发生时调用的回调函数。 int (*notifier_call)(struct notifier_block *, unsigned long, void *)成员是回调函数指针,int priority成员用于设置调用优先级。Kprobes中定义的优先级为最高优先级,确保注册的回调函数被首先调用到。

3)  register_kprobe()函数
    该函数用于完成struct kprobe结构的注册,插入探测点等操作。

4)  kprobe_handler()函数
    该函数用于处理由kprobe引发的int3异常。

5)  post_kprobe_handler ()函数
    该函数用于处理由kprobe引发的debug异常。

kprobe处理流程分析

kprobe是Kprobes实现体系中最底层也是最基本的一种探测方式,jprobe和kretprobe探测方式都是通过kprobe来实现的。kprobe的主要处理流程如下图所示:

1)kprobe的注册过程
    当调试者向内核插入一个 kprobe 模块时,首先会执行注册探测点操作。该操作主要由register_kprobe()函数完成,在下文中都称该函数为注册器。注册器的参数中包含一个 struct kprobe结构,该结构由调试者在调试模块中创建。
    首先,注册器会进行一些正确性检查工作,判断传入的 struct kprobe结构中symbol_name和addr 是否同时存在,如果是则返回错误。之后还会判断探测地址是否在内核代码段中并且不在Kprobes实现相关的代码中。这样的检查是很必要的,如果探测地址出现在Kprobes实现相关代码中就会造成递归现象。如果被探测的地址已经被注册过,则会在kprobe_table中以链表形式组织。
    其次,注册器会保存被探测地址的指令码到struct kprobe结构的ainsn.ainsn中去,以便以后进行single-step操作。对kprobe初始化完成后,注册器会把传入的struct kprobe结构指针插入哈希表中。最后,注册器把被探测的指令的第一个字节替换成 int3指令。到这里,kprobe的注册工作就完成了。

2)kprobe int3异常处理过程
    完成注册后kprobe的准备工作就完成了,一旦内核执行到被探测的指令,也就是注册时被替换成的int3指令时,就会引发一次软件异常。CPU会根据中断描述符表执行中断处理函数,int3的中断处理函数在/linux/arch/i386/kernel/entry.S中实现,KPROBE_ENTRY(int3)就是该中断处理函数的入口。汇编中断处理函数会调用 do_int3()函数,作为 int3 中断处理的 C 语言处理函数。
    do_int3()函数一开始就会去调用notify_die()函数,该函数的主要作用是调用内核代码注册的异常的 回 调 函 数 。 在 Kprobes 的初始化代码(init_Kprobes()函数)中调用了register_die_notifier() 用于注册异常回调函数 。 Kprobes注册的异常回调函数为probe_exceptions_notify()。
    此时执行权交到Kprobes之 中,kprobe_exception_notify()函数开始执行。该函数的参数中有一个参数val,该参数可以用于判断当前回调函数由有什么异常产 生的。这里异常由 int3指令产生,因此接收到的参数应该为“DIE_INT3”。此时,又会调用 kprobe_handler()函数,该函数是Kprobes处理int3异常的主要实现函数。该函数首先会把发生异常的地址记录下来,因为该地址就是注册探测点的地址。为了防止内核被抢占,该函数禁止内核抢占功能。在 i386 CPU上,进入int3中断处理时已经关闭CPU中断,目前Kprobes的实现中只有i386体系上会关闭CPU中断,在其他体系上的实现都没有这样做。
    接着,开始检查此次 int3 异常是否是由前一次 Kprobes 处理流程引发的,如果是由前一次Kprobes处理流程引发,则有两种可能性。第一是该次Kprobes处理由于前一次回调函数执行了被探测代码造成的,第二种可能性是由于 jprobe造成的,这部分将在 jprobe的实现一节中详细讨论。如果int3异常不是由前一次Kprobes处理流程引发的,根据先前记录下来的探测点地址到哈希表中找到已注册的struct kprobe结构。如果该结构中包含了pre_handler函数指针,则执行该预定的函数。

    执行完用户定义的 pre_handler函数时,已经完成了一部分的调试工作。接下来,就开始准备single-step步骤,该步骤用 prepare_singlestep()函数完成。这个函数与体系结构相关,下面是prepare_singlestep()函数在i386体系CPU 上的主要实现代码:
程序1  prepare_singlestep()函数部分代码
01 regs->eflags |= TF_MASK;
02 regs->eflags &= ~IF_MASK;
03 regs->eip = (unsigned long)p->ainsn.insn;
上面的代码中设置了EFLAGS中的TF位并清空IF位,同时把异常返回的指令寄存器地址改为保存起来的原探测指令处,当异常返回时这些设置就会生效。 single-step技术已经在上文中讨论过,这里不再赘述。执行完被探测的指令后,由于 CPU的标志寄存器被置位,此时又会引发一次CPU异常,该异
常在Linux内核中被称为DEBUG异常。

3)Kprobe DEBUG异常处理
    Linux内核中对DEBUG异常的处理方式与处理int3异常很类似。DEGUG异常的中断处理函数也是在/linux/arch/i386/kernel/entry.S 中实现,KPROBE_ENTRY(debug)就是该异常的中断处理函数的入口。该函数会调用do_debug()函数进一步处理DEBUG异常,同样 的notify_die()函数被调用。与int3异常不同的是此时传入notify_die()函数的第一个参数是“DIE_DEBUG”。
    最终,notify_die() 函数会调用Kprobes初始化时注册的回调函数kprobe_exceptions_notify() 。此时,控制权又一次交回Kprobes 。
    kprobe_exceptions_notify() 判断传入的类型为DIE_DEBUG,这时会去调用post_kprobe_handler ()函数。post_kprobe_handler ()首先判断用户定义的post_handler回调函数是否存在,如果存在则执行之。
    之后,会调用 resume_execution()函数做一些会做恢复工作,该函数会把 EFLAGES寄存器的TF 为清空,并根据被探测指令类型的不同,做不同的处理。在 resume_execution()返回后,post_kprobe_handler ()函数就会启用在 int3 异常处理中被禁止的内核抢占功能。到这里,Kprobes对DEBUG异常的处理基本完成了,又把控制权交回内核。
    以上是kprobe执行的主要流程,可以看出kprobe利用了两次CPU异常的方式执行了用户定义的pre_handler 和 post_handler 回调函数。并通过 single-step 技术执行了被探测指令。当一次kprobe 执行周期完成后,又开始等待新一轮执行周期的到来。只有当调试者卸载了调试模块后,kprobe的生命周期才算结束。

jprobe的实现
    相关数据结构与函数分析
1) struct jprobe结构
    该结构在注册jprobe探测点时使用,它包含两个成员:
    struct kprobe kp;//这是jprobe一个内嵌的struct kprobe结构成员,因为jprobe是基于kprobe实现的。
    kprobe_opcode_t *entry;//这是被探测函数的代理函数。

2) setjmp_pre_handler()函数
    该函数作为jprobe内嵌kprobe的pre_handler,在探测点被触发时首先会被调用到。

3) longjmp_break_handler()函数
    该函数作为jprobe内嵌kprobe的break_handler,当再次进入int3异常时被调用。

jprobe处理流程分析

    jprobe是Kprobes中实现的另一种调试方式,该调试方式主要为了满足调试内核函数传递的参数的情况。jprobe是基于kprobe实现的,是kprobe调试的一种扩展形式。
    jprobe的基本原理是利用了一个探测代理函数来接收传入参数,做相应处理后再把控制权交回被调试函数。下图为jprobe的执行流程图:

1)jprobe的注册过程
    由于jprobe用于调试传入参数情况,用户在编写jprobe探测模块时与编写kprobe模块有所不同。jprobe结构定义时只要给entry成员 赋值成调试代理函数,该函数的参数类型必须与被探测函数完全相同。当一个 jprobe 探测模块插入内核后,jprobe 的注册过程会被启动。首先,jp->kp.pre_handler 会 被 设 置 成 setjmp_pre_handler , jp->kp.break_handler 被设置成longjmp_break_handler,这两个函数会在以后讨论。之后会调用kprobe的注册函数进行探测点的插入。在之前的章节中已经 详细分析了kprobe的注册过程,这里不再重复。

2)jprobe探测点触发过程
    与 kprobe 相同,当内核执行到由 jprobe 插入的探测点时同样会产生 int3 CPU 异常。在kprobe实现一节,已经详细的分析了 kprobe对int3 CPU异常的处理方式。最终pre_handler函数会被调用。在 jprobe 中,pre_handler 不是由调试者定义的,而是在注册时被赋值成了setjmp_pre_handler函数。该函数会把异常发生时的堆栈内容保存起来,并且把异常返回时的 EIP值设为调试代理函数的地址。
    经过以上处理,int3中断返回时就会去执行调试代理函数而不是被探测的函数。代理函数执行完用户定义操作后必须调用 jprobe_return()函数,目的是为了确保执行流程能回到被探测的函数中。
    jprobe_return()函数利用内嵌汇编再次执行int3指令,此时又会发生一次int3 CPU异常处理。
    当异常处理执行到kprobe_handler()函数进行判断是否有kprobe正在运行时,发现目前有kprobe正在运行,而此时产生异常的地址并 没有被注册过。这种情况下,struct kprobe 结构中的break_handler函数会被调用,也就是在jprobe注册阶段注册的longjmp_break_handler()函数开始执行。       该函数主要作用是把在setjmp_pre_handler函数中保存起来的堆栈内容以及寄存器进行恢复,当异常返回时其环境与jprobe探测点异常发 生时完全相同。
    接着kprobe_handler()又会准备好单步执行的环境,并单步执行被探测指令,同时产生 Debug异常。在 kprobe 实现一节已经详细分析了 Debug 异常处理过程。当 Debug 异常返回时也就回到了jprobe探测指令的下一条指令的位置继续执行。
    通过以上分析可以看出,jprobe是基于kprobe调试方式实现的,jprobe利用了三次CPU异常,产生的前两次CPU异常都是int3异常,第 三次产生了Debug异常。jprobe主要通过代理函数的方式来实现传入参数的调试,并利用修改异常返回地址的方式来控制执行的流程。

kretprobe的实现
    相关数据结构与函数分析
1) struct kretprobe结构
    该结构是kretprobe实现的基础数据结构,以下是该结构的成员:
    struct kprobe kp; //该成员是kretprobe内嵌的struct kprobe结构。
    kretprobe_handler_t handler;//该成员是调试者定义的回调函数。
    int maxactive;//该成员是最多支持的返回地址实例数。
    int nmissed;//该成员记录有多少次该函数返回没有被回调函数处理。
    struct hlist_head free_instances;
    用于链接未使用的返回地址实例,在注册时初始化。
    struct hlist_head used_instances;//该成员是正在被使用的返回地址实例链表。

2) struct kretprobe_instance结构
    该结构表示一个返回地址实例。因为函数每次被调用的地方不同,这造成了返回地址不同,因此需要为每一次发生的调用记录在这样一个结构里面。以下是该结构的成员:
    struct hlist_node uflist;
    该成员被链接入kretprobe的used_instances或是free_instances链表。
    struct kretprobe *rp;//该成员指向所属的kretprobe结构。
    kprobe_opcode_t *ret_addr;//该成员用于记录被探测函数正真的返回地址。
    struct task_struct *task;//该成员记录当时运行的进程。

3) pre_handler_kretprobe()函数
    该函数在kretprobe探测点被执行到后,用于修改被探测函数的返回地址。

4) trampoline_handler()函数
    该函数用于执行调试者定义的回调函数以及把被探测函数的返回地址修改回原来的返回地址。

kretprobe处理流程分析
    kretprobe探测方式是基于kprobe实现的又一种内核探测方式,该探测方式主要用于在函数返回时进行探测,获得内核函数的返回值,还可以用于计算函数执行时间等方面。

1) kretprobe的注册过程
    调试者要进行kretprobe调试首先要注册处理,这需要在调试模块中调用register_kretprobe(),下文中称该函数为 kretprobe 注册器。kretprobe 注册器对传入的kretprobe结构的中kprobe.pre_handler赋值为pre_handler_kretprobe()函数,用于在探测 点被触发时被调用。接着,kretprobe注册器还会初始化kretprobe的一些成员,比如分配返回地址实例的空间等操作。最后, kretprobe注册器会利用 kretprobe内嵌的struct kprobe结构进行kropbe的注册。自此,kretprobe注册过程就完成了。

2) kretprobe探测点的触发过程
    kretprobe触发是在刚进入被探测函数的第一条汇编指令时发生的,因为 kretprobe注册时把该地址修改位int3指令。
    此时发生了一次CPU异常处理,这与kprobe探测点被触发相同。但与kprobe处理不同的是,这里并不是运行用户定义的 pre_handler函数,而是执行pre_handler_kretprobe()函数,该函数又会调用 arch_prepare_kretprobe()函数。arch_prepare_kretprobe()函数的主要功能是把被探测函数的返回地址变换 为&kretprobe_trampoline所在的地址,这是一个汇编地址标签。这个标签的地址在 kretprobe_trampoline_holder()中用汇编伪指令定义。替换函数返回地址是kretprobe实现的关键。当被探测函数返回 时,返回到&kretprobe_trampoline地址处开始运行。
    接着,在一些保护现场的处理后,又去调用trampoline_handler()函数。该函数的主要有两个作用,一是根据当前的实例去运行用户定义的调 试函数,也就是 kretprobe结构中的handler所指向的函数,二是把返回值设成被探测函数正真的返回地址。最后,在进行一些堆栈的处理后,被探测函数又返回到 了正常执行流程中去。
    以上讨论的就是kretprobe的执行过程,可以看出,该调试方式的关键点在于修改被探测函数的返回地址到kprobes的控制流程中,之后再把返回地址修改到原来的返回地址并使得该函数继续正常执行。

结束语
    Kprobes内核调试技术的优势是显而易见的:调试者可以通过 Kprobes提供的简单接口对Kprobes探测点进行注册,通过编写回调函数就可以实现调试,使用起来非常简便。利用 Kprobes调试时,可以以内核模块的形式编写调试代码。这样做可以在不重新编译内核的情况下实现内核调试,大大提高了调试的效率。目前Kprobes提供了三种调试手段:kprobe,jprobe,kretprobe,这三种手段可以满足不同的调试目的。同时Kprobes支持除了与Kprobes实现相关代码外的任意内核代码的调试,能满足大多数调试需要。Kprobes的分为体系结构无关和体系结构相关两部分代码实现,这样做增加了Kprobes的可扩展性。虽然优势明显,但Kprobes还是存在一些不足。比如,Kprobes虽然提供了丰富的调试手段,但调试者无法对插入的调试模块有
效的控制和管理。另外,Kprobes是一个汇编级的调试技术,只能对寄存器以及内存地址级别进行调试,目前还无法对内核函数中的局部变量等,也就是源码级别进行有效的调试。目前Kprobes还不能支持所有的CPU架构。这些不足在一定程度上限制了Kprobes的使用。

你可能感兴趣的:(Linux内核中Kprobes调试技术的实现)