systemtap工具的安装
准备工作
uname -a
查看当前内核版本是哪一个,然后使用
yum install kernel-devel
安装kernel debuginfo包
rpm -qi kernel-devel
找到内核构建的详细信息,然后去对应发布网站上找kernel-debuginfo和kernel-debuginfo-common包。
安装systemtap包
yum install systemtap
测试
完成安装后可以通过下面命令测试systemtap
stap -ve ‘probe begin { print(“hello world\n”) exit()}’
进行测试,看看systemtap有无安装成功。
systemtap常规命令
查看内核某个函数可以查看的target变量
下面命令演示查看__lookup_hash()函数返回时刻可以查看到的变量
stap -ve ‘probe kernel.function(“__lookup_hash”).return’ //查看__lookup_hash()函数返回时刻可以systemtap工具可以查看的target变量。
kernel.function(“__lookup_hash@fs/namei.c:1383”).return $return:struct dentry* $name:struct qstr* $base:struct dentry* $flags:unsigned int $need_lookup:bool
在上表中显示了lookup_hash在文件中的行号,显示了名为$return 的变量,其实这个return变量就是systemtap表示函数返回值的。而$name,$base,$flag我们对着linux源码看发现这是__lookup_hash的三个入参。
下面命令可以查看__lookup_hash函数入口可以查看的变量
stap -L ‘kernel.function(__lookup_hash)’
也可以通过statement方式查看内核符号表里有的__lookup_hash相关的行
stap -L ‘kernel.statement(“*”)’ |grep __lookup_hash
如果查找的内核函数位于某个模块里可以使用下面命令:
stap -L ‘module(“overlay”).function(“*”)’|grep ovl_lookup
stap -L ‘module(“overlay”).statement(“*”)’|grep ovl_lookup
查看用户态进程的可以stap的函数
通过下面命令可以查看到某个正在运行的进程的函数
stap -L ‘process(“/usr/bin/dockerd”).function(“*”)’|grep -i “syscall.Mount”
process(“/usr/bin/dockerd”).function(“syscall.Mount@/usr/local/go/syscall/syscall_linux.go:796”) $datap:uint8*
$source:struct string $target:struct string flags:uintptr err:error
上例中看到找到了syscall.Mount函数,并且把它的所有参数和参数类型都打印了出来。
后面可以在stap脚本中,这个函数的上下文里直接使用这些参数,例如通过$source可以访问到参数source
使用systemtap打印目标函数的变量
systemtap支持print()和printf()函数,其中printf使用语法和c语言一致。支持%s,%d,%x格式
常用变量
内部变量名 | 功能 | 其他 |
---|---|---|
$$locals | 探测点上所有的本地变量(含参数) | n/a |
$$parms | 探测点上函数的参数 | n/a |
$$returns | 探测点上所有的返回值 | 只对return probe生效 |
变量的显示方式
在systemtap里凡是以$开头的变量都是目标变量,如果目标变量结构体指针或者结构体对象,那么可以使用->直接访问其成员。例如上例中:
$return->d_inode //就是__lookup_hash()返回值dentry* 的d_inode成员的值。
常规情况下,printf()打印target变量时刻,只打印其值。如果需要将其成员(指针类型的target需要将其指向的对象的成员展开)可以在target变量后面加$的方式例如:
$return$ //显示返回值指向的dentry所有成员。
一般情况下对struct的展开只会到成员值一级,如果相对成员内部继续展开可以在目标变量后面跟$$
if逻辑语句
在systemtap中支持逻辑if语句格式为:
if (expr) {
语句
}
逻辑语句支持以下比较
==,!=,>=,>,<,<=
systemtap的内置函数
用户空间下取得目标内的值
函数 | 含义 | 备注 |
---|---|---|
User_char(address) | 取得地址上的字符串值 | Na |
User_short(address) | 取得short值 | na |
user_init(address) | 取得int值 | na |
User_long(address) | 取得long | na |
user_string(address) | 取得string | na |
user_string_n(address) | 取得string,最长n个byte | na |
用户空间下堆栈打印
stap -d /bin/ls --ldd -e ‘probe process(“ls”).function(“xmalloc”) {print_usyms(ubackstrace())}’ -c “ls -l”
上述例子对ls -l下的xmalloc进行堆栈回溯:
-d 可执行文件名
--ldd 指明共享库
-c “ls -l” 执行的子进程体
例子
内核态例子
例子1
下面例子将打印__lookup_hash中return返回dentry*里inode指向的i_ino子成员
stap -ve ‘probe kernel.function(“__lookup_hash”).return { if ($return!=0) { if($return!=-2) { if ($return->d_inode!=0){
printf(“return dentry:%x,dentry->d_inode->i_ino:%x”,$return,$return->d_inode->i_ino)}}}}’ -o zxy.txt
这一例子中-o zxy.txt的意思就是将结果写入文件zxy.txt中(默认输出到控制台)
例子2
下面例子将在内核中使用强制类型转换
stap -ve ‘probe module(“overlay”).statement(“ovl_lookup@fs/overlay/super.c:603”){ print_syms(backtrace()) if($dentry->d_inode)’ printf(“overlay_lookup 603: inode :%p,ino:%d \n”,@cast(@cast($dentry,”dentry”,”kernel<linux/dcache.h>”)->d_inode,”inode”,”kernel<linux/fs.h>”)->i_ino)
这里解释一下,内核中方法强制转换
@cast(p,”type_name”[,”module”])->member
例子三 嵌入c语言
在内核中使用systemtap时,可以嵌入c语言。c语言可以定义函数可以使用在该打点行上内核可以调用的函数,定义结构和引用头文件。
function get_mmCount:long()
%{
struct task_struct *t=current;
if(t!=NULl && t->mm != NULL;
STAP_RETVALUE =(long)atomic_read(&t->mm->mm_user);
else
STAP_RETVALUE = 0xFFFF;
}%
probe kernel.statement(“get_task_mm@kernel/fork.c:782)
{
printf(“get_task_mm call pid %d,ref count %d \n”,pid(),get_mmCount());
}
上述例子中在get_mmCount这个自定义函数中引用了内核fork.c 782行上可以使用的atomic_read函数和current这个全局变量
内核态小技巧
在用systemtap跟踪内核时使用堆栈打印命令,常常打印不出来另外模块的函数,这是因为这些模块没有被加载。可以在systemtap启动命令使用--all-modules 方法强制将所有模块符号加载起来。
用户态例子
下面例子对用golang写的dockerd进程syscall.Mount调用入口时刻打印syscall.Mount()函数的参数
source的string字段内容
probe process(“/usr/bin/dockerd”).function(“syscall.Mount”).call{
if($source->str)
printf(“source is %s Len %d \n”,user_string($source->str),$source->len)
print_usyms(ubacktrace())
}
下面例子打印golang写的dockerd进程xxx.Get函数返回时刻的参数情况
probe process(“/usr/bin/dockerd”).function(“github.com/docker/docker/daemon/graphdriver/overlay2.(*Driver).Get”).return{
printf(“return Parma :%s\n”,$$parms$)
}
使用systemtap抓golang
systemtap对golang支持不够完美,用户需要自己解析基本结构例如golang的string,array和slice这些都需要用户自己解析。string被systemtap识别为struct string,此结构systemtap可以识别的定义可以简化为:
Struct string{
int len
char* str
}
需要注意的是通过systemtap打印golang string的string->str会多打很多字符,因为string成员str并非按照c语言定义的字符串以\0表示字符串结束,我们只能结合string的字段len来获取精确的字符串内容
slice完全不被systemtap识别,我们可以将systemtap可以识别的slice简化为此种定义:
struct slice{
array
len
cap
}
其中array就是指向slice存储单元的首地址。
要是我们想获取helo=[]string{“hello”,”world”}这样的字符串slice的内容可以通过systemtap提供的@cast(addr,”type”,”file”)函数将某个地址强转为file中定义的type结构。具体来说可以如下做获取hello的内容
for(i=0;i<$helo->len;i++)
printf(“slice[%d]:%s\n”,i,user_string((&@cast($helo->array,”struct string”)[i])->str))
probe process(“/bin/docker-runc”).function(“github.com/opencontainers/runc/libcontainer/specconv.createDevices”).call{
for(i=0;i<$spec->Linux->Device->len;i++){
printf(“%d:Major %d n”,i,user_int((&@cast($spec->Linux->Devices->array,”github.com/opencontainers/runtime-spec/specs-go.Device”)[i])->Major))
}
}