内核探测kprobe

kprobe(内核探测,kernel probe)是一个动态地收集调试和性能信息的工具,如:收集寄存器和全局数据结构等调试信息,无需对Linux内核频繁编译和启动。用户可以在任何内核代码地址进行陷阱,指定调试断点触发时的处理例程。工作机制是:用户指定一个探测点,并把用户定义的处理函数关联到该探测点,当内核执行到该探测点时,相应的关联函数被执行,然后继续执行正常的代码路径。

kprobe允许用户编写内核模块添加调试信息到内核。当在远程机器上调试有bug的程序而日志/var/log/messages不能看出错误时,kprobe显得非常有用。用户可以编译一个内核模块,并将内核模块插入到调试的内核中,就可以输出所需要的调试信息了。

内核探测分为kprobe, jprobe和kretprobe(也称return probe,返回探测)三种。kprobe可插入内核中任何指令处;jprobe插入内核函数入口,方便于访问函数的参数;return probe用于探测指定函数的返回值。

内核模块的初始化函数init安装(或注册)了多个探测函数,内核模块的退出函数exit将注销它们。注册函数(如:register_kprobe())指定了探测器插入的地方、探测点触发的处理例程。

(1)配置支持kprobe的内核

配置内核时确信在.config文件中设置了CONFIG_KPROBES、CONFIG_MODULES、CONFIG_MODULE_UNLOAD、CONFIG_KALLSYMS_ALL和CONFIG_DEBUG_INFO。

配置了CONFIG_KALLSYMS_ALL,kprobe可用函数kallsyms_lookup_name从地址解析代码。配置了CONFIG_DEBUG_INFO后,可以用命令"objdump -d -l vmlinux"查看源到对象的代码映射。

调试文件系统debugfs含有kprobe的调试接口,可以查看注册的kprobe列表,还可以关闭/打开kprobe。

查看系统注册probe的方法列出如下:

#cat /debug/kprobes/list
c015d71a  k  vfs_read+0x0
c011a316  j  do_fork+0x0
c03dedc5  r  tcp_v4_rcv+0x0


第一列表示探测点插入的内核地址,第二列表示内核探测的类型,k表示kprobe,r表示kretprobe,j表示jprobe,第三列指定探测点的"符号+偏移"。如果被探测的函数属于一个模块,模块名也被指定。

打开和关闭kprobe的方法列出如下:

#echo ‘1’ /debug/kprobes/enabled
#echo ‘0’ /debug/kprobes/enabled

(2)kprobe样例

Linux内核源代码在目录samples/kpobges下提供了各种kprobe类型的探测处理例程编写样例,分别对应文件kprobe_example.c、jprobe_example.c和kretprobe_example.c,用户稍加修改就可以变成自己的内核探测模块。下面仅说明kprobe类型的探测例程。

样例kprobe_example是kprobe类型的探测例程内核模块,显示了在函数do_fork被调用时如何使用kprobe转储栈和选择的寄存器。当内核函数do_fork被调用创建一个新进程时,在控制台和/var/log/messages中将显示函数printk打印的跟踪数据。样例kprobe_example列出如下(在samples/kprobe_example.c中):

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
 
/* 对于每个探测,用户需要分配一个kprobe对象*/
static struct kprobe kp = {
    .symbol_name    = "do_fork",
};
 
/* 在被探测指令执行前,将调用预处理例程 pre_handler,用户需要定义该例程的操作*/
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_X86
    printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx,"
            " flags = 0x%lx\n",
        p->addr, regs->ip, regs->flags);  /*打印地址、指令和标识*/
#endif
#ifdef CONFIG_PPC
    printk(KERN_INFO "pre_handler: p->addr = 0x%p, nip = 0x%lx,"
            " msr = 0x%lx\n",
        p->addr, regs->nip, regs->msr);
#endif
 
    /* 在这里可以调用内核接口函数dump_stack打印出栈的内容*/
    return 0;
}
 
/* 在被探测指令执行后,kprobe调用后处理例程post_handler */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
                unsigned long flags)
{
#ifdef CONFIG_X86
    printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n",
        p->addr, regs->flags);
