Linux 調試: systemtap


安裝與配置

在ubuntu下直接用apt-get install之后不能正常使用,提示缺少調試信息或者編譯探測代碼時有問題。

1. 采用官網上的解決方法

2. 可以自己重新編譯一次內核,然后再手工編譯一次systemtap。這樣就可以正常使用了。

 

Systemtap的編譯說明,除了下載地址並沒有說太多東西。選擇一個版本,自己選擇了最新的2.7.

下載后解壓,執行

./configure

一般來說會提示缺少組件。Systemtap最先應該是redhat開發的,所以需要的包名稱ubuntu不能直接用來apt-get

列出幾個自己碰到的依賴問題:

configure: error: missing gnu /usr/bin/msgfmt

apt-get install gettext

 

configure: error: missing elfutils development headers/libraries (install elfutils-devel, libebl-dev, libdw-dev and/or libebl-devel)

可以通過apt-get install libdw-dev解決

 

configure: error: in `/root/systemtap-2.7':
configure: error: C++ preprocessor "/lib/cpp" fails sanity check

安裝apt-get install g++

 使用用例

 基本使用

詳見systemtap 官方tutorial。這里做個筆記。

hello world

把systemtap腳本轉換編譯為內核模塊然后執行預定義的動作,定義的動作由一系列的事件觸發。用戶可以指定在哪些事件上觸發哪些指定的動作。下面是一個systemtap的helloworld,在模塊裝載即在腳本運行前執行一次

root@userver:~# stap hello-world.stp 
hello world
root@userver:~# cat hello-world.stp 
probe begin
{
    print ("hello world\n")
    exit ()
}

如果打開-v選項的話,可以看到執行的詳細步驟:

root@userver:~# stap -v hello-world.stp 
Pass 1: parsed user script and 106 library script(s) using 66544virt/37432res/4324shr/33908data kb, in 120usr/10sys/127real ms.
Pass 2: analyzed script: 1 probe(s), 1 function(s), 0 embed(s), 0 global(s) using 67204virt/38136res/4512shr/34568data kb, in 0usr/0sys/4real ms.
Pass 3: translated to C into "/tmp/stapyZxhXI/stap_847497c1de7927412685a2282f37c57d_881_src.c" using 67204virt/39028res/5232shr/34568data kb, in 0usr/0sys/0real ms.
Pass 4: compiled C into "stap_847497c1de7927412685a2282f37c57d_881.ko" in 1000usr/590sys/1582real ms.
Pass 5: starting run.
helloworld
Pass 5: run completed in 10usr/20sys/472real ms.

如果多次運行同一個腳本的話快很多,因為systemtap直接使用了已經編譯好的緩存模塊文件。

還可以定時運行一定時間:

root@userver:~# stap strace-open.stp 
cat(24307) open ("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC)
cat(24307) open ("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC)
cat(24307) open ("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC)
cat(24307) open ("iw.c", O_RDONLY)
root@userver:~# cat strace-open.stp 
probe syscall.open 
{
    printf("%s(%d) open (%s)\n", execname(), pid(), argstr);
}
probe timer.ms(4000) # after 4 seconds
{
    exit()
}

在systemtap運行期間執行了一個cat命令得到的結果,腳本記錄了執行系統調用open的進程信息。

 如何跟蹤

跟蹤點選擇

begin The startup of the systemtap session.
end The end of the systemtap session.
kernel.function("sys_open") The entry to the function named sys_open in the kernel.
syscall.close.return The return from the close system call.
module("ext3").statement(0xdeadbeef) The addressed instruction in the ext3 filesystem driver.
timer.ms(200) A timer that fires every 200 milliseconds.
timer.profile A timer that fires periodically on every CPU.
perf.hw.cache_misses A particular number of CPU cache misses have occurred.
procfs("status").read A process trying to read a synthetic file.
process("a.out").statement("*@main.c:200") Line 200 of the a.out program.

全局:probe begin {}, probe end {}用於整個跟蹤過程的開頭和結尾。

函數: kernel.function("sys_open"){}用於在某個指定的內核函數中執行定義的動作,sys_open可以換成其他的函數如ext4_release_file(在文件close時會執行)

系統調用:syscall.close在執行close調用時執行,其他系統調用也是類似。因為系統調用的函數是通過宏定義實現的

修飾:

       內聯:kernel.function("xx").inline {} 指定函數被內聯時進入

       調用:kernel.function("xx").call {} 指定函數被調用是進入(不含內聯)

       返回:kernel.function("").return{}可以在該函數返回時執行。

格式輸出

printf %s表示字符串,%d表示數值類型

tid() The id of the current thread.
pid() The process (task group) id of the current thread.
uid() The id of the current user.
execname() The name of the current process.
cpu() The current cpu number.
gettimeofday_s() Number of seconds since epoch.
get_cycles() Snapshot of hardware cycle counter.
pp() A string describing the probe point being currently handled.
ppfunc() If known, the the function name in which this probe was placed.
$$vars If available, a pretty-printed listing of all local variables in scope.
print_backtrace() If possible, print a kernel backtrace.
print_ubacktrace() If possible, print a user-space backtrace.

1. print_backtrace比較實用可以打印內核的調用棧

2. gettimeofday_s用於獲得以秒為單位的時間,gettimeofday_ms則是以毫秒為單位的時間,gettimeofday_us

3. thread_indent用於進程/線程輸出時的縮進相當於一個thread_local變量,參數表示作用在該變量上的一個增量,進入一個函數時參數為正值,退出時為負值,就可以產生函數調用的縮進效果,下面是一個類似tutorial上的示例:

probe kernel.function("*@fs/open.c").call {
    printf("%s -> %s(%s)\n", thread_indent(4), ppfunc(), $$parms);
}

probe kernel.function("*@fs/open.c").return {
    printf("%s <- %s\n", thread_indent(-4), ppfunc());
}

部分輸出:

0 prltoolsd(1230):    -> SyS_open(filename=0x40fa78 flags=0x0 mode=0x6118a0)
     7 prltoolsd(1230):        -> do_sys_open(dfd=0xffffffffffffff9c filename=0x40fa78 flags=0x8000 mode=0x18a0)
    20 prltoolsd(1230):            -> finish_open(file=0xffff88002ff1a000 dentry=0xffff88009a978840 open=0x0 opened=0xffff88002f89fdec)
    24 prltoolsd(1230):                -> do_dentry_open(f=0xffff88002ff1a000 open=0x0 cred=0xffff880140efe300)
    29 prltoolsd(1230):                    -> generic_file_open(inode=0xffff88002f71a820 filp=0xffff88002ff1a000)
    31 prltoolsd(1230):                    <- generic_file_open
    33 prltoolsd(1230):                <- do_dentry_open
    34 prltoolsd(1230):            <- finish_open
    36 prltoolsd(1230):            -> open_check_o_direct(f=0xffff88002ff1a000)
    38 prltoolsd(1230):            <- open_check_o_direct
    41 prltoolsd(1230):        <- do_sys_open
    42 prltoolsd(1230):    <- SyS_open
     0 prltoolsd(1230):    -> SyS_close(fd=0x5)
     6 prltoolsd(1230):        -> filp_close(filp=0xffff88002ff1a000 id=0xffff880148cb5c80)
    15 prltoolsd(1230):        <- filp_close
    17 prltoolsd(1230):    <- SyS_close

 更實用的例子

 分析執行

變量默認的都是局部變量,即每個處理函數內的變量是不共享的。使用全局變量的話,要在開始使用global關鍵字進行定義。變量是弱類型的,可以相互轉換但是要手工顯式進行。字符串使用.連接和php與perl一樣。流程控制語句和C語言基本一致。下面是tutorial中的一個例子:

global count_jiffies, count_ms;

probe timer.jiffies(100) {
    count_jiffies++;
}

probe timer.ms(100) {
    count_ms++;
}

probe timer.ms(10000) {
    hz = (1000 * count_jiffies) / count_ms;
    printf("jiffies:ms ratio: %d:%d = %d\n", count_jiffies, count_ms, hz);
}

 

目標變量

這些變量在跟蹤點處理函數所在的上下文種獲取,可以直接使用被跟蹤函數的參數變量等。下面是一個示例:

probe kernel.function("filp_close") {
    printf("%s %d: %s(%s:%d)\n", 
        execname(), 
        pid(), 
        ppfunc(), 
        kernel_string($filp->f_path->dentry->d_iname),
        $filp->f_path->dentry->d_inode->i_ino);    
}

輸出如下:

bash 1724: filp_close(:24831)
bash 1724: filp_close(:24831)
bash 31781: filp_close(:24831)
bash 31781: filp_close(:24831)
a.out 31781: filp_close(1:4)
a.out 31781: filp_close(ld.so.cache:788003)
a.out 31781: filp_close(libc-2.19.so:3936539)
a.out 31781: filp_close(data.out:1460185)
a.out 31781: filp_close(1:4)
a.out 31781: filp_close(1:4)
a.out 31781: filp_close(1:4)

函數

函數定義function name(arg1, arg2) { return somthing},跟javascript里差不多。

 

數組

systemtap里的數組實際上就是一個hashmap,還支持多維hash(hashmap[key1, key2...] = value),但是需要預先定義容量,當已有的元素超過容量時會報錯:

global hashmap[3]
global multimap[3]

global countmap[5]

probe begin {
    hashmap[1] = "a";
    hashmap[3] = "c";
    hashmap[100] = "last";

        # 
    # ERROR: Array overflow, check size limit (3) near identifier 'hashmap' at array-demo.stp:8:2
    # hashmap[222] = "excced."
    #

    multimap[1,"init"] = "important"
    multimap[0, "swap"] = "more import"


    for (i = 0; i<5; i++) {
        countmap[i] = i * 10;
    }
}

probe timer.ms(1000) {
    exit();
}

probe end {

    printf("-----------------------------\n")
    printf("exist: %s, %s, %s\n", hashmap[1], hashmap[3], hashmap[100]);
    printf("!exist: %s\n", hashmap[121]);
    printf("-----------------------------\n")
    printf("exist: %s\n", multimap[1, "init"]);
    printf("!exist: %s\n", multimap[1, "haha"]);
    printf("--------------sorted by key inc[default]-------------\n")
    foreach([a] in countmap) {
        printf("countmap[%d] = %d\n", a, countmap[a]);
    }
    printf("--------------sorted by key desc-------------\n")
    foreach([a-] in countmap) {
        printf("countmap[%d] = %d\n", a, countmap[a]);
    }
    printf("--------------sorted by value desc-------------\n")
    foreach([a] in countmap-) {
        printf("countmap[%d] = %d\n", a, countmap[a]);
    }
    
}

foreach 語法默認對hashmap中的key進行一個升序的迭代,如果要改變方向可以在key后加個減號,如果需要按值升降序迭代則在hashmap數組名稱后加符號。單個key時foreach中的[]可以省略。

 統計聚合

 聚合變量操作可以使用<<<對變量進行增量,按照tutorial的解釋這個變量是分布在各個CPU特有的關聯空間所以可以減少競爭,然后使用@avg(增量值的平均),@sum(增量值的累加),@count(增量執行次數)函數進行聚合,不能直接訪問。

global hitcount[10000];

probe kernel.function("__schedule") {
    hitcount[execname()] <<< 1;
}

probe timer.ms(10000) {
    exit();
}

probe end {
    foreach (prog in hitcount) {
        printf("%15s : %-6d\n", prog, @count(hitcount[prog]));
    }
}

運行結果:

root@userver:~/stp# stap schedule-stat.stp 
      swapper/0 : 2     
  rs:main Q:Reg : 40    
      rcu_sched : 7     
   kworker/0:1H : 6     
    kworker/0:2 : 10    
     watchdog/0 : 2     
        rcuos/1 : 3     
    kworker/1:1 : 10    
  systemd-udevd : 3     
      swapper/1 : 579   
        rcuos/0 : 2     
  kworker/u64:0 : 12    
    jbd2/sda1-8 : 7     
    migration/1 : 1     
     khugepaged : 1     
      prltoolsd : 40    
     watchdog/1 : 2     
      in:imklog : 446   
         stapio : 51    
    ksoftirqd/1 : 27    

 每次執行都是+1的話不能體現出這些聚集函數的作用,對於每次增量是不同的需求,聚合函數就非常的有用。另外一個例子用來統計調用vfs_read的數據量:

global data_count[10000]

probe begin {
    print("start profiling.");
}

probe kernel.function("vfs_read") {
    data_count[execname()] <<< $count;
}

probe timer.ms(1000) {
    print(".");
}

probe timer.ms(20000) { # 20 seconds
    exit();
}

probe end {
    print("\n");
    foreach (prog in data_count) {
        printf("%15s : avg:%-8d cnt:%-12d sum:%-12d\n", 
            prog,
            @avg(data_count[prog]),
            @count(data_count[prog]),
            @sum(data_count[prog]));
    }
}

輸出:

root@userver:~/stp# stap vfs-read-stat.stp
start profiling.....................
            top : avg:1050     cnt:1034         sum:1086553     
      in:imklog : avg:8095     cnt:3398         sum:27510134    
           sshd : avg:16384    cnt:20           sum:327680      
         stapio : avg:129057   cnt:122          sum:15745032    
          acpid : avg:24       cnt:46           sum:1104        
           bash : avg:88       cnt:9            sum:793         
  systemd-udevd : avg:128      cnt:2            sum:256       

 Tapset

tapset是一些systemtap腳本文件,存在於/usr/share/systemtap/tapset。

符號選擇

當用戶執行腳本時如果發現符號沒定義那么會在tapset內進行搜索,其中還有些文件夾,其名稱代表了kernel體系架構名稱或者kernel版本名稱。搜索匹配是具體到泛化的過程,跟路由IP選擇一樣,如果有精確的選擇則優先選擇可以精確匹配的,不行則在采用一般腳本中的定義,如都沒找到則報錯。不過不知為什么按着tutorial上的做依然提示找不到。。。

跟蹤點別名

global groups

probe syscallgroup.io = 
    syscall.open, syscall.close, syscall.read, syscall.write 
{
    groupname = "io";    
}

probe syscallgroup.process = 
    syscall.fork, syscall.execve
{
    groupname = "process"
}

probe syscallgroup.* {
    groups[pid(), execname() . "/" . groupname]++;
}

probe end {
    foreach ([id, eg+] in groups) {
        printf("%5d %-20s %d\n", id, eg, groups[id, eg])
    }
}

嵌入C代碼

用戶腳本中嵌入C語言的腳本,在運行時需要使用-g選項。

  1. Do not dereference pointers that are not known or testable valid. (不要隨意對指針解引用)
  2. Do not call any kernel routine that may cause a sleep or fault. (不要調用那些會引起阻塞或者睡眠的函數)
  3. Consider possible undesirable recursion, where your embedded C function calls a routine that may be the subject of a probe. If that probe handler calls your embedded C function, you may suffer infinite regress. Similar problems may arise with respect to non-reentrant locks.   (不要調用會引起自身腳本無限循環的調用)
  4. If locking of a data structure is necessary, use a trylock type call to attempt to take the lock. If that fails, give up, do not block.(獲取鎖時先用trylock類型的調用嘗試)

頭文件可以使用

%{ %}方式在腳本開頭引入。

function get_msg:string (id:long) %{
    snprintf(STAP_RETVALUE, MAXSTRINGLEN, "helloworld %ld\n(%d)\n", (long)STAP_ARG_id, MAXSTRINGLEN);
%}

probe begin {
    print(get_msg(123));
}

輸出:

# stap -g embedded-c.stp 
helloworld 123
(512)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM