kprobe 的原理、编程接口、局限性和使用注意事项

本系列文章详细地介绍了一个Linux下的全新的调式、诊断和性能测量工具Systemtap和它所依赖的基础kprobe以及促使开发该工具的先驱DTrace并给出实际使用例子使读者更进一步了解和认识这些工具。 本文是该系列文章之一,它讲解了kprobe的原理、编程接口、局限性和使用注意事项并给出实际使用示例帮助读者理解和认识kprobe。本系列文章之二讲解了DTrace以及Systemtap与DTrace比较。本系列文章之三讲解了Systemtap的原理,并通过一个例子向读者展示Systemtap的工作机理。

一、kprobe简介

kprobe是一个动态地收集调试和性能信息的工具,它从Dprobe项目派生而来,是一种非破坏性工具,用户用它几乎可以跟踪任何函数或被执行的指令以及一些异步事件(如timer)。它的基本工作机制是:用户指定一个探测点,并把一个用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。

kprobe实现了三种类型的探测点: kprobes, jprobes和kretprobes (也叫返回探测点)。 kprobes是可以被插入到内核的任何指令位置的探测点,jprobes则只能被插入到一个内核函数的入口,而kretprobes则是在指定的内核函数返回时才被执行。

一般,使用kprobe的程序实现作一个内核模块,模块的初始化函数来负责安装探测点,退出函数卸载那些被安装的探测点。kprobe提供了接口函数(APIs)来安装或卸载探测点。目前kprobe支持如下架构:i386、x86_64、ppc64、ia64(不支持对slot1指令的探测)、sparc64 (返回探测还没有实现)。

二、kprobe实现原理

当安装一个kprobes探测点时,kprobe首先备份被探测的指令,然后使用断点指令(即在i386和x86_64的int3指令)来取代被探测指令的头一个或几个字节。当CPU执行到探测点时,将因运行断点指令而执行trap操作,那将导致保存CPU的寄存器,调用相应的trap处理函数,而trap处理函数将调用相应的notifier_call_chain(内核中一种异步工作机制)中注册的所有notifier函数,kprobe正是通过向trap对应的notifier_call_chain注册关联到探测点的处理函数来实现探测处理的。当kprobe注册的notifier被执行时,它首先执行关联到探测点的pre_handler函数,并把相应的kprobe struct和保存的寄存器作为该函数的参数,接着,kprobe单步执行被探测指令的备份,最后,kprobe执行post_handler。等所有这些运行完毕后,紧跟在被探测指令后的指令流将被正常执行。

jprobe通过注册kprobes在被探测函数入口的来实现,它能无缝地访问被探测函数的参数。jprobe处理函数应当和被探测函数有同样的原型,而且该处理函数在函数末必须调用kprobe提供的函数jprobe_return()。当执行到该探测点时,kprobe备份CPU寄存器和栈的一些部分,然后修改指令寄存器指向jprobe处理函数,当执行该jprobe处理函数时,寄存器和栈内容与执行真正的被探测函数一模一样,因此它不需要任何特别的处理就能访问函数参数, 在该处理函数执行到最后时,它调用jprobe_return(),那导致寄存器和栈恢复到执行探测点时的状态,因此被探测函数能被正常运行。需要注意,被探测函数的参数可能通过栈传递,也可能通过寄存器传递,但是jprobe对于两种情况都能工作,因为它既备份了栈,又备份了寄存器,当然,前提是jprobe处理函数原型必须与被探测函数完全一样。

kretprobe也使用了kprobes来实现,当用户调用register_kretprobe()时,kprobe在被探测函数的入口建立了一个探测点,当执行到探测点时,kprobe保存了被探测函数的返回地址并取代返回地址为一个trampoline的地址,kprobe在初始化时定义了该trampoline并且为该trampoline注册了一个kprobe,当被探测函数执行它的返回指令时,控制传递到该trampoline,因此kprobe已经注册的对应于trampoline的处理函数将被执行,而该处理函数会调用用户关联到该kretprobe上的处理函数,处理完毕后,设置指令寄存器指向已经备份的函数返回地址,因而原来的函数返回被正常执行。