#endif
#ifdef CONFIG_PPC
    printk(KERN_INFO "post_handler: p->addr = 0x%p, msr = 0x%lx\n",
        p->addr, regs->msr);
#endif
}
 
/*在pre-handler或post-handler中的任何指令或者kprobe单步执行的被探测指令产生了例外时,会调用fault_handler*/
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
    printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
        p->addr, trapnr);
    /* 不处理错误时应该返回*/
    return 0;
}
 
/*初始化内核模块*/
static int __init kprobe_init(void)
{
    int ret;
    kp.pre_handler = handler_pre;
    kp.post_handler = handler_post;
    kp.fault_handler = handler_fault;
 
    ret = register_kprobe(&kp);  /*注册kprobe*/
    if (ret < 0) {
        printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
        return ret;
    }
    printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
    return 0;
}
 
static void __exit kprobe_exit(void)
{
    unregister_kprobe(&kp);
    printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
}
 
module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");


Systemtap调试
(1)Systemtap原理

Systemtap是一个基于kprobe调试内核的开源软件。调试者只需要写一些脚本,通过Systemtap提供的命令行接口对正在运行的内核进行诊断调试,不需要修改或插入调试代码、重新编译内核、安装内核和重启动等工作,使内核调试变得简单容易。Systemtap调试过程与在gdb调试器中用断点命令行调试类似。

Systemtap用类似于awk语言的脚本语言编写调试脚本,该脚本命名事件并给这些事件指定处理例程。只要指定的事件发生,Linux内核将运行对应的处理例程。

有几种类型的事件,如:进入或退出一个函数,一个定时器超时或整个systemtap会话开始或停止。处理例程是一系列脚本语言语句指定事件发生时所做的工作,包括从事件上下文提取数据,存储它们进入内部变量或打印结果。


Systemtap的运行过程如图所示,用户调试时用Systemtap编写调试脚本,Systemtap的翻译模块(translator)将脚本经语法分析(parse)、功能处理(elaborate)和翻译后生成C语言调试程序,然后,运行C编译器编译(build)创建调试内核模块。再接着将该内核模块装载入内核,通过kprobe机制,内核的hook激活所有的探测事件。当任何处理器上有这些事件发生时,对应的处理例程被触发工作,kprobe机制在内核获取的调试数据通过文件系统relayfs传回Systemtap,输出调试数据probe.out。在调试结束时,会话停止,内核断开hook连接,并卸载内核模块。整个操作过程由单个命令行程序strap驱动控制。


(2)stap程序

stap程序是Systemtap工具的前端,它接受用systemtap脚本语言编写的探测指令,翻译这些指令到C语言代码,编译C代码产生并装载内核模块到正运行的Linux内核,执行请求的跟踪或探测函数。用户可在一个命名文件中提供脚本或从命令行中提供调试语句。

命令stap的用法列出如下:

stap [ OPTIONS ] FILENAME [ ARGUMENTS ]

stap [ OPTIONS ] - [ ARGUMENTS ]

stap [ OPTIONS ] -e SCRIPT [ ARGUMENTS ]

stap [ OPTIONS ] -l PROBE [ ARGUMENTS ]

选项[ OPTIONS ]说明如下:

-h 显示帮助信息。

-V 显示版本信息。

-k 在所有操作完成后,保留临时目录。对于检查产生的C代码或重使用编译的内核对象来说,这是有用的。

-u 非优化编译模式。.

-w 关闭警告信息。

-W 将警告信息变成错误信息打印。

-b 让内核到用户数据传输使用bulk模式。使用RelayFS文档系统来将数据从内核空间传输到用户空间;


-t 收集时间信息:探测执行的次数、每个探测花费的平均时间量。

-sNUM 内核到用户数据传输使用NUM MB 的缓冲区。当多个处理器工作在bulk模式时,这是单个处理器的缓冲区大小。

-p NUM Systemtap在通过NUM个步骤后停止。步骤数为1-5: parse, elaborate, translate, compile, run。

