systemtap
# 用户态程序简单使用示例
# bpftrace 打印执行到指定地址:
bpftrace -e 'uprobe:/home/zhangsan/sometest/a.out:0x401146 {printf("aa\n");}'
# bpftrace脚本测量fun1到fun2的耗时:
# time_cost.bt:
uprobe:/home/a.out:fun1 {
@start = nsecs;
}
uprobe:/home/a.out:fun2 {
@cost[nsecs-@start]++;
}
2
3
4
5
6
7
运行: bpftrace time_cost.bt
# systemtap脚本打印函数调用入口或执行到指定地址:
#probe process("/home/zhangsan/sometest/a.out").statement(0x401146)
probe process("/home/zhangsan/sometest/a.out").function("*")
{
# printf ("%s called\n", ppfunc())
printf ("addr called\n")
}
2
3
4
5
6
# systemtap画函数调用图
tmp.stp:
probe process("/home/zhangsan/sometest/a.out").function("*").call
{
printf ("%s -> %s\n", thread_indent(2), ppfunc())
}
probe process("/home/zhangsan/sometest/a.out").function("*").return
{
printf ("%s <- %s\n", thread_indent(-2), ppfunc())
}
2
3
4
5
6
7
8
9
执行 stap tmp.stp
可得到a.out程序执行时的函数调用图
也可以用它自带的例子,下面有。
# systemtap
# 安装
以centos7.6.1810为例,内核版本为 3.10.0-957.el7.x86_64
更新yum源为国内源 yum install systemtap*
在Index of /7/x86_64 (centos.org) http://debuginfo.centos.org/7/x86_64/ (opens new window) 下载对应内核版本kernel-debuginfo 和 kernel-debuginfo-common rpm包并安装
- kernel-debuginfo: http://debuginfo.centos.org/7/x86_64/kernel-debuginfo-3.10.0-957.el7.x86_64.rpm (opens new window)
- kernel-debuginfo-common: http://debuginfo.centos.org/7/x86_64/kernel-debuginfo-common-x86_64-3.10.0-957.el7.x86_64.rpm (opens new window)
- glibc-debuginfo: http://debuginfo.centos.org/7/x86_64/glibc-debuginfo-2.17-260.el7.x86_64.rpm (opens new window)
- glibc-debuginfo-common: http://debuginfo.centos.org/7/x86_64/glibc-debuginfo-common-2.17-260.el7.x86_64.rpm (opens new window)
执行stap -v -e 'probe vfs.read {printf("read performed\n"); exit()}' 成功即systemtap安装成功
调试glibc还需要安装glibc调试信息 vim /etc/yum.repos.d/CentOS-Debuginfo.repo enabled=1 yum install yum-utils debug-install glibc* 安装glibc调试信息 或者在这个centos官网下载https://vault.centos.org/7.6.1810/os/x86_64/Packages/ (opens new window)
# 下载centos glibc和kernel源码的方法:
- 参考: https://blog.csdn.net/u010743406/article/details/121293287 (opens new window)
- 官网: https://vault.centos.org/7.6.1810/os/Source/SPackages/ (opens new window)
- glibc: https://vault.centos.org/7.6.1810/os/Source/SPackages/glibc-2.17-260.el7.src.rpm (opens new window)
- kernel: https://vault.centos.org/7.6.1810/os/Source/SPackages/kernel-3.10.0-957.el7.src.rpm (opens new window)
# 使用
安装后自带的例子,/usr/share/systemtap/examples/general/para-callgraph-verbose.stp
#!/usr/bin/stap
function trace(entry_p, extra) {
%( $# > 1 %? if (tid() in trace) %)
printf("%s%s%s %s\n",
thread_indent (entry_p),
(entry_p>0?"->":"<-"),
ppfunc (),
extra)
}
%( $# > 1 %?
global trace
probe $2.call {
trace[tid()] = 1
}
probe $2.return {
delete trace[tid()]
}
%)
probe $1.call { trace(1, $$parms$$) }
probe $1.return { trace(-1, $$return) }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
使用:
cp /usr/share/systemtap/examples/general/para-callgraph-verbose.stp .
# 内核追踪
stap para-callgraph-verbose.stp 'kernel.function("*@net/socket.c")'
# 程序或so追踪
stap para-callgraph-verbose.stp 'process("/path/to/exe_or_so").function("*")'
# 追踪程序中某个函数调用, 如send
stap para-callgraph-verbose.stp 'process("/path/to/exe_or_so").function("send")'
2
3
4
5
6
7
8
9
10
# 参考
网络 - SystemTap 新手指南 - 文江博客 (wenjiangs.com)
# ppfunc
https://www.cnblogs.com/zengkefu/p/6372255.html (opens new window)
# systemtap、bpftrace的相同点和不同点
参考: https://blog.csdn.net/Linux_Everything/article/details/116141384 SystemTap 会将用户提供的脚本翻译成 C 代码,然后作为 module 编译并加载到所运行的 Linux 内核中;bpftrace 则相反,会将脚本转换为 LLVM 中间表示,然后再编译为 BPF 程序。 对于许多应用场景来说,bpftrace 开箱即用,而 SystemTap 通常需要安装额外的依赖关系才能充分利用其所有功能。Bpftrace 的速度一般比较快,而且提供了各种快速汇总和报告的功能,可以说比 SystemTap 提供的类似功能更简单易用。另一方面,SystemTap 提供了一些与众不同的功能,比如:生成用户空间 backtrace 而不需要 frame pointer、通过变量名来访问函数参数和局部变量、以及对任意语句进行 probe 的能力
参考2:https://catbro666.github.io/posts/46dd3f4b/ bpftrace语言特性上没有systemtap丰富,不太能进行复杂的探测操作。想将探针放在函数某个语句上时,可以指定函数名+偏移量(通过汇编查看)。应用程序中的局部变量没有方便的获取方法,可以查看汇编代码,通过寄存器获取。
BPF限制
- 不能调用任意的内核函数:只能调用BPF helper。
- BPF程序不能做循环,因为必须在规定时间内结束。后续(Linux 5.3)会支持受限的循环。(随着即将添加的循环,您可能会开始怀疑BPF是否将成为图灵完整的。 BPF指令集本身允许创建图灵完备的自动机,但是由于验证器引入的安全性限制,BPF程序不是图灵完整的。)
- BPF栈大小限制为MAX_BPF_STACK,设置为512,多用几个字符串就用完了。解决方法是使用BPF映射存储替代
- 指令数限制为4096。Linux5.2大幅增加了这个限制。
BPF其他缺点
- 探针附到函数一定偏移处不是很方便
- 无法直接获取被探测应用程序中函数局部变量值
- 结构体的访问非常麻烦,需要将所有依赖的头文件及其路径都包含进来
- 内核版本要求较高,且在较旧的发行版上安装体验不佳
bpftrace优点
- bpftrace基于内置Linux技术,不用追赶内核版本改动,稳定性更高
- 脚本执行速度比systemtap快(使用llvm编译成BPF)
systemtap缺点
- 基于内核模块技术,在RHEL以外的系统上都不可靠
- 不是内置于内核,需要追赶内核版本改动,稳定性不如bpftrace
- 脚本执行速度比bpftrace慢,且在生产环境需要gcc编译工具链、内核头文件
- 依赖dwarf,需要安装内核符号,用户层程序则在编译时需要-g
systemtap优点
- 脚本语言支持的特性更全,如函数偏移、循环、函数局部变量、结构体引用
- 具有成熟的用户态符号自动加载,可以写比较复杂的探针处理程序
- systemtap中有大量帮助程序(tapset),可用于检测不同的目标
- systemtap普通模式只读,guru模式可以写,如修改程序中某个变量值
- systemtap内核版本要求较低,在4.x以前内核上也能使用。
# systemtap/bpftrace底层实现
systemtap/bpftrace底层使用的是kprobe/uprobe,通过编写内核模块(ko)调用内核提供的kprobe/uprobe接口实现探测功能。
kprobe/uprobe可以在任何一条语句上加上探针,当执行到这条语句时将控制流转移到探针的handler上。其实现原理是在module_init时将相应指令的第一个字节替换成0xcc,也就是INT 3。这样执行到这条指令时就会跳转到INT 3的中断处理函数,由其判断出应该跳转到哪个handler。在module_exit中将这些指令还原。
# 热补丁/gdb调试原理
通过ptrace系统调用修改目标进程。ptrace系统调从名字上看是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。其基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被系统标注为TASK_TRACED。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。
断点的实现类似于kprobe,将要中断的指令的第一个字节替换为0xcc,继续运行时恢复该字节。