203. systemtap stap命令
安装
Systemtap Installation
1 | sudo apt-get install -y systemtap gcc |
Where to get debug symbols for kernel X?
GPG key import
16.04 and higher
1
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys C8CAB6595FDFF622
older distributions
1
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys ECDCAD72428D7C01
Add repository config
1 | codename=$(lsb_release -c | awk '{print $2}') |
简介
概览
介绍
- systemtap是一个强大的调试工具,一种调试语言
- 支持脚本编程,“tapset”,更简洁,更高效
特点:
- Flexibility:稳定,提供问题的接口,方便用户扩展
- Easeofuse:动态监控,不影响原程序运行
官方链接:https://sourceware.org/systemtap
systemtap 典型应用场景
- 定位(内核)函数位置
- 查看函数被调用时的调用堆栈、局部变量、参数
- 查看函数指针变量实际指的是哪个函数
- 查看代码的执行轨迹(哪些行被执行了)
- 查看内核或者进程的执行流程
- 调试内存泄露或者内存重复释放
- 统计函数调用次数
官方链接:https://sourceware.org/systemtap/tapsets/index.html
如何使用systemtap
stap 命令
常用参数:
参数 | 说明 |
---|---|
-e SCRIPT |
Run given script. |
-l PROBE |
List matching probes. |
-L PROBE |
List matching probes and local variables. |
-g |
Guru mode |
-D NM=VAL |
emit macro definition into generated C code |
-o FILE |
send script output to file, instead of stdout. |
-x PID |
sets target() to PID |
注意安装内核调试包
3种执行方式
从标准输入中读入并运行脚本
1 | $ stap [选项] |
如: 定位函数位置: stap -l 'kernel.function("vfs_open")'
linux源码:https://elixir.bootlin.com/linux/v6.5/source/fs/open.c#L1045
运行命令行中的脚本
1 | stap [选项] -e 脚本 |
示例
1 | stap -e 'probe begin { log("hell world") exit() }' |
常用选项
-v
: 展开显示脚本解析、细化、翻译C、编译C、加载内核模块整个过程1
stap -ve 'probe begin { log("hell world") exit() }'
-k
: 保存编译期间的中间文件1
stap -ke 'probe begin { log("hell world") exit() }'
-o
: FILE 输出到文档,而不是输出到标准输出-p
NUM: 运行完Pass Num后停止,缺省是运行到 Pass 51
stap -p 2 -kve 'probe begin { log("hell world") exit()}'
-g
: 采用guru
模式,允许脚本中嵌入C语句;-c CMD
: 启动探测后,运行CMD命令,直到命令结束后退出;-x pid
: 使用target()
捕获指定的进程ID (-x procss_id
指定探测进程ID为’process_id‘的程序)
实例:systemtap 是如何运行的?

Systemtap 的处理流程有5个步骤:
- 解析 script 文件(Parse)
- 细化(Elaborate)
- Script 文件翻译成 C 语言代码(Translate)
- 编译 C 语言代码(生成内核模块)(Compile)
- 加载内核模块(Run)