-I DIR 添加tapset库(用于翻译C代码的函数集)搜索目录。

-D NAME=VALUE 添加C语言宏定义给内核模块Makefile,用于重写有限的参数。

-R DIR 在给定的目录查找Systemtap运行源代码。

-r RELEASE 为给定的内核发布版本RELEASE而不是当前运行内核编译内核模块。

-m MODULE 给编译产生的内核模块命名MODULE,替代缺省下的随机命名。产生的内核模块被拷贝到当前目录。

-o FILE 发送标准输出到命名文件FILE。在bulk模式,每个CPU的文件名将用"FILE_CPU序号"表示。

-c CMD 开始探测,运行CMD,当CMD完成时退出。

-x PID 设置target()

-v 打印中间信息

-g 采用guru模式,允许脚本中嵌入C语句


Systemtap使用

探针

SystemTap 脚本由探针和在触发探针时需要执行的代码块组成。探针有许多预定义模式,表 1列出了其中的一部分。这个表列举了几种探针类型,包括调用内核函数和从内核函数返回。


1.探针模式例子

探针类型

说明

begin

在脚本开始时触发

end

在脚本结束时触发

kernel.function("sys_sync")

调用sys_sync时触发

kernel.function("sys_sync").call

同上

kernel.function("sys_sync").return

返回sys_sync时触发

kernel.syscall.*

进行任何系统调用时触发

kernel.function("*@kernel/fork.c:934")

到达 fork.c 的第 934行时触发

module("ext3").function("ext3_file_write")

调用 ext3 write函数时触发

timer.jiffies(1000)

每隔 1000 个内核 jiffy触发一次

timer.ms(200).randomize(50)

每隔 200 毫秒触发一次,带有线性分布的随机附加时间(-50 +50

我们通过一个简单的例子来理解如何构造探针,并将代码与该探针相关联。清单 3显示了一个样例探针,它在调用内核系统调用sys_sync时触发。当该探针触发时,您希望计算调用的次数,并发送这个计数以及表示调用进程 IDPID)的信息。首先,声明一个任何探针都可以使用的全局值(全局名称空间对所有探针都是通用的),然后将它初始化为 0。其次,定义您的探针,它是一个探测内核函数sys_sync的条目。与探针相关联的脚本将递增count变量,然后发出一条消息,该消息定义调用的次数和当前调用的 PID。注意,这个例子与C语言中的探针非常相似(探针定义语法除外),如果具有C语言背景将非常有帮助。


清单 3.一个简单的探针和脚本

                                  

global count=0

 

probe kernel.function("sys_sync") {

  count++

  printf( "sys_sync called %d times, currently by pid %d/n", count, pid );

}

 

您还可以声明探针可以调用的函数,尤其是希望供多个探针调用的通用函数。这个工具还支持递归到给定深度。

变量和类型

SystemTap 允许定义多种类型的变量,但类型是从上下文推断得出的,因此不需要使用类型声明。在 SystemTap中,您可以找到数字(64位签名的整数)、整数(64 位)、字符串和字面量(字符串或整数)。您还可以使用关联数组和统计数据(我们稍后讨论)。

表达式

SystemTap 提供C语言中常用的所有必要操作符,并且用法也是一样的。您还可以找到算术操作符、二进制操作符、赋值操作符和指针废弃。您还看到从C语言带来的简化,其中包括字符串连接、关联数组元素和合并操作符。

语言元素

在探针内部,SystemTap 提供一组类似于C一样易于使用的语句。注意,尽管该语言允许您开发复杂的脚本,但每个探针只能执行 1000条语句(这个数量是可配置的)。表 2列出了一小部分语句作为例子。注意,在这里的许多元素和C中的一样,尽管有一些附加的东西是特定于 SystemTap的。


2. SystemTap的语言元素

语句

说明

if (exp) {} else {}

标准的if-then-else语句

for (exp1 ; exp2 ; exp3 ) {}

一个for循环

while (exp) {}

标准的while循环

do {} while (exp)

一个do-while循环

break

退出迭代

continue

继续迭代