被探测函数的返回地址保存在类型为kretprobe_instance的变量中,结构kretprobe的maxactive字段指定了被探测函数可以被同时探测的实例数,函数register_kretprobe()将预分配指定数量的kretprobe_instance。如果被探测函数是非递归的并且调用时已经保持了自旋锁(spinlock),那么maxactive为1就足够了; 如果被探测函数是非递归的且运行时是抢占失效的,那么maxactive为NR_CPUS就可以了;如果maxactive被设置为小于等于0, 它被设置到缺省值(如果抢占使能, 即配置了 CONFIG_PREEMPT,缺省值为10和2*NR_CPUS中的最大值,否则缺省值为NR_CPUS)。

如果maxactive被设置的太小了,一些探测点的执行可能被丢失,但是不影响系统的正常运行,在结构kretprobe中nmissed字段将记录被丢失的探测点执行数,它在返回探测点被注册时设置为0,每次当执行探测函数而没有kretprobe_instance可用时,它就加1。

三、kprobe的接口函数

kprobe为每一类型的探测点提供了注册和卸载函数。

1.register_kprobe

它用于注册一个kprobes类型的探测点,其函数原型为:

int register_kprobe(struct kprobe *kp);

为了使用该函数,用户需要在源文件中包含头文件linux/kprobes.h。

该函数的参数是struct kprobe类型的指针,struct kprobe包含了字段addr、pre_handler、post_handler和fault_handler,addr指定探测点的位置,pre_handler指定执行到探测点时执行的处理函数,post_handler指定执行完探测点后执行的处理函数,fault_handler指定错误处理函数,当在执行pre_handler、post_handler以及被探测函数期间发生错误时,它会被调用。在调用该注册函数前,用户必须先设置好struct kprobe的这些字段,用户可以指定任何处理函数为NULL。

该注册函数会在kp->addr地址处注册一个kprobes类型的探测点,当执行到该探测点时,将调用函数kp->pre_handler,执行完被探测函数后,将调用kp->post_handler。如果在执行kp->pre_handler或kp->post_handler时或在单步跟踪被探测函数期间发生错误,将调用kp->fault_handler。

该函数成功时返回0,否则返回负的错误码。

探测点处理函数pre_handler的原型如下:

int pre_handler(struct kprobe *p, struct pt_regs *regs);

用户必须按照该原型参数格式定义自己的pre_handler,当然函数名取决于用户自己。参数p就是指向该处理函数关联到的kprobes探测点的指针,可以在该函数内部引用该结构的任何字段,就如同在使用调用register_kprobe时传递的那个参数。参数regs指向运行到探测点时保存的寄存器内容。kprobe负责在调用pre_handler时传递这些参数,用户不必关心,只是要知道在该函数内你能访问这些内容。

一般地,它应当始终返回0,除非用户知道自己在做什么。

探测点处理函数post_handler的原型如下:

void post_handler(struct kprobe *p, struct pt_regs *regs,
unsigned long flags);

前两个参数与pre_handler相同,最后一个参数flags总是0。

错误处理函数fault_handler的原刑如下:

int fault_handler(struct kprobe *p, struct pt_regs *regs, int trapnr);

前两个参数与pre_handler相同,第三个参数trapnr是与错误处理相关的架构依赖的trap号(例如,对于i386,通常的保护错误是13,而页失效错误是14)。

如果成功地处理了异常,它应当返回1。

2.register_jprobe

该函数用于注册jprobes类型的探测点,它的原型如下:

int register_jprobe(struct jprobe *jp);

为了使用该函数,用户需要在源文件中包含头文件linux/kprobes.h。

