现在的位置: 首页 > 综合 > 正文

《System语言详解》——4. 探测点

2013年08月03日 ⁄ 综合 ⁄ 共 9650字 ⁄ 字号 评论关闭



 

英文原文:http://sourceware.org/systemtap/langref/Probe_points.html


译者:林永听

注:本系列文章为作者连载作品,请勿转载,否则视为侵权。


 

4

探测点



4.1

探测点的一般语法形式

探测点采用点分格式的语法,事件命名空间划分成多个部分,类似于域名系统。每部分可以由字符串或数字等字面值来参数化,与函数调用的语法格式十分相似。

下面是符合语法规则的探测点:

kernel.function("foo")

kernel.function("foo").return

module{"ext3"}.function("ext3_*")

kernel.function("no_such_function") ?

syscall.*

end

timer.ms(5000)

在某种程度上,探测点可分成同步
异步
两大类。当CPU
执行到探针指定的指令时,产生一个同步事件,探针可利用引用点(指令地址)来获取更多的上下文数据。另一探测点家族与异步事件相关,如定时器。与同步探测点不同的是,它没有固定的引用点。每个探测点可以指定匹配多个位置,例如使用通配符或多个探针别名,多个位置均被探测。在探针声明中,使用逗句作为分隔符来指定多个探测位置。



4.1.1



前缀

探测点的前缀阐明了探测目标,如kernel

module

timer

,等等。



4.1.2



后缀

后缀进一步细化了探测点,例如.return

探测函数的退出点。缺少后缀时意味着探测函数的进入点。



4.1.3



文件名和函数名的通配符

探测点各部分若包含星号字符(*)
,则扩展为与之匹配的探测点。请看下述例子:

kernel.syscall.*