next

从探针返回

return

从函数返回一个表达式

foreach (VAR in ARRAY) {}

迭代一个数组,将当前的键分配给VAR

本文在样例脚本中探索了统计数据和聚合功能,因为这是C语言中不存在的。

最后,SystemTap 提供许多内部函数,这些函数提供关于当前上下文的额外信息。例如,您可以使用caller()识别当前的调用函数,使用cpu()识别当前的处理器号码,以及使用pid()返回 PIDSystemTap 还提供许多其他函数,提供对调用堆栈和当前注册表的访问。

SystemTap 例子

在简单介绍了 SystemTap 的要点之后,我们接下来通过一些简单的例子来了解 SystemTap的工作原理。本文还展示了该脚本语言的一些有趣方面,比如聚合。

系统调用监控

前一个小节探索了一个监控sync系统调用的简单脚本。现在,我们查看一个更加具有代表性的脚本,它可以监控所有系统调用并收集与它们相关的额外信息。

清单 4 显示的简单脚本包含一个全局变量定义和 3个独立的探针。在首次加载脚本时调用第一个探针(begin探针)。在这个探针中,您可以发出一条表示脚本在内核中运行的文本消息。接下来是一个syscall探针。注意这里使用的通配符 (*),它告诉 SystemTap监控所有匹配的系统调用。当该探针触发时,将为特定的 PID和进程名增加一个关联数组元素。最后一个探针是 timer探针。这个探针在 10,000毫秒(10 秒)之后触发。与这个探针相关联的脚本将发送收集到的数据(遍历每个关联数组成员)。当遍历了所有成员之后,将调用exit调用,这导致卸载模块和退出所有相关的 SystemTap 进程。


清单 4.监控所有系统调用 (profile.stp)

                                  

global syscalllist

 

probe begin {

  printf("System Call Monitoring Started (10 seconds).../n")

}

 

probe syscall.*

{

  syscalllist[pid(), execname()]++

}

 

probe timer.ms(10000) {

  foreach ( [pid, procname] in syscalllist ) {

    printf("%s[%d] = %d/n", procname, pid, syscalllist[pid, procname] )

  }

  exit()

}

 

清单 4 中的脚本的输出如清单 5所示。从这个脚本中您可以看到运行在用户空间中的每个进程,以及在 10秒钟内发出的系统调用的数量。


清单 5. profile.stp脚本的输出

                                  

$ sudo stap profile.stp

System Call Monitoring Started (10 seconds)...

stapio[16208] = 104

gnome-terminal[6416] = 196

Xorg[5525] = 90

vmware-guestd[5307] = 764

hald-addon-stor[4969] = 30

hald-addon-stor[4988] = 15

update-notifier[6204] = 10

munin-node[5925] = 5

gnome-panel[6190] = 33

ntpd[5830] = 20

pulseaudio[6152] = 25

miniserv.pl[5859] = 10

syslogd[4513] = 5

gnome-power-man[6215] = 4

gconfd-2[6157] = 5

hald[4877] = 3

$

 

特定的进程的系统调用监控

在这个例子中,您稍微修改了上一个脚本,让它收集一个进程的系统调用数据。此外,除了仅捕捉计数之外,还捕捉针对目标进程的特定系统调用。清单 6显示了该脚本。

这个例子根据特定的进程进行了测试(在本例中为syslog守护进程),然后更改关联数组以将系统调用名映射到计数数据。


清单 6.新系统调用监控脚本 (syslog_profile.stp)

                                  

global syscalllist

 

probe begin {

  printf("Syslog Monitoring Started (10 seconds).../n")

}

 

probe syscall.*

{

  if (execname() == "syslogd") {

    syscalllist[name]++

  }

}

 

probe timer.ms(10000) {

  foreach ( name in syscalllist ) {

    printf("%s = %d/n", name, syscalllist[name] )

  }

  exit()

}

 

清单 7 提供了该脚本的输出。


清单 7.新脚本的 SystemTap 输出 (syslog_profile.stp)

                                  

$ sudo stap syslog_profile.stp

