1.简介
SystemTap是一个Linux非常有用的调试(跟踪/探测)工具,常用于Linu
2.何时使用
定位(内核)函数位置
3.原理
在网上找了个原理图:
SystemTap的处理流程有5个步骤:解析script文件(parse)、细化(elaborate)、script文件翻译成C语言代码(translate)、编译C语言代码(生成内核模块)(build)、加载内核模块(run)
4.安装
SystemTap依赖的package:
elfutils、gcc、kernel-devel、kernel-debuginfo
如果调用用户态进程,还需要该程序有调试符号,否则无法调试。
推荐使用最新稳定版的SystemTap,目前最新稳定版为:systemtap-2.9.tar.gz
5.入门
5.1 stap命令
stap [OPTIONS] FILENAME [ARGUMENTS]
Hello World:
root@j9 ~/stp# cat hello-world.stpprobe begin { print("===Hello World===\n")
5.2 staprun命令
staprun [OPTIONS] MODULE [MODULE-OPTIONS]
stap命令与staprun命令的区别在于:
stap命令的操作对象是stp文件或script命令等,而staprun命令的操作对象是编译生成的内核模块。
6.脚本语言
6.1 probe
“probe” <=> “探测”, 是SystemTap进行具体地收集数据的关键字。
“probe point” 是probe动作的时机,也称探测点。也就是probe程序监视的某事件点,一旦侦测的事件触发了,则probe将从此处插入内核或者用户进程中。
“probe handle” 是当probe插入内核或者用户进程后所做的具体动作。
probe用法:
probe probe-point { statement }
在Hello World例子中begin和end就是probe-point, statement就是该探测点的处理逻辑,在Hello World例子中statement只有一行print,statement可以是复杂的代码块。
探测点语法:
kernel.function(PATTERN)
PATTERN语法为:
func[@file]
例如:
kernel.function("*init*")module("ext3").function("*")kernel.statement("*@kernel/time.c:296")process("/home/admin/tengine/bin/nginx").function("ngx_http_process_request")
在return探测点可以用\$return获取该函数的返回值。
inline函数无法安装.return探测点,也无法用$return获取其返回值。
6.2 基本语法
SystemTap脚本语法比较简单,与C语言类似,只是每一行结尾";"是可选的。主要语句如下:
if/else、while、for/foreach、break/continue、return、next、delete、try/catch
其中:
next:主要在probe探测点逻辑处理中使用,调用此语句时,立刻从调用函数中退出。不同于exit()的是,next只是退出当前的调用函数,而此SystemTap并没有终了,但exit()则会终止SystemTap。
6.2.1 变量
不需要明确声明变量类型,脚本语言会根据函数参数等自动判断变量是什么类型的。
局部变量:在声明的probe和block(”{ }“范围内的部分)内有效。
全局变量:用”global“声明的变量,在此SystemTap的整个动作过程中都有效。全局变量的声明位置没有具体要求。需要注意的是,全局变量默认有锁保护,使用过多会有性能损失,如果用全局变量保存指针,可能出现指针所指的内容被进程修改,在探测点中拿不到真正的数据。
获取进程中的变量(全局变量、局部变量、参数)直接在变量名前面加$即可(后面会有例子)
6.2.2 注释
# ...... : Shell语言风格
6.2.3 操作符
比较运算符、算数运算符基本上与C语言一样,需要特别指出的是:
(1)、.操作符:连接两个字符串,类似于php;
(2)、=~和!~:正则匹配和正则不匹配;
6.2.4 函数
函数定义例子:
function indent:string (delta:long){
官方有很多很有用的函数,详情请参考:https://sourceware.org/systemtap/tapsets/
以及在本机安装了SystemTap之后在目录/usr/local/share/systemtap/tapset/下也可以看具体函数的实现以及一些奇特的用法。
7.技巧
7.1 定位函数位置
在一个大型项目中找出函数在哪里定义有时很有用,特别是一些比较难找出在哪里定义的函数,比如内核或者glibc中的某个函数想要看其实现时,首先得找出其在哪个文件的哪一行定义,用SystemTap一行命令就可以搞定。
比如要看printf在glibc中哪里定义的:
root@j9 ~# stap -l 'process("/lib/x86_64-linux-gnu/libc.so.6").function("printf")'
可以看出printf是在printf.c第29行定义的。
再比如要看内核中recv系统的调用是在哪里定义的:
root@j9 ~# stap -l 'kernel.function("sys_recv")'
可以看出recv是在socket.c第1868行定义的。
甚至可以*号来模糊查找:
root@j9 ~# stap -l 'kernel.function("*recv")'
同理,也可以用来定位用户进程的函数位置:
比如tengine的文件ngx_shmem.c里面为了兼容各个操作系统而实现了三个版本的ngx_shm_alloc,用#if (NGX_HAVE_MAP_ANON)、#elif (NGX_HAVE_MAP_DEVZERO)、#elif (NGX_HAVE_SYSVSHM)、#endif来做条件编译,那怎么知道编译出来的是哪个版本呢,用SystemTap的话就很简单了,否则要去grep一下这几宏有没有定义才知道了。
[root@cache4 tengine]# stap -l 'process("/home/admin/tengine/bin/nginx").function("ngx_shm_alloc")'
7.2 查看可用探测点以及该探测点上可用的变量
在一些探测点上能获取的变量比较有限,这是因为这些变量可能已经被编译器优化掉了,优化掉的变量就获取不到了。一般先用-L参数来看看有哪些变量可以直接使用:
[root@cache4 tengine]# stap -L 'process("/home/admin/tengine/bin/nginx").function("ngx_shm_alloc")'
可见在该探测点上可以直接使用$shm这个变量,其类型是ngx_shm_t*。
statement探测点也类似:
[root@cache4 tengine]# stap -L 'process("/home/admin/tengine/bin/nginx").statement("ngx_pcalloc@src/core/ngx_palloc.c:*")' process("/home/admin/tengine/bin/nginx").statement("ngx_pcalloc@src/core/ngx_palloc.c:395") $pool:ngx_pool_t* $size:size_tprocess("/home/admin/tengine/bin/nginx").statement("ngx_pcalloc@src/core/ngx_palloc.c:398") $pool:ngx_pool_t* $size:size_tprocess("/home/admin/tengine/bin/nginx").statement("ngx_pcalloc@src/core/ngx_palloc.c:399") $size:size_tprocess("/home/admin/tengine/bin/nginx").statement("ngx_pcalloc@src/core/ngx_palloc.c:404") $size:size_t $p:void*
7.3 输出调用堆栈
用户态探测点堆栈:print_ubacktrace()、sprint_ubacktrace()
内核态探测点堆栈:print_backtrace()、sprint_backtrace()
不带s和带s的区别是前者直接输出,后者是返回堆栈字符串。
这几个函数非常有用,在排查问题时可以根据一些特定条件来过滤函数被执行时是怎么调用进来的,比如排查tengine返回5xx时的调用堆栈是怎样的:
#cat debug_tengine_5xx.stp probe process("/home/admin/tengine/bin/nginx").function("ngx_http_finalize_request").call { if ($rc >= 500) {
比如看看内核是怎么收包的:
root@jusse ~# cat netif_receive_skb.stp
7.4 获取函数参数
一些被编译器优化掉的函数参数用-L去看的时候没有找到,这样的话在探测点里面也不能直接用$方式获取该参数变量,这时可以使用SystemTap提供的*_arg函数接口,*是根据类型指定的,比如pointer_arg是获取指针类型参数,int_arg是获取整型参数,类似的还有long_arg、longlong_arg、uint_arg、ulong_arg、ulonglong_arg、s32_arg、s64_arg、u32_arg、u64_arg:
root@j9 ~# stap -L 'kernel.function("sys_open")' kernel.function("SyS_open@/build/buildd/linux-lts-trusty-3.13.0/fs/open.c:1011") $ret:long int
再比如两个函数的函数参数类型兼容也可以使用这种方法获取:
这两个函数的参数完全兼容,只是第二个参数命名不一样而已,可以像下面这么用:
#cat debug_tengine_5xx.stp probe process("/home/admin/tengine/bin/nginx").function("ngx_http_finalize_request").call, process("/home/admin/tengine/bin/nginx").function("ngx_http_special_response_handler").call {
7.5 获取全局变量
有时候用$可以直接获取到全局变量,但有时候又获取不到,那可以试试@var:
比如获取nginx的全局变量ngx_cycyle:
root@j9 ~# cat get_ngx_cycle.stpprobe process("/home/admin/tengine/bin/nginx").function("ngx_process_events_and_timers").call { printf("ngx_cycle->connections: %d\n", $ngx_cycle->connections) exit()
7.6 获取数据结构成员用法
typedef struct { size_t len;
上面这个是nginx里面的http请求结构里面的几个成员,在C语言里,如果r是struct ngx_http_request_t *,那么要获取uri的data是这样的:r->uri.data,但在SystemTap里面,不管是指针还是数据结构,都是用->访问其成员:
#cat get_http_uri.stp
7.7 输出整个数据结构
SystemTap有两个语法可以输出整个数据结构:在变量的后面加一个或者两个
$即可,例子如下:
#cat get_r_pool.stpprobe process("/home/admin/tengine/bin/nginx").function("ngx_http_process_request").call {
其中r->pool的结构如下:
typedef struct {
ngx_pool_s包含了结构ngx_pool_data_t。变量后面加和$的区别是后者展开了里面的结构而前者不展开,此用法只输出基本数据类型的值。
7.8 输出字符串指针
用户态使用:user_string、user_string_n
内核态使用:kernel_string、kernel_string_n、user_string_quoted
#cat get_http_uri.stp
user_string_quoted是获取用户态传给内核的字符串,代码中一般有__user宏标记:
#cat sys_open.stpprobe kernel.function("sys_open")
7.9 指针类型转换
SystemTap提供@cast来实现指针类型转换,比如可以将void *转成自己需要的类型:
#cat get_c_fd.stp probe process("/home/admin/tengine/bin/nginx").function("ngx_http_process_request_line").call { printf("c->fd: %d\n", @cast($rev->data, "ngx_connection_t")->fd)
7.10 定义某个类型的变量
同样是用@cast,定义一个变量用来保存其转换后的地址即可,用法如下:
#cat get_c.stp
7.11 多级指针用法
root@j9 ~# cat cc_multi_pointer.c
简言之:通过[0]去解引用即可。
7.12 遍历C语言数组
下面是在nginx处理请求关闭时遍历请求头的例子:
#cat debug_http_header.stp
7.13 查看函数指针所指的函数名
获取一个地址所对应的符号:
用户态:usymname
内核态:symname
#cat get_c_handler.stp
7.14 修改进程中的变量
root@j9 ~# cat stap_set_var.c -n
可以看出在第17行用SystemTap修改后的值在第19行就生效了。
需要注意的是stap要加-g参数在guru模式下才能修改变量的值。
7.15 跟踪进程执行流程
thread_indent(n): 补充空格
ppfunc(): 当前探测点所在的函数
在call探测点调用thread_indent(4)补充4个空格,在return探测点调用thread_indent(-4)回退4个空格,效果如下:
#cat trace_nginx.stp
7.16 查看代码执行路径
pp(): 输出当前被激活的探测点
#cat ngx_http_process_request.stpprobe process("/home/admin/tengine/bin/nginx").statement("ngx_http_process_request@src/http/ngx_http_request.c:*") {
可以看出该函数哪些行被执行了。
7.17 巧用正则匹配过滤
在排查问题时,可以利用一些正则匹配来获取自己想要的信息,比如下面是只收集*.j9.com的堆栈:
#cat debug_tengine_5xx.stp
7.18 关联数组用法
SystemTap的关联数组必须是全局变量,需要用global进行声明,其索引可以支持多达9项索引域,各域间以逗号隔开。支持 =, ++ 与 +=操作,其默认的初始值为0。
例如:
root@j9 ~# cat stap_array.stp
也可以用+、-进行排序:
root@j9 ~# cat stap_array.stp
7.19 调试内存泄漏以及内存重复释放
probe begin { printf("=============begin============\n")
详细请看:http://blog.csdn.net/wangzuxi/article/details/44901285
7.20 嵌入C代码
在进程fork出子进程时打印出进程id和进程名:
root@jusse ~/systemtap# cat copy_process.stp
有三个需要注意的地方:
1)、SystemTap脚本里面嵌入C语言代码要在每个大括号前加%前缀,是%{…… %} 而不是%{ …… }%;
2)、获取脚本函数参数要用STAP_ARG_前缀;
3)、一般long等返回值用STAP_RETURN,而string类型返回值要用snprintf、strncat等方式把字符串复制到STAP_RETVALUE里面。
7.21 调试内核模块
这小节就不细讲了,这篇博客 (http://blog.chinaunix.net/uid-14528823-id-4726046.html) 写得很详细,这里只copy两个关键点过来记录一下:
要调试自己的内核模块,需要注意的有两个关键点:
1)、使用SystemTap调试内核模块,探测点的编写格式示例为:
module("ext3").function("ext3_*")
2)、需要将自己的模块cp到/lib/modules/uname -r/extra目录中,否则找不到符号,如果/lib/modules/uname -r/目录下没有extra这个目录,自己mkdir一下就可以。
7.22 一些错误提示及解决办法
错误提示1:
ERROR: MAXACTION exceeded near keyword at debug_connection.stp:86:9ERROR: MAXACTION exceeded near operator '->' at debug_connection.stp:84:30
解决办法:
加上stap参数:-DMAXACTION=102400,如果还报这种类型的错误,只需把102400调成更大的值即可。
错误提示2:
WARNING: Number of errors: 0, skipped probes: 82
解决办法:
加上-DMAXSKIPPED=102400和-DSTP_NO_OVERLOAD参数
还有一些可以去掉限制的宏:
MAXSTRINGLEN:这个宏会影响sprintf的buffer大小,默认为512字节。
MAXTRYLOCK:对全局变量进行try lock操作的次数,超过则次数还拿不到锁则放弃和跳过该探测点,默认值为1000.全局变量多的时候可以把这个宏开大一点。