kernel.function("sys_*)




4.1.4



可选探测点

如果探测点后面跟随一个问号(?)
字符,表明这是可选探测点,即使它扩展失败,也不会导致错误。从顶层开始到底层,各层的别名和通配符扩展同样遵循此法则。

可选探测点的语法形式如下:

kernel.function("no_such_function") ?




4.2

内置探测点分类(DWARF
探针)

探测点家族使用目标内核或模块的符号调试信息(symbolic debugging information)
,这些信息可包含在未经stripped
的可执行文件,或在一个独立的debuginfo

软件包中。通过指源或目标代码点的集合,探针可在逻辑上定位到目标执行路径。当任一处理器执行与之匹配的语句,探针处理函数将在些上下文中运行。

内核点(points in a kernel)
可由模块,源文件,行号,函数名或者它们的组合来定位。

下述是目前支持的探测点清单:

kernel.function(PATTERN)

kernel.function(PATTERN).call

kernel.function(PATTERN).return

kernel.function(PATTERN).return.maxactive(VALUE)

kernel.function(PATTERN).inline

kernel.function(PATTERN).label(LPATTERN)

module(MPATTERN).function(PATTERN)

module(MPATTERN).function(PATTERN).call

module(MPATTERN).function(PATTERN).return.maxactive(VALUE)

module(MPATTERN).function(PATTERN).inline

kernel.statement(PATTERN)

kernel.statement(ADDRESS).absolute

module(MPATTERN).statement(PATTERN)

.function

变体使探针定位在命名函数的开始之处,因此探针可用上下文变量的方式来获取函数参数。

.return

变体让探针定位到命名函数返回的那一时刻,因此,探针可能过上下文变量$return
来获取函数的返回值。探针仍然可以获得函数的参数,但此时它们的值可能在函数执行过程中发生了变化。可以使用.maxactive

进一步修饰return
探针,它指定该函数有多少个实例可以同时被探测。大多数情况下,不需要指定.maxactive

,
默认情况已足够使用了。然而,如果被忽略的探针过多,可以尝试将.maxactive

调高,再看看被忽略的探针是否减少。

.inline

修饰符使.function
变体过滤出那些仅为内联函数的实例,而.call

修饰符刚好选择相反的子集。内联函数没有唯一的返回点,因此.inline

探针不支持.return

后缀。

.statement

变体使探针探测到确切的代码行,函数内的局部变量对探针来说是可见的。

在上述探针描述中,MPATTERN
是一个字符串字面值,它标识加载的内核模块;LPATTERN
代表源程序标签。MPATTERN
LPATTERN
两者均可包含星号(*)
,方括号“[]
”和问号(?)
等通配符。

PATTERN
是一个字符串字面值,它标识程序中的代码点。它由3
部分构成。

  1. 第一部分是函数名字,该名字与nm
    工具的输出一致。此部分可使用星号和问号通配符来匹配多个函数名字。
  2. 第二部分是可选的,它以@
    字符开头,紧跟着此函数所在源文件的路径。此路径可以包含通配符模式,如mm/slab*
    。大多数情况下,路径名应为从Linux
    代码树顶层目录开始的相对路径,尽管某些内核要求使用绝对路径。如果相对路径不能工作,尝试使用绝对路径。
  3. 如果给定文件名,第三部仍是可选的。它以“:
    ”或“+
    ”开头,用来标识源文件的行号。”:”
    后面跟的是绝对行号,而”+”
    后面跟的是函数入口的相对行号。”:*”
    匹配函数的每一行,而”:x-y”
    可以从x
    行匹配到y
    行。

另外,PATTERN
指定为数字常量时,它表示模块的相对地址或内核的绝对地址。

部分在编译单元内可见的源代码级别变量,诸如函数参数,局部或全局变量,在探针处理函数内同样是可见的。在脚本里使用美元符号($)
加上它们的名字就可以引用这些变量。此外,特殊的语法可防止无节制地遍历结构体,指针和数组。

$var
引用可见(in-scope
)变量var
。如果它的类型是整数类型(译者注:即char, short, int, long
这些类型),脚本会把它转换为64
位的整数。如果指针的类型是字符串(char *)
,脚本会使用kernel_string()
user_string()
函数将它拷贝到SystemTap
的字符串变量。

$var->field
遍历结构体的field
成员。可重复使用->
操作符沿着子指针链访问各级成员。

$var[N]
访问数组的元素,下标由N
指定。下标只能是字面值整数。

$$vars
扩展为字符串并等价于sprintf("parm1=%x ...
parmN=%x var1=%x ... varN=%x", $parm1, ..., $parmN, $var1, ..., $varN)

$$locals
扩展为字符串并等价于sprintf("var1=%x ...
varN=%x", $var1, ..., $varN)


$$parms
扩展为字符串并等价于sprintf("parm1=%x ...
parmN=%x", $parm1, ..., $parmN)





4.2.1

kernel.function, module().function

.function

变体将探针定位到命名函数开始之处,因此function
探针可用方问上下文变量的方式来访问函数的参数。

一般语法形式:

kernel.function("func[@file]"

module("modname").function("func[@file]"

例子:

#
引用内核所有名字具有init
exit
字符串的函数。

kernel.function("*init*"),
kernel.function("*exit*")

 

#
引用文件kernel/sched.c
内跨越第240
行的函数。

kernel.function("*@kernel/sched.c:240")

 

#
引用模块ext3
内的所有函数

module("ext3").function("*")




4.2.2

kernel.statement, module().statement

.statement

变体允许探针定位到确切的代码行,此代码行可见的变量均可被脚本访问。

一般语法形式如下:

kernel.statement("func@file:linenumber")

module("modname").statement("func@file:linenumber")

例子:

#
引用文件kernel/sched.c
内第2917
行这一语句:

kernel.statement("*@kernel/sched.c:2917")

#
引用文件fs/bio.c
bio_init+3
这一行语句:

kernel.statement("bio_init@fs/bio.c+3")



4.3 DWARF-less probing

当目标内核或模块缺少调试信息时,你仍然可以使用kprobe

家族探针来探测它们函数的进入点和退出点。但使用此种探针时你不能获取函数参数或局部变量的值。然而当你使用这方法时,systemTap
仍然为你提供了一种访问参数的方法:

当函数因被探测而停滞在它的进入点时,可以使用编号来引用它的参数。例如,假设被探测函数声明如下:

asmlinkage ssize_t sys_read(unsigned int fd, char __user
* buf, size_t

count)

可以分别使用uint_arg(1)
pointer_arg(2)
ulong_arg(3)
来获得fd, buf
count
的值。此时,探针处理函数必须先调用asmlinkage()
,因为在某些架构里,asklinkage
属性影响函数参数的传递方式。

译者注:例子中的sys_read
函数在定义时使用了asmlinkage
属性,在不同的CPU
架构上有不同的参数传递方式,例如使用寄存器和堆栈一起传递参数。在我们熟悉的x86CPU
上,asklinkage
修饰符的要义是通过堆栈来传递函数参数。因此在systemTap
脚本里,需要调用asklinkage()
函数来根据CPU
架构来初始化一系列数据,好让后面的type_arg(N)
调用知道在寄存器还是堆栈里获得参数的值。

当函数因被探测而停滞在它的退出点时,此种非DWARF
探针不支持$return
目标变量。但可以通过调用returnval()
来获得寄存器的值,函数的返回值通常是保持在这一寄存器里的,也可调用returnstr()
来获得返回值的字符串形式。

在处理函数代码里面,可调用register("regname")
来获得它被调用时特定CPU
寄存器的值。u_register("regname")
类似于register("regname")
,不同的是它将寄存器的值解释成无符号整数。

SystemTap
支持下述的kprobe
结构:

kprobe.function(FUNCTION)

kprobe.function(FUNCTION).return

kprobe.module(NAME).function(FUNCTION)

kprobe.module(NAME).function(FUNCTION).return

kprobe.statement(ADDRESS).absolute

.function

探针探测内核函数,而.module

探针探测特定模块的函数。如果知道内核或模块函数的绝对地址,可使用.statement

探针。注意,FUNCTION
MODULE
名字中不能出现通配符,通配符引致探针不能注册。同时statement
探针只能运行在guru
模式下。




4.4

用户空间探测技术

要支持用户空间探测,只需将kernel
配置成包括utrace
扩展即可。本文撰写之时,Red Hat
CentOS
发行版的内核已支持utrace
了。关于utrace
更多的信息,请参阅http://people.redhat.com/roland/utrace/


用户空间探测有几种形式。无调试符号的探测点,如process(PID).statement(ADDRESS).absolute
类似于kernel.statement(ADDRESS).absolute
,它们都使用原始的(raw)
、未经验证的虚拟地址,并且不能使用$variable
目标变量。目标PID
参数必须是正在运行的进程,ADRESS
必须是一个有效的指令地址。进程里的所有线程均被探测。此探针只能运行guru
模式下。

你可探测无调试符号的用户-
内核接口事件,这些事件由utrace
进行处理。可以通过下述的方式来探测:

process(PID).begin

process("PATH").begin

process.begin

process(PID).thread.begin

process("PATH").thread.begin

process.thread.begin

process(PID).end

process("PATH").end

process.end

process(PID).thread.end

process("PATH").thread.end

process.thread.end

process(PID).syscall

process("PATH").syscall

process.syscall

process(PID).syscall.return

process("PATH").syscall.return

process.syscall.return

process(PID).insn

process("PATH").insn.block

process(PID).insn.block

process("PATH").insn

process("PATH").mark("LABEL")

process("PATH").function("NAME")

process("PATH").statement("*@FILE.c:123")

process("PATH").function("*").return

process("PATH").function("myfun").label("foo")

PID
PATH
描述的进程被创建时,.begin
变体探针会被调用。如果不指定PID
PATH
(如process.begin
),任何新进程的繁衍都会调用该探针。

PID
PATH
描述的线程被创建时,.thread.begin
变体探针会被调用。

PID
PATH
描述的进程结束时,.end
变体探针会被调用。

PID
PATH
描述的线程结束时,.thread.end
变体探针会被调用。

PID
PATH
描述的线程进行系统调用时,.syscall
变体探针会被调用。系统调用编号可在$syscall
上下文变量中获得。系统调用的前6
个参数可从$argN
目标变量中获取,即$arg1, $arg2
等。

PID
PATH
描述的线程从系统调用中返回时,.syscall.return
变体探针被调用。系统调用编号同样可以$syscall
上下文变量中获得,而系统调用的返回值可在$return
上下文变量中获得。

.mark
变体探针由应用程序定义的静态探针来调用,更多信息请参阅4.4.1
节。

除此之外,用户空间程序和共享库支持带完整调试符号的源代码级别的探针。它们十分类似于上述基于DWARF
带调试符号的内核或模块探针,并且访问上下文$
变量的方式也很相似。

process("PATH").function("NAME")

process("PATH").statement("*@FILE.c:123")

process("PATH").function("*").return

process("PATH").function("myfun").label("foo")

对于所有进程探针,PATH
名字引用可执行文件,执行文件的搜索方式和shell
的完全一致:要么明确指定该可以执行文件的路径,要么指定从当前工作目录开始的相对路径,好PATH
参数以./
字符串开始。否则从$PATH
环境变量指的目录中搜索。下述是探针语法的例子:

probe process("ls").syscall {}

probe process("./a.out").syscall {}

等价于下述的探针:

probe process("/bin/ls").syscall {}

probe process("/my/directory/a.out").syscall {}

如果进程探针没有指定PID
PATH
参数,那么所有用户空间线程将被探测。然而,如果systemTap
以目标进程模式(target process mode)
运行(invoked)
,进程探针仅限于探测目标进程家族树里的那些进程。

目标进程模式(使用-c CMD
-x PID
选项运行stap
)蕴含所有的process.*
探针只能局限于探测给定的进程和它的子进程,但不影响kernel.*
和其它的探针类型。通常而言,CMD
字符串是运行程序的名称,而不是”/bin/sh –c”
shell
程序的名字,因为utrace
uprobe
探针会(从内核)接收到相当“干净”的事件流。如果CMD
中出现元字符,如重定向操作符,要求使用”/bin/sh –c CMD”
形式的名称,届时utrace
uprobe
探针将从shell
中接收事件。请看下述例子:

% stap -e 'probe process.syscall, process.end {

          

printf("%s %d %s/n", execname(), pid(), pp())}' /

      
-c ls

下述是这个命令的一种输出:

ls 2323 process.syscall

ls 2323 process.syscall

ls 2323 process.end

如果PATH
名字为共享库,
那么所有映射该共亨库的进程均被探测。若安装了带dwarf
调试信息的版本,尝试下述语法命令:

probe process("/lib64/libc-2.8.so").function("....")
{ ... }

 

此命令探测所有调用进共享库里面的线程,键入”stap –c CMD”
”stap –x PID”
将之限制到仅探测某一命令和它的子孙进程。这里同样可使用$$var
和其它变量。可以使用-d DIRECTORY
选项告知stap
命令带调试信息文件的位置。

Process().insn
process().insn.block
探针依次检查进程每个执行的指令或区块。但此探针仅在部分平台上实现,因此如果你所使用的系统没有实现该探针,那么在启动脚本时会收到错误信息。

PID
PATH
描述的进程每执行一个单步指令,.insn
探针都会被调用。

PID
PATH
描述的进程每执行一个区块指令,.insn.block
探针都会被调用。

若想统计进程执行的指令总数,可以使用类似下述的命令:

$ stap -e 'global steps; probe
process("/bin/ls").insn {steps++}

          
probe
end {printf("Total instructions: %d/n", steps);}' /

      
-c /bin/ls

但使用此特性会使进程执行速度放慢很多。



4.4.1

静态用户空间探测技术

你可以探测程序的静态符号测量仪(instrumentation)
,只需将此测量信编译进编程或共享库,使用下述语法即可:

 

process("PATH").mark("LABEL")

.mark
变体由静态探针调用,而该静态探针是由应用程序使用STAP_PROBE1(handle,LABEL,arg1)
预先定义的。STAP_PROBE1
定义在sdt.h
文件中,参数如下:

 

参数

描述

 

handle

应用程序句柄(handle)

 

LABEL

对应.mark
探针的参数

 

arg1

参数(译者注:传递给探针的参数)

 

 

STAP_PROBE1
为探针提供1
个参数,STAP_PROBE2
可提供2
个,依次类推。探针可通过上下文变量$arg1, $arg2
等来获取参数。

此外,可以利用dtrace
脚本定制新的STAP_PROBE
宏。Sdt.h
文件使用DTRACE_PROBE
提供了兼容dtrace
marker
和与之对应的python
脚本。你可直接使用这些基于dtrace
的内置的宏,只需将dtrace –h
-G
功能打开即可。

下述是一个用户空间支持符号探测的原型例子:

# stap -e 'probe
process("ls").function("*").call {

          
log
(probefunc()." ".$$parms)

          
}' /

      
-c 'ls -l'

此脚本需要命令程序带有调试信息并且内核支持utrace
才能运行。如果看见“pass 4a-time
”这样的构建失败信息,请确保你的内核支持utrace



4.5 PROCFS

探针

此类探测点允许探测procfs
伪文件系统中/proc/systemtap/MODNAME
目录下文件的创建,读和写,其中NODNAME
sytemTap
模块的名字。转换器目前支持4
种探测点变种:

procfs("PATH").read

procfs("PATH").write

procfs.read

procfs.write

PATH
是被探测的文件,它是以/proc/systemtap/MODNAME
为起始目录的相对路径。如果没有指定PATH
参数(上述清单中的最后两个变种),PATH
的值默认为”command”

当用户程序读取/proc/systemtap/MODNAME/PATH
文件时,相应的procfs
读探针将被激活(triggered)
。从文件中已读取的数据串被分配到$value
变量,如下所示:

procfs("PATH").read { $value =
"100/n" }

当用户程序写数据到/proc/systemtap/MONNAME/PATH
文件时,相应的procfs
写探针将被激法。即将要写到文件的数据被分配到$value
变量,如下所示:

procfs("PATH").write { printf("User wrote:
%s", $value) }



4.6 Marker

探针

Marker
探针家族关联被编译进内核或模块的静态marker
探针。这些marker
是内核里特殊的宏,与基于DWARF
的探针相比,它使用探测更快,更可靠。Marker
探针不需要利用DWARF
调试信息。

Marker
探测点名字以kernel
前缀开头,即标识用于查找marker
的模号表的源头,后缀是它自身即marker.(“MARK”)
MARK
可以包含通配符,它匹配那些被编译进内核或模块的marker
宏的名字。可选地,你可以使用format(
FORMAT
)
来指定marker
格式字符串,以区别两个有同样名字但格式字符串不相同的marker

Marker
探针处理函数可读取可选参数$arg1
$argN
,这些参数由宏(STAP_MACRO)
的调用方指定,其中NN
为宏提供的参数个数。参数个数和参数串均使用类型安全的方式进行传递。

Marker
探针中的marker
格式字符可以通过$format
变量获取,同样地,marker
的名字字符串可通过$name
变量获取。

下述是marker
探针的结构:

kernel.mark("MARK")

kernel.mark("MARK").format("FORMAT")

抱歉!评论已关闭.