执行.stp脚本文件
执行stap文件命令
1 | stap[选项] xx.stp |
示例
1 | stap isprime_test.stp |
常用选项
-o
: FILE 输出到文档,而不是输出到标准输出1
stap -o outfile.log isprime_test.stp
-S size,count
: 限制输出文件的大小(单位MB)和最大的文件数。这个选项会实现一个回滚机制的日志输出,日志文件名会加一个序列号的后缀。1
stap -o outfile.log -S 1,2 isprime_test.stp
-F
: 使用SystemTap的长期记录模式,脚本后台执行。-x process_id
: 设置SystemTap处理函数target()
指向特定进程号
脚本语法
systemtap 脚本文件是 .stp 后缀的文件,使用的脚本语言是前面讲到的 systemtap 自己定义的脚本语言,一个 systemtap 脚本描述了将要探测的探测点以及定义了相关联的处理函数,每一个探测点对应于一个内核函数或事件或函数内部的某一位置。被关联的处理函数将在内核执行到对应的探测点时被执行。
systemtap 脚本包含两个基本元素:event 和 handler。Systemtap 执行脚本时,它会监控事件(event),当事件发生时,Linux 内核就会执行 handler, 其中:
- 事件(event)的类型有开始/结束、定时器超时、会话终止等。
- Handler 是指定事件发生时需要做的一些脚本语句,我们可以在这里指定为各类维测或者想跟踪的信息。
关键字:
- probe: 探测,是 systemtap 进行具体地收集数据的关键字。
- probe point:是 probe 动作的时机,也称探测点,是 probe 程序监视的某事件点,一旦侦测的事件(event)触发了,则 probe 将从此处插入内核或者用户进程中。
- probe handler:是当 probe 插入内核或者用户进程后所做的具体动作。
probe 的用法如下:
1 | probe probe-point { statement } |
数据结构
- 整数(integers)
- 字符串(strings)
- 关联数组(associative Arrays)
控制结构
条件(
if(){}else{}
)循环(
for(exp1;exp2;exp3)
,do{}while(exp)
,foreach
)函数(
functions
)systemtap 提供了丰富的内置函数,主要有转换函数、字符操作函数、时间戳信息等。详情见:
break
continue
next
return
delete
try/catch
变量
无需声明, 上下文自动推测和检查
全局变量: 使用 globle
修饰
语句
语句分隔符 ;
是可选的
探测probe
使用kprobe提供的接口来实现探测,对于每一个探测,需要定义探测点以及相应的处理函数
探针语法
内核的探测点语法:
kernel.function(pattern)
: 在内核函数的入口处放置探测点,可以获取参数$parm
kernel.function(pattern).return
: 在内核函数返回时的出口处放置探测点,可以获取返回时的参数$parm
kernel.function(PATTERN).return.maxactive(VALUE)
kernel.function(pattern).call
: 内核函数的调用入口处放置探测点,获取对应函数信息kernel.fuction(pattern).inline
: 获取符合条件的内联函数kernel.function(pattern).exported
: 只选择导出的函数kernel.function(PATTERN).label(LPATTERN)
module(modulename).fuction(pattern)
: 在模块modulename中调用的函数入口处放置探测点module(modulename).fuction(pattern).return
: 在模块module中调用的函数返回时放置探测点module(modulename).fuction(pattern).return.maxactive(VALUE)
module(modulename).fuction(pattern).call
: 在模块modulename中调用的函数入口处放置探测点module(modulename).fuction(pattern).inline
: 在模块modulename中调用的内联函数处放置探测点kernel.statement(pattern)
: 在内核中的某个地址处增加探针(函数、文件行号)kernel.statement(pattern).absolute
: 在内核中的某个地址处增加探针(函数、文件行号),精确匹配地址module(modulename).statement(pattern)
: 在内核模块中的某个地址处增加探针(函数、文件行号)
用户态程序的探测点语法
process(PROCESSPATH).function(PATTERN)
process(PROCESSPATH).function(PATTERN).call
process(PROCESSPATH).function(PATTERN).return
process(PROCESSPATH).function(PATTERN).inline
process(PROCESSPATH).statement(PATTERN)
语法解释说明:
return
: 表示返回点探测return.maxactive(VALUE)
: 修饰return
,控制同时探测多少个实例,默认足够一搬不用,如果出现了跳过探测现象且很多,可以使用此参数,提升探测效果Call
: 表示函数被调用时触发此调用点Inline
: 表示内联函数需要展示时候用此参数Label
: 表示内核常常用到goto
函数,用此标签可以探测出具体的goto
返回点Statement
: 定位到具体的行或者函数,将这些定位点作为跟踪点
PATTERN语法:
1 | func[@file] |
示例:
kernel.function("*init*")
: 表示对内核中包含有 init 的函数进行探测module(“ext3”).function(“*”)
: 表示对 ext3 内核模块的所有函数进行探测kernel.statement("cmdline_proc_show@fs/proc/cmdline.c:9")
: 表示对 cmdline_proc_show 函数中的 /fs/proc/cmdline.c 文件的第9行进行探测 ==???没看懂==process("/home/tianyu/chmod").function("GetUidGid")
: 表示对用户态程序 /home/tianyu/chmod 的函数GetUidGid
进行探测
探针名称
探针名称 | 探针含义 |
---|---|
begin |
脚本开始时触发 |
end |
脚本结束时触发 |
kernel.function("sys_read") |
调用sys_read 时触发 |
kernel.function("sys_read").call |
同上 |
kernel.function("sys_read").return |
sys_read 执行完,返回时触发 |
syscall.* |
调用任何系统调用时触发 |
kernel.function("*@kernel/fork.c:934") |
执行到 fork.c 的934行时触发 |
module(“ext3”).function(“ext3_file_write”) |
调用ext3模块中的ext3_file_write 时触发 |
timer.jiffies(1000) |
每隔1000个内核jiffy 时触发一次 |
timer.ms() |
|
timer.s() |
Systemtap黑名单
Systemtap包含了一个黑名单,其中列出的函数不能被Systemtap探测,因为它们会导致无限探测循环、锁重入等问题
所有的脚本内容在转换时进行严格的检查,并且在运行时也要检查(如无限循环、内存使用、递归和无效指针等)
API函数
tapsets 是一个脚本库,包含了许多 tapset,每一个 tapset 一般为某一内核子系统或特定的功能块预定义了一套探测点、辅助函数或全局变量供用户脚本或其它的 tapset 引用,它定义的一些数据能够被每一个探测点处理函数或脚本使用
函数 | 说明 |
---|---|
execname() | 获取当前进程名称 |
pid() | 当前进程的ID |
tid() | 当前线程ID |
cpu() | 当前cpu号 |
gettimeofday_s() | 获取当前系统时间,秒 |
gettimeofday_usec() | 获取当前系统时间,微秒 |
ppfunc() | 获取当前probe的函数名称,可以知道当前probe位于哪个函数 |
print_backtrace() | 打印内核函数调用栈 |
变量用法
变量格式 | 使用 |
---|---|
$varname |
引用变量varname |
$var->field |
引用结构的成员变量 |
$var[N] |
引用数组的成员变量 |
&$var |
变量的地址 |
@var(“varname”) |
引用变量varname |
@var(“var@src/file.c”) |
引用src中file.c编译时的全局变量 |
@var(“var@src/file.c”)->field |
src/file.c中全局结构的成员变量 |
@var(“var@src/file.c”)[N] |
src/file.c中全局数据变量 |
&@var(“var@src/file.c”) |
引用变量的地址 |
$var$ |
将变量转为字符串类型 |
$$vars |
包含函数所有参数,局部变量,需以字符串类型输出 |
$$locals |
包含函数所有局部变量,需以字符串类型输出 |
$$params |
包含所有函数参数的变量,需以字符串类型输出 |
systemtap使用技巧
跟踪内核态调用栈和入参
systemtap 提供库函数,分别用于跟踪用户态和内核态的调用堆栈。
用户态:(不带 s 和带 s 的区别是前者直接输出,后者是返回堆栈字符串)
print_ubacktrace()
sprint_ubacktrace()
内核态:
print_backtrace()
sprint_backtrace()
跟踪对文件的 open 流程,更多调用栈和入参,提升内核代码分析和调试的效率
更多跟踪函数:https://linux.die.net/man/5/stapfuncs
示例:
查看
do_sys_open
位置1
2root@ubuntu18:/tmp/stapw0gXSN# stap -L 'kernel.function("do_sys_open")'
kernel.function("do_sys_open@/build/linux-5s7Xkn/linux-4.15.0/fs/open.c:1049") $dfd:int $filename:char const* $flags:int $mode:umode_t $op:struct open_flagsvfsopen.stp
脚本文件运行
vfsopen.stp
脚本文件
跟踪整个数据结构
systemtap 有两个语法可以输出整个数据结构:
在变量的后面加一个或者两个$
号即可,输出的内容是字符串,所以打印的时候需要用 %s
。