用户在调用该注册函数前需要定义一个struct jprobe类型的变量并设置它的kp.addr和entry字段,kp.addr指定探测点的位置,它必须是被探测函数的第一条指令的地址,entry指定探测点的处理函数,该处理函数的参数表和返回类型应当与被探测函数完全相同,而且它必须正好在返回前调用jprobe_return()。如果被探测函数被声明为asmlinkage、fastcall或影响参数传递的任何其他形式,那么相应的处理函数也必须声明为相应的形式。

该注册函数在jp->kp.addr注册一个jprobes类型的探测点,当内核运行到该探测点时,jp->entry指定的函数会被执行。

如果成功,该函数返回0,否则返回负的错误码。

3.register_kretprobe

该函数用于注册类型为kretprobes的探测点,它的原型如下:

int register_kretprobe(struct kretprobe *rp);

为了使用该函数,用户需要在源文件中包含头文件linux/kprobes.h。

该注册函数的参数为struct kretprobe类型的指针,用户在调用该函数前必须定义一个struct kretprobe的变量并设置它的kp.addr、handler以及maxactive字段,kp.addr指定探测点的位置,handler指定探测点的处理函数,maxactive指定可以同时运行的最大处理函数实例数,它应当被恰当设置,否则可能丢失探测点的某些运行。

该注册函数在地址rp->kp.addr注册一个kretprobe类型的探测点,当被探测函数返回时,rp->handler会被调用。

如果成功,它返回0,否则返回负的错误码。

kretprobe处理函数的原型如下:

int kretprobe_handler(struct kretprobe_instance *ri, struct pt_regs *regs);

参数regs指向保存的寄存器,ri指向类型为struct kretprobe_instance的变量,该结构的ret_addr字段表示返回地址,rp指向相应的kretprobe_instance变量,task字段指向相应的task_struct。结构struct kretprobe_instance是注册函数register_kretprobe根据用户指定的maxactive值来分配的,kprobe负责在调用kretprobe处理函数时传递相应的kretprobe_instance。

4.unregister_*probe

对应于每一个注册函数,有相应的卸载函数。

void unregister_kprobe(struct kprobe *kp);
void unregister_jprobe(struct jprobe *jp);
void unregister_kretprobe(struct kretprobe *rp);

上面是对应与三种探测点类型的卸载函数,当使用探测点的模块卸载或需要卸载已经注册的探测点时,需要使用相应的卸载函数来卸载已经注册的探测点,kp,jp和rp分别为指向结构struct kprobe,struct jprobe和struct kretprobe的指针,它们应当指向调用对应的注册函数时使用的那个结构,也就说注册和卸载必须针对同样的探测点,否则会导致系统崩溃。这些卸载函数可以在注册后的任何时刻调用。

四、kprobe的特点和限制

kprobe允许在同一地址注册多个kprobes,但是不能同时在该地址上有多个jprobes。