Syslog Monitoring Started (10 seconds)...

writev = 3

rt_sigprocmask = 1

select = 1

$

 

使用聚合步骤数字数据

聚合实例时捕捉数字值的统计数据的出色方法。当您捕捉大量数据时,这个方法非常高效有用。在这个例子中,您收集关于网络包接收和发送的数据。清单 8定义两个新的探针来捕捉网络 I/O。每个探针捕捉特定网络设备名、PID和进程名的包长度。在用户按 Ctrl-C调用的 end 探针提供发送捕获的数据的方式。在本例中,您将遍历recv聚合的内容、为每个元组(设备名、PID和进程名)相加包的长度,然后发出该数据。注意,这里使用提取器来相加元组:@count提取器获取捕获到的长度(包计数)。您还可以使用@sum提取器来执行相加操作,分别使用@min@max来收集最短或最长的程度,以及使用@avg来计算平均值。


清单 8.收集网络包长度数据 (net.stp)

                                  

global recv, xmit

 

probe begin {

  printf("Starting network capture (Ctl-C to end)/n")

}

 

probe netdev.receive {

  recv[dev_name, pid(), execname()] <<< length

}

 

probe netdev.transmit {

  xmit[dev_name, pid(), execname()] <<< length

}

 

probe end {

  printf("/nEnd Capture/n/n")

 

  printf("Iface Process........ PID.. RcvPktCnt XmtPktCnt/n")

 

  foreach ([dev, pid, name] in recv) {

    recvcount = @count(recv[dev, pid, name])

    xmitcount = @count(xmit[dev, pid, name])

    printf( "%5s %-15s %-5d %9d %9d/n", dev, name, pid, recvcount, xmitcount )

  }

 

  delete recv

  delete xmit

}

 

清单 9 提供了清单 8中的脚本的输出。注意,当用户按 Ctrl-C时退出脚本,然后发送捕获的数据。


清单 9. net.stp的输出

                                  

$ sudo stap net.stp

Starting network capture (Ctl-C to end)

^C

End Capture

 

Iface Process........ PID.. RcvPktCnt XmtPktCnt

 eth0 swapper         0           122        85

 eth0 metacity        6171          4         2

 eth0 gconfd-2        6157          5         1

 eth0 firefox         21424        48        98

 eth0 Xorg            5525         36        21

 eth0 bash            22860         1         0

 eth0 vmware-guestd   5307          1         1

 eth0 gnome-screensav 6244          6         3

Pass 5: run completed in 0usr/50sys/37694real ms.

$

 

捕获柱状图数据

最后一个例子展示 SystemTap 用其他形式呈现数据有多么简单 —— 在本例中以柱状图的形式显示数据。返回到是一个例子中,将数据捕获到一个名为histogram 的聚合中(见清单 10)。然后,使用netdev接收和发送探针以捕捉包长度数据。当探针结束时,您将使用@hist_log提取器以柱状图的形式呈现数据。


清单 10.步骤和呈现柱状图数据 (nethist.stp)

                                  

global histogram

 

probe begin {

  printf("Capturing.../n")

}

 

probe netdev.receive {

  histogram <<< length

}

 

probe netdev.transmit {

  histogram <<< length

}

 

probe end {

  printf( "/n" )

  print( @hist_log(histogram) )

}

 

清单 11 显示了清单 10的脚本的输出。在这个例子中,使用了一个浏览器会话、一个 FTP会话和ping来生成网络流量。@hist_log提取器是一个以 2为底数的对数柱状图(如下所示)。还可以步骤其他柱状图,从而使您能够定义 bucket的大小。


清单 11. nethist.stp的柱状图输出

                                  

$ sudo stap nethist.stp

Capturing...

^C

value |-------------------------------------------------- count

    8 |                                                      0

   16 |                                                      0

   32 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@            1601

   64 |@                                                    52

  128 |@                                                    46

  256 |@@@@                                                164

  512 |@@@                                                 140

 1024 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@  2033

 2048 |                                                      0

 4096 |                                                      0

 

$

 



Logo

更多推荐