跟踪用户态
利用 systemtap 可以轻松跟踪用户态程序和 so 的调用轨迹、参数等
1 | #!/usr/bin/stap |
1 | root@ctumhispra00449:/home/tianyu/stap# ./userspace.stp |
简单实例
打印素数
1 | #!/usr/bin/stap |
通过 stap 脚本实现对内核接口调用点的跟踪, stap 脚本名为 hellowolrd.stp:
1 | #!/usr/bin/stap |
systemtap 技术
systemtap 基本思想是命名事件,并为它们提供处理程序。每当发生指定的事件时,内核都会将处理程序视为子例程运行,然后继续运行。处理程序是一系列脚本语言语句,用于指定事件发生时要完成的工作。
systemtap 基本原理是将脚本翻译成 C 语言,执行 C 编译器创建一个内核模块。当模块被加载后,通过挂载到内核来激活所有的探测事件。
然后,当事件发生在任何处理器上时,编译后的处理程序就会运行。
最终,systemtap 会话停止,hook 取消,内核模块被移除,整个过程由命令行程序 stap 驱动。

用户手册: https://sourceware.org/systemtap/documentation.html

典型应用场景库 193个: https://sourceware.org/systemtap/examples/

基本应用
定位函数位置
查找内核函数的定义位于哪一个文件的哪一行时很有用,特别是有些函数有多个定义的时候(不同的架构或者宏会来控制当前使用哪一个,实际上只会有一个),用 systemtap 一行命令就可以搞定。
比如说我们查看内核的 vfs_open
函数在哪里定义的:

可以在 open.c 文件的第862行定义的。

同时支持*
号来实现模糊查找

-L
查看变量,例如 vfs_open
的两个参数:

其他的一些用法:
1 | stap -l 'kernel.function("vfs_open")' |
查看文件能够添加探针的位置(对比源代码)
1 | stap -L 'kernel.statement("*@fs/statfs.c")' |
打印函数参数
1 | #!/usr/bin/stap |
打印函数局部变量
函数实现

查看结果
查看error在函数statfs_by_dentry执行完成之后的结果,那么就在下一行处添加探针
1 | #!/usr/bin/stap |
修改函数局部变量(慎重)
将上一个打印的变量的值从0 更改为其他的数值
1 | #!/usr/bin/stap |
执行stap -g test.stp 2
将2传入,但是运行的时候需要增加-g参数
打印函数返回时的变量
1 | #!/usr/bin/stap |
打印函数调用栈
1 | #!/usr/bin/stap |
执行时增加参数 --all-modules
, 类似如: stap --all-modules test.stp
, 探测所有的系统模块
嵌入C代码
获取系统调用了vfs_statfs函数的次数
1 | #!/usr/bin/stap |
- 格式上:C语言代码要在每个大括号前加
%
前缀,是%{…… %}
而不是%{ …… }%
- 获取脚本函数参数要用
STAP_ARG_前缀
,即getcount
函数使用的是STAP_ARG_task
来获取传入的count参数 - 一般long等返回值用
STAP_RETURN
,一般字符串则使用snprintf, strncat
等方式把字符串复制到STAP_RETVALUE
里面
1 | stap -g test.stp |
追踪函数流程
函数被哪个进程调用,且在该函数处执行了多长时间
1 | #!/usr/bin/stap |
其中thread_indent()
函数为/usr/share/systemtap/tapset/indent.stp
中实现的一个stap脚本,该函数的功能是增加函数执行时间(微妙),进程名称(pid)打印出来,传入的参数是打印空格的个数
跟踪特定进程
跟踪一个进程所调用过的函数:sshd进程调用的系统调用
1 | #!/usr/bin/stap |
查看代码执行路径

想要知道当前系统针对该函数的处理过程,走到哪个分支
1 | #!/usr/bin/stap |
输出:
1 | root@ubuntu18:~/stap_scipts# stap show_run_flow.stp |
查看内核文件函数的执行流程
1 | #!/usr/bin/stap |
1 | #!/usr/bin/stap |