Day 5:ply 語法簡介

之前只示範了如何用 ply 追蹤函數呼叫的堆疊。接下來講解更詳敘的語法,以達成更有效的追蹤。 ply 的語法在這個專案的文件 中可以找到。在這個專案的 README 中也有一些例子。

前言

雖然有文件可以參考,但要注意的是文件有的地方是過時的。比如說裡面有用到 SyS 來表示追蹤的是系統呼叫,像是文件中的第一個例子:

kprobe:SyS_*
{
    @syscalls[caller] = count();
}

假定這個檔案叫做 syscall_count.ply,如果用最新版的 ply 執行的話,就會發現下面的錯誤:

$ sudo ply syscall_count.ply 
ERR:-22

相關的議題可以在這個 issue#59 中看到。除此之外,tracepoint 目前看來也有些問題,可以參考issue#46。不過 kprobe 看起來沒有什麼問題,所以接下來的例子會以 kprobe 為主。

ply 執行腳本方式可以直接在命令列用 ply 執行,比如說 ply 'kretprobe:vfs_read { @["size"] = quantize(retval); },或是把後面 '...' 的部分獨立成一個檔案:

kretprobe:vfs_read
{
    @["size"] = quantize(retval);
}

然後叫 ply 執行他。比如說假定上面這個檔案叫做 vfs.ply,那就直接執行:

$ ply vfs.ply

基本架構

基本的架構跟 awk 有點像。最基本的樣子是:

kprobe:<name>
{
    /* stuffs to do */
}

其中,<name> 是想要放 kprobe 的地方,而大括號中間是要做的事情。比如說這個 README 中的例子:

kprobe:do_sys_open
{
    printf("%v(%v): %s\n", comm, uid, str(arg1));
}

這個例子會追蹤所有的 open 系統呼叫。如果行程呼叫了這個系統呼叫,就會把行程的名稱 (comm)、uid (uid),以及這個系統呼叫的第 1 個參數 (按:參數由編號 0 開始,依次往上。也就是 arg0arg1arg2 ... argN 一直下去。為了方便,這邊就直接用「第 N 個參數」來稱呼 argN)。

另外一個例子是原來的 execsnoop.ply 。可以把這個例子稍微修改,使得可以在目前的 Raspberry Pi OS 上運作:

kprobe:sys_execve {
    execs[kpid] = str(arg0, 48);
}

kretprobe:sys_execve {
    printf("(%4u) %v %3ld\n", uid, execs[kpid], retval);
}

這個例子中的 exec[] 稱為一個 map,是一個 key-value pair,會以 [] 中的東西為 key,= 右側的東西為 value 儲存。而 kretprobe 則是在對應的函數「回傳」的時候被觸發。在使用 kretprobe 時,可以用 retval 這個變數來得到回傳值。

內建變數

有一些內建的變數可以使用。比如說 comm 表示現在這個行程的名稱、pid 就表示行程 id、stack 表示目前的呼叫堆疊等等。除此之外,可以利用 arg0arg1 ... argN 來表示這個函數中的第 N 個參數的值 (argN 就表示第 N 個參數)。time 則是 timestamp,在計算延遲時也滿方便的。

Aggregations

用來統計結果。用法是 @[expr],其中 <name> 是這個統計結果的名稱。這個東西右邊只能有放兩種東西:一個是 count(),另外一個是 quantize(scalar-expr)。前者就是統計數字,統計每個 expr 的結果出現了幾次。比如說統計呼叫某個函數的路徑:

kprobe:gpiod_*
{
    @[stack] = count();
}

這個統計的是「所有名字是 gpiod_ 開頭的函數被呼叫時,那個堆疊的路徑出現了幾次」:

@:
{ 
    gpiod_set_value_nocheck
    gpio_led_set+120
    gpio_led_set_blocking+24
    set_brightness_delayed+140
    process_one_work+592
    worker_thread+96
    kthread+368
    ret_from_fork+20
 }: 10
{ 
    gpiod_set_raw_value_commit
    gpiod_set_value_cansleep+56
    gpio_led_set+120
    gpio_led_set_blocking+24
    set_brightness_delayed+140
    process_one_work+592
    worker_thread+96
    kthread+368
    ret_from_fork+20
 }: 10
{ 
    gpiod_set_value_cansleep
    gpio_led_set_blocking+24
    set_brightness_delayed+140
    process_one_work+592
    worker_thread+96
    kthread+368
    ret_from_fork+20
 }: 10

而後者則是畫出區間分佈圖。比如說下面這個程式

kretprobe:vfs_read
{
    @["size"] = quantize(retval);
}

這個就是在統計「vfs_read 的回傳值區間分佈圖」。執行完之後結果像下面這樣:

@:
{ size    }: 
             < 0           1 ┤▏                               │
    [   0,    1]          36 ┤██▌                             │
    [   2,    3]           3 ┤▎                               │
    [   4,    7]          15 ┤█                               │
    [   8,   15]           4 ┤▎                               │
    [  16,   31]           4 ┤▎                               │
    [  32,   63]         192 ┤█████████████▎                  │
    [  64,  127]           1 ┤▏                               │
    [ 128,  255]           6 ┤▍                               │
    [ 256,  511]           5 ┤▍                               │
    [ 512,   1k)          89 ┤██████▏                         │
    [  1k,   2k)          94 ┤██████▌                         │
    [  2k,   4k)          14 ┤█                               │

Filter

除了這個之外,也可以只「過濾」出滿足特定條件的事件。這個用法就是在 kprobe 的最後面加上 / ... /:

kprobe:<name>/filter_expr/
{
    /* stuffs to do */
}

filter_expr 為真時,才會觸發接下來的程式片段。這在追蹤特定的可執行檔的行為時很有用。比如說下面這個程式統計名稱為 hello 的可執行檔在執行時,進行了哪些系統呼叫:

kprobe:__se_sys_* / !strcmp(execname, "hello") /
{
    @[caller] = count();
}

結果類似以下:

@:
{ __se_sys_exit_group }: 1
{ __se_sys_newuname }: 1
{ __se_sys_access }: 1
{ __se_sys_write }: 1
{ __se_sys_readlink }: 1
{ __se_sys_munmap }: 2
{ __se_sys_brk }: 3
{ __se_sys_openat }: 4
{ __se_sys_close }: 4
{ __se_sys_fstat64 }: 5
{ __se_sys_mprotect }: 6
{ __se_sys_mmap_pgoff }: 8
{ __se_sys_llseek }: 24
{ __se_sys_read }: 26

filtr_expr 也可以是其他 map,這樣就可以有「傳接球」的方式來統計特定資料傳遞需要花費的時間。比如說可以拿 skb 當作 key,紀錄他到達某一個函數的時間; 然後再去另外一個參數有 skb 的函數,看他什麼時候收到這個被其他人收過的 skb

kprobe:__netif_receive_skb_core
{
    rx[arg0] = time;
}

kprobe:ip_rcv / rx[arg0] /
{
    @["diff"] = quantize(time - rx[arg0]);
}