整体的学习思维导图如下,后续持续更新完善
文章目录
- 安装
- 简介
- 执行流程
- 执行方式
- stap脚本语法
- 探针语法
- API函数
- 探针举例
- 变量使用
- 1. 定位函数位置
- 2. 查看文件能够添加探针的位置
- 3. 打印函数参数(结构体)
- 4. 打印函数局部变量
- 5. 修改函数局部变量(慎重)
- 6. 打印函数返回时的变量
- 7. 打印函数调用栈
- 8. 嵌入C代码
- 9. 追踪函数流程
- 10. 跟踪特定进程
- 11. 查看代码执行路径
- 12. 查看内核文件函数的执行流程
- 13. 调试指定模块
- 14. 抓取`kill -l`相关的信号
环境:
3.10.0-957.5.1.el7.x86_64
安装
centos下安装如下rpm包, 安装前uname -r
核对自己的内核版本,下载自己对应的内核版kernel包进行安装即可
rpm -ivh kernel-debuginfo-common-x86_64-3.10.0-123.el7.x86_64.rpm
rpm -ivh kernel-debuginfo-3.10.0-123.el7.x86_64.rpm
rpm -ivh kernel-debug-devel-3.10.0-123.el7.x86_64.rpm
安装systemtap
yum install systemtap-devel-2.4-14.el7.x86_64
yum install systemtap-client-2.4-14.el7.x86_64
安装成功后测试如下:
[root@node1 kernel_compile]# stap -L 'kernel.statement("sys_open")'
kernel.statement("SyS_open@fs/open.c:1063") $filename:long int $flags:long int $mode:long int
简介
systemtap 是一个非常强大的性能诊断以及内核调试工具,相比于传统的内核调试方法(开debug级别,printk,加打印。。):修改代码,编译模块,安装模块,运行模块 的调试过程,systemtap提供友好的内核调试方法,极大得节省了调试损耗的精力。
官网地址: systemtap
执行流程
主要分为5步,从开始到结果最终是将具有探针行为的Stap脚本转换为内核模块并加载
- 将stap 脚本转换为解析树
- 解析stap脚本中的符号
- 将解析后的结果转为c代码
- 将c代码编译成对应的ko驱动文件
- 加载驱动,开始运行(staprun加载生成的模块,stapio将结果输出到终端)
可以使用命令stap -v xxx.stp
将解析以及加载过程打印出来
[root@node1 ~]# stap -v -g vfs.stp
Pass 1: parsed user script and 116 library script(s) using 230588virt/40808res/3156shr/38524data kb, in 190usr/10sys/195real ms.
Pass 2: analyzed script: 43 probe(s), 28 function(s), 1 embed(s), 0 global(s) using 709984virt/91896res/8712shr/81664data kb, in 880usr/190sys/1072real ms.
Pass 3: using cached /root/.systemtap/cache/b0/stap_b066af4ce5ad5d99a19d4ad9ec41fc8e_23076.c
Pass 4: using cached /root/.systemtap/cache/b0/stap_b066af4ce5ad5d99a19d4ad9ec41fc8e_23076.ko
Pass 5: starting run
执行方式
- 终端执行stap 命令
stap -L 'kernel.statement("sys_read")'
查看sys_read系统调用的函数位置 - 执行stp脚本
cat test.stp
该脚本为打印调用sys_read
系统调用的进程名称和进程号 #!/usr/bin/stap
probe begin {
printf("begin to probe");
}
probe kernel.function("vfs_read") {
printf("%s %d\n", execname(),pid());
}
probe end {
printf("end to probe");
}
- 执行方式
stap test.stp
或者./test.stp
脚本方式执行更加规范,且更容易编写,所以这里建议使用脚本方式进行执行
stap脚本语法
探针语法
kernel.function(pattern)
| 在内核函数的入口处放置探测点,可以获取参数$parm
|
kernel.function(pattern).return
| 在内核函数返回时的出口处放置探测点,可以获取返回时的参数$parm
|
kernel.function(pattern).call
| 内核函数的调用入口处放置探测点,获取对应函数信息
|
kernel.fuction(pattern).inline
| 获取符合条件的内联函数
|
kernel.function(pattern).exported
| 只选择导出的函数
|
module(moduname).fuction(pattern)
| 在模块modulename中调用的函数入口处放置探测点
|
module(moduname).fuction(pattern).return
| 在模块module中调用的函数返回时放置探测点
|
module(moduname).fuction(pattern).call
| 在模块modulename中调用的函数入口处放置探测点
|
module(moduname).fuction(pattern).inline
| 在模块modulename中调用的内联函数处放置探测点
|
kernel.statement(pattern)
| 在内核中的某个地址处增加探针(函数、文件行号)
|
kernel.statement(pattern).absolute
| 在内核中的某个地址处增加探针(函数、文件行号),精确匹配地址
|
module(modulename).statement(pattern)
| 在内核模块中的某个地址处增加探针(函数、文件行号)
|
API函数
函数
| 说明
|
execname()
| 获取当前进程名称
|
pid()
| 当前进程的ID
|
tid()
| 当前线程ID
|
cpu()
| 当前cpu号
|
gettimeofday_s()
| 获取当前系统时间,秒
|
gettimeofday_usec()
| 获取当前系统时间,微秒
|
ppfunc()
| 获取当前probe的函数名称,可以知道当前probe位于哪个函数
|
print_backtrace()
| 打印内核函数调用栈
|
print_ubacktrace()
| 打印用户态函数调用栈
|
探针举例
探针名称
| 探针含义
|
begin
| 脚本开始时触发
|
end
| 脚本结束时触发
|
kernel.function(“sys_read”)
| 调用sys_read时触发
|
kernel.function(“sys_read”).call
| 同上
|
kernel.function(“sys_read”).return
| sys_read执行完,返回时触发
|
kernel.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(200).randomize(50)
| 每隔200毫秒触发一次,带有线性分布的随机附加时间(-50到+50)
|
变量使用
变量格式
| 使用
|
$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
| 包含所有函数参数的变量,需以字符串类型输出
|
基本应用
1. 定位函数位置
stap -L 'kernel.function("vfs_statfs")'
kernel.function("vfs_statfs@fs/statfs.c:68") $path:struct path* $buf:struct kstatfs* $error:int
打印出函数的所处文件位置及行号,同时-L
参数支持打印函数的参数及类型
stap -l 'kernel.function("vfs_statfs")'
-l
参数打印的信息就稍微简略一点
kernel.function("vfs_statfs@fs/statfs.c:68")
2. 查看文件能够添加探针的位置
stap -L 'kernel.statement("*@fs/statfs.c")'
查看statfs.c 文件中能够添加探针的位置
kernel.statement("SYSC_fstatfs64@fs/statfs.c:204") $fd:unsigned int $sz:size_t $buf:struct statfs64* $st:struct kstatfs
kernel.statement("SYSC_fstatfs@fs/statfs.c:195") $fd:unsigned int $buf:struct statfs* $st:struct kstatfs
kernel.statement("SYSC_statfs64@fs/statfs.c:183") $pathname:char const* $sz:size_t $buf:struct statfs64* $st:struct kstatfs
kernel.statement("SYSC_statfs@fs/statfs.c:174") $pathname:char const* $buf:struct statfs* $st:struct kstatfs
kernel.statement("SYSC_ustat@fs/statfs.c:230") $dev:unsigned int $ubuf:struct ustat* $tmp:struct ustat $sbuf:struct kstatfs
kernel.statement("SyS_fstatfs64@fs/statfs.c:204") $fd:long int $sz:long int $buf:long int $ret:long int
kernel.statement("SyS_fstatfs@fs/statfs.c:195") $fd:long int $buf:long int $ret:long int
kernel.statement("SyS_statfs64@fs/statfs.c:183") $pathname:long int $sz:long int $buf:long int $ret:long int
kernel.statement("SyS_statfs@fs/statfs.c:174") $pathname:long int $buf:long int $ret:long int
kernel.statement("SyS_ustat@fs/statfs.c:230") $dev:long int $ubuf:long int $ret:long int
kernel.statement("calculate_f_flags@fs/statfs.c:45")
kernel.statement("do_statfs64@fs/statfs.c:150") $st:struct kstatfs* $p:struct statfs64* $buf:struct statfs64
kernel.statement("do_statfs_native@fs/statfs.c:108") $st:struct kstatfs* $p:struct statfs* $buf:struct statfs
kernel.statement("fd_statfs@fs/statfs.c:97") $fd:int $st:struct kstatfs*
kernel.statement("flags_by_mnt@fs/statfs.c:12") $mnt_flags:int
kernel.statement("flags_by_sb@fs/statfs.c:33") $s_flags:int
kernel.statement("statfs_by_dentry@fs/statfs.c:51") $dentry:struct dentry* $buf:struct kstatfs*
kernel.statement("user_statfs@fs/statfs.c:79") $pathname:char const* $st:struct kstatfs* $path:struct path
kernel.statement("vfs_statfs@fs/statfs.c:68") $path:struct path* $buf:struct kstatfs* $error:int
kernel.statement("vfs_ustat@fs/statfs.c:218") $dev:dev_t $sbuf:struct kstatfs*
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
3. 打印函数参数(结构体)
vfs_statfs函数如下:
int vfs_statfs(struct path *path, struct kstatfs *buf)
{
int error;
error = statfs_by_dentry(path->dentry, buf);
if (!error)
buf->f_flags = calculate_f_flags(path->mnt);
return error;
}
如下脚本test.stp
:
#!/usr/bin/stap
probe begin {
printf("begin to probe");
}
probe kernel.function("vfs_statfs") { //在调用vfs_statfs处添加探针
printf("%s %d\n", execname(),pid());//打印调用vfs_statfs函数的进程名称以及进程号
printf("path : %s buf : %s\n",$path->mnt->mnt_root->d_iname$,$buf$); //打印path参数的结构体成员,以及整个buf结构体
probe end {
printf("end to probe");
}
运行stap test.stp
输出如下:
[root@node1 ~]# stap test.stp
begin to probesafe_timer 145286
df 2041244
path : "/" buf : {.f_type=393, .f_bsize=140207161574688, .f_blocks=94629230035116, .f_bfree=29879, .f_bavail=18446620715516395336, .f_files=18446744071970029248, .f_ffree=4294967295, .f_fsid={...}, .f_namelen=4294967295, .f_frsize=94629230035016, .f_flags=1574682515518227985, .f_spare=[...]}
safe_timer 145286
4. 打印函数局部变量
vfs_statfs函数如下:
int vfs_statfs(struct path *path, struct kstatfs *buf)
{
int error;
error = statfs_by_dentry(path->dentry, buf);
if (!error)
buf->f_flags = calculate_f_flags(path->mnt);
return error;
}
我想要查看error
在函数statfs_by_dentry
执行完成之后的结果,那么就在下一行处添加探针
查看如下脚本test.stp
:
#!/usr/bin/stap
probe begin {
printf("begin to probe");
}
probe kernel.statement("vfs_statfs@fs/statfs.c:73") {//探测statfs.c中的第73行
printf("%s %d\n", execname(),pid());
printf("error number is %d\n",$error);
}
probe end {
printf("end to probe");
}
这里需要注意脚本中在statfs.c的第73行增加探测点,必须填写行号正确,如果73行处没有接下来想要探测的error
变量,执行报错
执行stap test.stp
输出如下
begin to probe
safe_timer 145286
error number is 0
...
5. 修改函数局部变量(慎重)
我们想要将上一个打印的变量的值从0 更改为其他的数值
查看如下脚本test.stp
#!/usr/bin/stap
probe begin {
printf("begin to probe");
}
probe kernel.statement("vfs_statfs@fs/statfs.c:73") {
printf("%s %d\n", execname(),pid());
printf("error number before modify is %d\n",$error);
$error=$1; //传入的第一个参数
printf("error number after modify is %d\n",$error);
}
probe end {
printf("end to probe");
}
执行stap -g test.stp 2
将2传入,但是运行的时候需要增加-g
参数
输出如下:
begin to probe
df 3173946
error number before modify is 0
error number after modify is 2
...
6. 打印函数返回时的变量
还是举例我们的vfs_statfs
,这个函数主要是在ls,df,stat…类似获取文件或者文件夹属性时由系统调用SyS_statfs
调用的
函数实现如下
int vfs_statfs(struct path *path, struct kstatfs *buf)
{
int error;
error = statfs_by_dentry(path->dentry, buf);
if (!error)
buf->f_flags = calculate_f_flags(path->mnt);
return error;
}
这里我们想要查看一下函数返回时error变量的值,查看如下test1.stp
#!/usr/bin/stap
probe begin {
printf("begin to probe\n");
}
probe kernel.function("vfs_statfs").return { //在调用vfs_statfs处添加探针
printf("%s %d\n", execname(),pid());//打印调用vfs_statfs函数的进程名称以及进程号
printf("error's return value is %d\n", @entry($error));//打印局部变量需使用@entry($varname)
}
probe end {
printf("end to probe");
}
输出如下:
begin to probe
safe_timer 752879
error's return value is 0
safe_timer 752879
error's return value is 0
7. 打印函数调用栈
我们想要打印某一个系统调用的调用栈,可以执行如下脚本
#!/usr/bin/stap
probe kernel.function("vfs_statfs") {
printf("%s %d\n", execname(),pid());
printf("----------------------------------\n");
print_backtrace();//打印vfs_statfs在内核态的调用栈
printf("----------------------------------\n");
}
如果过程中发现调用栈打印不全,则尝试如下两种办法解决
- 执行时增加参数
--all-modules
,类似如:stap --all-modules test.stp
,探测所有的系统模块 - 检查
stap
版本,像我的环境版本为version 2.4/0.158, rpm 2.4-14.el7
导致调用栈没有任何打印;需要升级stap的库才行,执行yum install systemtap -y
即可,升级之后我的版本version 4.0/0.176, rpm 4.0-10.el7_7
,这个时候成功打印内核调用栈
输出如下:
safe_timer 752879
----------------------------------
0xffffffff98677430 : vfs_statfs+0x0/0xc0 [kernel]
0xffffffff98677725 : user_statfs+0x55/0xa0 [kernel]
0xffffffff98677797 : SYSC_statfs+0x27/0x60 [kernel]
0xffffffff9867799e : SyS_statfs+0xe/0x10 [kernel]
0xffffffff98b5fddb : system_call_fastpath+0x22/0x27 [kernel]
0x7fcb8bcfe787
----------------------------------
如果想要通过print_ubacktrace()
函数来打印用户态的系统调用,则需要安装glibc的符号调试包glibc-debuginfo
从而能够对libc
库进行调试
8. 嵌入C代码
如下脚本test.stp
,想要获取系统调用了vfs_statfs
函数的次数
#!/usr/bin/stap
global count = 0;//全局变量
function getcount:long(task:long) //C函数计算次数
%{
int count = (int) STAP_ARG_task;
count ++;
STAP_RETURN(count);
%}
probe begin {
printf("begin to probe\n");
}
probe kernel.function("vfs_statfs") {
printf("%s %d\n", execname(),pid());
printf("----------------------------------\n");
print_ubacktrace();
printf("----------------------------------\n");
count = getcount(count);
printf("c code caculate the count is %d\n", count);
}
probe end {
printf("end to probe\n");
}
最终输出如下,可以看到count一直在增加:
[root@node1 ~]# stap -g test.stp
WARNING: Missing unwind data for a module, rerun with 'stap -d /usr/lib64/libc-2.17.so'
begin to probe
safe_timer 752879
----------------------------------
0x7fcb8bcfe787 [/usr/lib64/libc-2.17.so+0xef787/0x3c8000]
----------------------------------
c code caculate the count is 1
safe_timer 752879
----------------------------------
0x7fcb8bcfe787 [/usr/lib64/libc-2.17.so+0xef787/0x3c8000]
----------------------------------
c code caculate the count is 2
systemd-journal 564
----------------------------------
0x7f6176e2f7b7 [/usr/lib64/libc-2.17.so+0xef7b7/0x3c8000]
----------------------------------
c code caculate the count is 3
safe_timer 752879
以上出现的warning是因为系统并未安装glibc的调试库导致的
关于Stap嵌入C代码需要注意以下几点
- 格式上:C语言代码要在每个大括号前加%前缀,是%{…… %} 而不是%{ …… }%;
- 获取脚本函数参数要用STAP_ARG_前缀,即
getcount
函数使用的是STAP_ARG_task
来获取传入的count参数 - 一般long等返回值用
STAP_RETURN
,一般字符串则使用snprintf、strncat等方式把字符串复制到STAP_RETVALUE
里面
9. 追踪函数流程
我们想要知道当前函数被哪个进程调用,且在该函数处执行了多长时间,可以使用如下脚本进程探测
trace_sys_read.stp
#!/usr/bin/stap
probe begin {
printf("begin to probe");
}
probe kernel.function("sys_read").call {
printf("%s -> %s\n",thread_indent(4),ppfunc());
}
probe kernel.function("sys_read").return {
printf("%s <- %s\n",thread_indent(-4),ppfunc());
}
其中thread_indent()
函数为/usr/share/systemtap/tapset/indent.stp
中实现的一个stap脚本,该函数的功能是增加函数执行时间(微妙),进程名称(pid)打印出来,传入的参数是打印空格的个数
输出如下:
0 msgr-worker-1(2445831): -> SyS_read
31 ps(2445722): <- SyS_read
0 ps(2445722): -> SyS_read
1 ps(2445722): <- SyS_read
0 sed(2445856): -> SyS_read
2 sed(2445856): <- SyS_read
24 msgr-worker-1(2445831): <- SyS_read
发现Sys_read
系统调用被某个进程调用时的开始到返回的执行时间,这个信息对内核代码的流程分析非常有利
10. 跟踪特定进程
除了跟踪具体的某个函数被哪个进程调用之外我们还能够跟踪一个进程所调用过的函数
如下脚本,我们追踪sshd
进程调用的系统调用
#!/usr/bin/stap
probe begin {
printf("begin to probe\n");
}
probe syscall.* //探测所有的系统调用
{
procname = execname();
if (procname =~ "sshd.*"){ //使用stp脚本中的通配符匹配所有的sshd服务的子进程
printf("%s[%d]: %s -> %s\n", procname,pid(),name,ppfunc()); //name为sshd内部函数,ppfunc为该函数调用的系统调用
}
}
输出如下,非常直观
sshd[2087388]: write -> SyS_write
sshd[2087388]: clock_gettime -> SyS_clock_gettime
sshd[2087388]: select -> SyS_select
sshd[2087388]: rt_sigprocmask -> SyS_rt_sigprocmask
sshd[2087388]: rt_sigprocmask -> SyS_rt_sigprocmask
sshd[2087388]: clock_gettime -> SyS_clock_gettime
sshd[2087388]: write -> SyS_write
sshd[2087388]: clock_gettime -> SyS_clock_gettime
sshd[2087388]: select -> SyS_select
11. 查看代码执行路径
我们来看一个有意思且非常便捷准确的阅码方式,如下代码为内核处理文件属性的逻辑
分支相对来说较多,我们想要知道当前系统针对该函数的处理过程,走到哪个分支,则执行如下探测脚本
#!/usr/bin/stap
probe begin
{
printf("begin to probe\n");
}
probe kernel.statement("do_statfs_native@fs/statfs.c:*")
{
printf("%s\n",pp());
}
输出结果如下:
[root@node1 stap]# stap trace_code.stp
begin to probe
kernel.statement("do_statfs_native@fs/statfs.c:109")
kernel.statement("do_statfs_native@fs/statfs.c:113")
kernel.statement("do_statfs_native@fs/statfs.c:146")
kernel.statement("do_statfs_native@fs/statfs.c:148")
执行过程的具体行号已经打印出来了,此时对照代码即可知道内核处理该函数时如何进行分支处理
12. 查看内核文件函数的执行流程
我们想要查看一个源码文件中函数的执行流程时怎么查看呢?因为上文已经描述了如何跟踪特定进程的执行过程,对代码稍作修改如下
#!/usr/bin/stap
probe begin
{
printf("begin to probe\n");
}
probe module("ceph").function("*@mds_client.c").call{ //监控mds_client.c文件中所有的函数,调用时打印
printf("%s -> %s \n", thread_indent(4), ppfunc());
}
probe module("ceph").function("*@mds_client.c").return{//监控mds_client.c文件中所有的函数,返回时打印
printf("%s <- %s \n", thread_indent(-4), ppfunc());
}
输出如下:
[root@node3 stap]# stap trace_mdsclient.stp
begin to probe
0 kworker/3:2(582080): -> delayed_work
6 kworker/3:2(582080): -> __ceph_lookup_mds_session
8 kworker/3:2(582080): -> get_session
10 kworker/3:2(582080): <- get_session
12 kworker/3:2(582080): <- __ceph_lookup_mds_session
14 kworker/3:2(582080): -> con_get
16 kworker/3:2(582080): -> get_session
16 kworker/3:2(582080): <- get_session
18 kworker/3:2(582080): <- con_get
如果我们想要监控指定的进程在指定的内核文件中的执行过程,可以使用如下代码进行监控
#!/usr/bin/stap
probe begin
{
printf("begin to probe\n");
}
probe module("ceph").function("*@mds_client.c").call{
if(target() == pid()) {//使用target过滤我们输入的进程ip
printf("%s -> %s \n", thread_indent(4), ppfunc());
}
}
probe module("ceph").function("*@mds_client.c").return{
if(target() == pid()) {
printf("%s <- %s \n", thread_indent(-4), ppfunc());
}
}
执行如下stap -x pid trace_mdsclient.stp
即可对指定的进程idpid
的过滤,输出其在mds_client.c代码中的执行流程
13. 调试指定模块
如我想要调试ceph模块,基本的脚本编写语法我们已经在前文脚本语法中提到过,类似如下module("ceph").function("")
,整体的调试方式和我们前面描述的内核调试方式类似
需要注意调试模块之前需要将模块拷贝到目录/usr/lib/modules/`uname -r`/extra/ 之下才能够正常调试
如何检测能够正常调试一个自己的模块呢,使用如下命令
这里使用ceph模块中的ceph_statfs来做测试
stap -l 'module("ceph").function("ceph_statfs")'
,显示如下输出即可
module("ceph").function("ceph_statfs@fs/ceph/super.c:55")
14. 抓取kill -l
相关的信号
想要抓取系统中哪个进程发送的kill信号
global target
global signal
probe nd_syscall.kill
{
target[tid()] = uint_arg(1);
signal[tid()] = uint_arg(2);
}
probe nd_syscall.kill.return
{
if (target[tid()] != 0) {
printf("%-6d %-12s %-5d %-6d %6d\n", pid(), execname(),
signal[tid()], target[tid()], int_arg(1));
delete target[tid()];
delete signal[tid()];
}
}
stap test.stp
会抓取发送kill信号的进程