s-systemtap工具使用图谱(持续更新)
整體的學習思維導圖如下,后續持續更新完善
文章目錄
- 安裝
- 簡介
- 執行流程
- 執行方式
- 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*
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/stapprobe 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/stapprobe 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/stapprobe 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/stapprobe 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/stapprobe 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/stapprobe 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/stapprobe 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/stapprobe 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 probe0 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/stapprobe begin
{printf("begin to probe\n");
}probe module("ceph").function("*@mds_client.c").call{if(target() == pid()) {//使用target過濾我們輸入的進程ipprintf("%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信號的進程
總結
以上是生活随笔為你收集整理的s-systemtap工具使用图谱(持续更新)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我39级2w战斗力有救么
- 下一篇: 分布式存储(ceph)技能图谱(持续更新