通常,用户可以在内核的任何位置注册探测点,特别是可以对中断处理函数注册探测点,但是也有一些例外。如果用户尝试在实现kprobe的代码(包括kernel/kprobes.c和arch/*/kernel/kprobes.c以及do_page_fault和notifier_call_chain)中注册探测点,register_*probe将返回-EINVAL.

如果为一个内联(inline)函数注册探测点,kprobe无法保证对该函数的所有实例都注册探测点,因为gcc可能隐式地内联一个函数。因此,要记住,用户可能看不到预期的探测点的执行。

一个探测点处理函数能够修改被探测函数的上下文,如修改内核数据结构,寄存器等。因此,kprobe可以用来安装bug解决代码或注入一些错误或测试代码。

如果一个探测处理函数调用了另一个探测点,该探测点的处理函数不将运行,但是它的nmissed数将加1。多个探测点处理函数或同一处理函数的多个实例能够在不同的CPU上同时运行。

除了注册和卸载,kprobe不会使用mutexe或分配内存。

探测点处理函数在运行时是失效抢占的,依赖于特定的架构,探测点处理函数运行时也可能是中断失效的。因此,对于任何探测点处理函数,不要使用导致睡眠或进程调度的任何内核函数(如尝试获得semaphore)。

kretprobe是通过取代返回地址为预定义的trampoline的地址来实现的,因此栈回溯和gcc内嵌函数__builtin_return_address()调用将返回trampoline的地址而不是真正的被探测函数的返回地址。

如果一个函数的调用次数与它的返回次数不相同,那么在该函数上注册的kretprobe探测点可能产生无法预料的结果(do_exit()就是一个典型的例子,但do_execve() 和 do_fork()没有问题)。

当进入或退出一个函数时,如果CPU正运行在一个非当前任务所有的栈上,那么该函数的kretprobe探测可能产生无法预料的结果,因此kprobe并不支持在x86_64上对__switch_to()的返回探测,如果用户对它注册探测点,注册函数将返回-EINVAL。

五、如何让内核支持kprobe

kprobe已经被包含在2.6内核中,但是只有最新的内核才提供了上面描述的全部功能,因此如果读者想实验本文附带的内核模块,需要最新的内核,作者在2.6.18内核上测试的这些代码。内核缺省时并没有使能kprobe,因此用户需使能它。

为了使能kprobe,用户必须在编译内核时设置CONFIG_KPROBES,即选择在“Instrumentation Support“中的“Kprobes”项。如果用户希望动态加载和卸载使用kprobe的模块,还必须确保“Loadable module support” (CONFIG_MODULES)和“Module unloading” (CONFIG_MODULE_UNLOAD)设置为y。如果用户还想使用kallsyms_lookup_name()来得到被探测函数的地址,也要确保CONFIG_KALLSYMS设置为y,当然设置CONFIG_KALLSYMS_ALL为y将更好。

六、kprobe使用实例

本文附带的包包含了三个示例模块,kprobe-exam.c是kprobes使用示例,jprobe-exam.c是jprobes使用示例,kretprobe-exam.c是kretprobes使用示例,读者可以下载该包并执行如下指令来实验这些模块:

$ tar -jxvf kprobes-examples.tar.bz2
$ cd kprobes-examples
$ make

$ su -

$ insmod kprobe-example.ko
$ dmesg

$ rmmod kprobe-example
$ dmesg

$ insmod jprobe-example.ko
$ cat kprobe-example.c
$dmesg

$ rmmod jprobe-example
$ dmesg

$ insmod kretprobe-example.ko
$ dmesg

$ ls -Rla / > /dev/null &
$ dmesg

$ rmmod kretprobe-example
$ dmesg

$

示例模块kprobe-exame.c探测schedule()函数,在探测点执行前后分别输出当前正在运行的进程、所在的CPU以及preempt_count(),当卸载该模块时将输出该模块运行时间以及发生的调度次数。这是该模块在作者系统上的输出:

kprobe registered
current task on CPU#1: swapper (before scheduling), preempt_count = 0
current task on CPU#1: swapper (after scheduling), preempt_count = 0
current task on CPU#0: insmod (before scheduling), preempt_count = 0
current task on CPU#0: insmod (after scheduling), preempt_count = 0
current task on CPU#1: klogd (before scheduling), preempt_count = 0
current task on CPU#1: klogd (after scheduling), preempt_count = 0
current task on CPU#1: klogd (before scheduling), preempt_count = 0
current task on CPU#1: klogd (after scheduling), preempt_count = 0
current task on CPU#1: klogd (before scheduling), preempt_count = 0

Scheduling times is 5918 during of 7655 milliseconds.
kprobe unregistered

示例模块jprobe-exam.c是一个jprobes探测例子,它示例了获取系统调用open的参数,但读者不要试图在实际的应用中这么使用,因为copy_from_user可能导致睡眠,而kprobe并不允许在探测点处理函数中这么做(请参看前面内容了解详细描述)。

这是该模块在作者系统上的输出:

Registered a jprobe.
process 'cat' call open('/etc/ld.so.cache', 0, 0)
process 'cat' call open('/lib/libc.so.6', 0, -524289)
process 'cat' call open('/usr/lib/locale/locale-archive', 32768, 1)
process 'cat' call open('/usr/share/locale/locale.alias', 0, 438)
process 'cat' call open('/usr/lib/locale/en_US.UTF-8/LC_CTYPE', 0, 0)
process 'cat' call open('/usr/lib/locale/en_US.utf8/LC_CTYPE', 0, 0)
process 'cat' call open('/usr/lib/gconv/gconv-modules.cache', 0, 0)
process 'cat' call open('kprobe-exam.c', 32768, 0)

process 'rmmod' call open('/etc/ld.so.cache', 0, 0)
process 'rmmod' call open('/lib/libc.so.6', 0, -524289)
process 'rmmod' call open('/proc/modules', 0, 438)
jprobe unregistered

示例模块kretprobe-exam.c是一个返回探测例子,它探测系统调用open并输出返回值小于0的情况。它也有意设置maxactive为1,以便示例丢失探测运行的情况,当然,只有系统并发运行多个sys_open才可能导致这种情况,因此,读者需要有SMP的系统或者有超线程支持才能看到这种情况。如果读者比较仔细,会看到在前面的命令有”ls -Rla / > /dev/null & ,那是专门为了导致出现丢失探测运行的。

这是该模块在作者系统上的输出:

Registered a return probe.
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2
sys_open returns -2

kretprobe unregistered
Missed 11 sys_open probe instances.

小结

本文详细地讲解了kprobe的方方面面并给出实际的例子代码帮助读者学习和使用kprobe。本文是系列文章“Linux下的一个全新的性能测量和调式诊断工具 -- Systemtap”之一,有兴趣的读者可以阅读该系列文章之二和三。

参考资料

  1. Gaining insight into the Linux kernel with Kprobes
  2. Kernel debugging with Kprobes
  3. Linux kernel documentation, Documentation/kprobes.txt
  4. Linux kernel source code, linux-2.6.18
  5. 使用Kprobes 调试内核
  6. Systemtap
  7. 中国Linux大学

Linux 下的一个全新的性能测量和调式诊断工具 Systemtap,第 1 部分: kprobe的更多相关文章

  1. Linux 下的一个全新的性能测量和调式诊断工具 Systemtap, 第 3 部分: Systemtap

    Systemtap的原理,Systemtap与DTrace比较,以及安装要求和安装步骤本系列文章详细地介绍了一个Linux下的全新的调式.诊断和性能测量工具Systemtap和它所依赖的基础kprob ...

  2. Linux 下的一个全新的性能测量和调式诊断工具 Systemtap, 第 2 部分: DTrace

    DTrace的原理本系列文章详细地介绍了一个 Linux 下的全新的调式.诊断和性能测量工具 Systemtap 和它所依赖的基础 kprobe 以及促使开发该工具的先驱 DTrace 并给出实际使用 ...

  3. Linux下性能测量和调试诊断工具Systemtap

    一.简介 SystemTap是一个诊断Linux系统性能或功能问题的开源软件.它使得对运行时的Linux系统进行诊断调式变得更容易.更简单.有了它,开发者或调试人员不再需要重编译.安装新内核.重启动等 ...

  4. Linux下配置一个VNC服务器

    在Linux下配置一个VNC服务器,并设置2个用户,要求其中一个用户登录时不需要输入密码. 然后在客户端使用ssh+vncview的方式访问. 1确认vnc安装 2配置vncserver 3测试vnc ...

  5. 如何在Linux下拷贝一个目录呢

    cp -af newadmin/movie/.   uploadfile/mallvideo/ 如何在Linux下拷贝一个目录呢?这好像是再简单不过的问题了. 比如要把/home/usera拷贝到/m ...

  6. 如何在linux下制作一个windows的可启动u盘?

    如何在linux下制作一个windows的可启动u盘? 情景是这样的,有一个windows10的iso,现在想通过U盘安装,要求即支持UEFI(启动引导器),又支持Legacy(启动引导器),因为有一 ...

  7. 【转载】在Linux下,一个文件也有三种时间,分别是:访问时间、修改时间、状态改动时间

    在windows下,一个文件有:创建时间.修改时间.访问时间.而在Linux下,一个文件也有三种时间,分别是:访问时间.修改时间.状态改动时间. 两者有此不同,在Linux下没有创建时间的概念,也就是 ...

  8. 在Linux下制作一个磁盘文件,在u-boot 阶段对emmc 烧写整个Linux系统方法

    在Linux 下制作一个磁盘文件, 可以给他分区,以及存储文件,然后dd 到SD卡便可启动系统. 在u-boot 下启动后可以读取该文件,直接在u-boot 阶段就可以做烧写操作,省略了进入系统后才进 ...

  9. linux下,一个运行中的程序,究竟占用了多少内存

    linux下,一个运行中的程序,究竟占用了多少内存 1. 在linux下,查看一个运行中的程序, 占用了多少内存, 一般的命令有 (1). ps aux: 其中  VSZ(或VSS)列 表示,程序占用 ...

随机推荐

  1. ubuntu临时修改ip,mac的方法示例

    ifconfig eth0 down ifconfig eth0 154.84.28.148 netmask 255.255.255.0 route add default gw 154.84.28. ...

  2. iOS10 越狱, openSSH

    iOS 10 已经可以越狱, 不过比较蛋疼的是非完美越狱,每次重启都要从新越狱. 感兴趣的同学可以尝试一下,本人使用同步推上的教程,亲测可用. 越狱完后想安装OpenSSH, 在Cydia上搜索安装, ...

  3. .Net中集合排序还可以这么玩

    背景: public class StockQuantity { public StockQuantity(string status, DateTime dateTime, int quantity ...

  4. chrome浏览器再次打开黑屏一段时间

    打开chrome设置 最下面-显示高级设置 再拉到最下面-使用硬件加速模式(把勾去掉)

  5. C#之Excel操作

    下面的这几个方法是我在项目中经常用到的,欢迎大家批评指正 读取Excel表中的数据 第一种:功能丰富,速度慢 /// <summary> /// 从Excel读取数据 /// </s ...

  6. python-文件操作和集合

    1.打开文件 如果文件不存在会报错 f = open('information.txt','r+') 2.读取文件 read 读取文件 readline 读取文件的一行内容 readlines 读取文 ...

  7. 深入java多线程一

    涉及到 1.线程的启动(start) 2.线程的暂停(suspend()和resume()) 3.线程的停止(interrupt与异常停止,interrupt与睡眠中停止,stop(),return) ...

  8. Centos常用命令之:文件与目录管理

    在centos中常用的文件与目录操作命令有: ◇chmod:修改文件或目录的权限 ◇mkdir:新建目录◇rmdir:删除目录◇rm:删除目录或文件◇cp:复制目录或文件◇mv:移动目录或文件 下面就 ...

  9. 51nod 1752 哈希统计

    Description Solution 考虑用倍增来处理答案: 设 \(f[i][j]\) 表示长度恰好为 \(2^{i}\) 的哈希值为 \(j\) 的字符串的种数 \(dp[i][j]\) 表示 ...

  10. hdu 5480(前缀和)

    题意:如果一个点,则这点的横竖皆被占领,询问矩阵是否全被占领. 思路:将被占领的x,y标记为1,用x表示1 - i的和 如果x轴的差为 x2 - x1 + 1则表示全被占领,y轴同理 #include ...