SystemTapの使い方(グルモード編)

#1 グルモード(guru)とは?
グルモードを使うと、systemtapのスクリプトにC言語のソースコードを埋め込むことができます。
systemtapスクリプトだけでは実現できないことができるようになります。
なお、systemtapのインストール、基本的な使い方は、[ここ(SystemTapの使い方)](https://qiita.com/hana_shin/items/9b265b4f9a51f98d0f4d)を参照してください。


#2 環境
VMware Workstation 14 Playerで作成した仮想マシンを使用しました。

```
[root@server ~]# cat /etc/redhat-release
CentOS Linux release 7.6.1810 (Core)

[root@server ~]# uname -r
3.10.0-957.el7.x86_64
```

#3 基本的な使い方
C言語の部分は`%{`と`%}`で囲みます。
関数への引数は、`STAP_ARG_`と引数を連結した形で使用します。
以下の例では、引数`x`と`y`を`STAP_ARG_x`と`STAP_ARG_y`というかたちで使用しています。
また、関数の戻り値は、STAP_RETVALUEを使用しています。

```console:スクリプトの内容
[root@server stap]# cat ping.stp
#!/usr/bin/stap

private function add_param:long(x:long, y:long) %{
  int sum;
  sum = STAP_ARG_x + STAP_ARG_y;
  STAP_RETVALUE = sum;
%}

probe kernel.function("ip_output@net/ipv4/ip_output.c")
{
  if( execname() == "ping") {
    sum = add_param(100,200);
    printf("pp=%s,sum=%d\n",pp(),sum);
  }
}
```

guruモードを使うので、`-g`オプションを付けて、systemtapを実行します。

```console:systemtapの実行
[root@server stap]# stap -vg ping.stp
```

デフォルトGWに対してpingを1回実行しました。
なお、pingの使い方は、[ここ(pingコマンドの使い方)](https://qiita.com/hana_shin/items/c31b0d05a91244c4db83)を参照してください。

```console:pingの実行
[root@server stap]# ping -c 1 192.168.3.1
```

guruモードとして定義した関数の戻り値300が出力されていることがわかります。

```console:systemtapの実行結果
[root@server stap]# stap -vg ping.stp
-snip-
pp=kernel.function("ip_output@net/ipv4/ip_output.c:348"),sum=300
```

#4 syslogにメッセージを出力する方法
プローブ(ip_output関数)を実行したら、syslogにメッセージを出力してみます。

なお、下記スクリプト中のprintkは、カーネルがsyslogに
メッセージを出力するときに使う関数です。

```console:スクリプトの内容
[root@server stap]# cat ping.stp
#!/usr/bin/stap

private function output_log() %{
  printk(KERN_ERR "Hello");
%}

probe kernel.function("ip_output@net/ipv4/ip_output.c")
{
  if(execname() == "ping") {
    output_log();
    printf("pp=%s\n",pp());
  }
}
```

syslogへのログ出力を確認するため、journalctlコマンドを実行します。

```console:journalctlコマンドの実行
[root@server ~]# journalctl -f
```

guruモードを使うので、`-g`オプションを付けて、systemtapを実行します。

```console:systemtapの実行
[root@server stap]# stap -vg ping.stp
```

デフォルトGWに対してpingを1回実行しました。

```console:pingの実行
[root@server stap]# ping -c 1 192.168.3.1
```

syslogにメッセージが出力されたことがわかります。

```console:journalctlコマンドの実行
[root@server ~]# journalctl -f
 9月 27 19:21:36 server kernel: Hello
```

#5 syslogにjiffiesを出力する方法
jiffiesは、カーネルが使用する変数です。
カーネルが起動してからの相対時間を表します。1ミリ秒ごとにカウントアップされます。
ここでは、プローブを実行したら、その時のjiffiesの値をsyslogに出力してみます。

```console:スクリプトの内容
[root@server stap]# cat ping.stp
#!/usr/bin/stap

private function output_log() %{
  printk(KERN_ERR "jiffies=%ld\n", jiffies);
%}

probe kernel.function("ip_output@net/ipv4/ip_output.c")
{
  if(execname() == "ping") {
    output_log();
    printf("pp=%s\n",pp());
  }
}
```

syslogへのログ出力を確認するため、journalctlコマンドを実行します。

```console:journalctlコマンドの実行
[root@server ~]# journalctl -f
```

guruモードを使うので、`-g`オプションを付けて、systemtapを実行します。

```console:systemtapの実行
[root@server stap]# stap -vg ping.stp
```

デフォルトGWに対してpingを1回実行しました。

```console:pingの実行
[root@server stap]# ping -c 1 192.168.3.1
```

syslogにjiffiesの値が出力されたことがわかります。
プローブを実行したときのjiffiesの値が4299765913であることがわかります。

```console:systemtapの実行結果
[root@server stap]# stap -vg ping.stp
-snip-
 9月 27 19:39:16 server kernel: jiffies=4299765913
```


#6 文字列を返す方法
pingを実行すると、ICMP echoパケットが送信されます。

ICMP echoパケットのルーティング処理が実行されると、
sk_buff構造体が示すnet_device構造体のnameメンバに、
ICMP echoパケットを出力するデバイス名が設定されます。
以下の例では、nameメンバにeth0が設定されています。

```console:データ構造
       sk_buff構造体
 skb-> +----------+            net_device構造体
       |   *dev   | --------> +---------------+ <- dev
       |          |           |   name(eth0)  |
       |          |           |               |
       |          |           +---------------+
       |          |
       |          |
       |          |            icmp echoパケット
       |          | --------> +---------------+
       +----------+           |               |
                              |               |
                              |               |
                              +---------------+
```

ip_finish_output関数を実行した際、ICMP echoパケットがどのデバイスから
送信されるのかを確認するスクリプトを以下に示します。

なお、この例は、わざわざグルモードを使わなくても、実現可能です。
デバイス名の文字列を返す関数の使い方を確認したかったので、
グルモードを使ってみました。

なお、下記スクリプト中のIFNAMSIZは16と定義されています。

```console:スクリプトの内容
[root@server stap]# cat ping.stp
#!/usr/bin/stap

%{
#include <linux/netdevice.h>
%}

private function show_dev:string(skb:long) %{
  struct sk_buff *skb;
  struct net_device *dev;

  skb = (struct sk_buff *)STAP_ARG_skb;
  dev = skb->dev;
  snprintf(STAP_RETVALUE, IFNAMSIZ, "%s", dev->name);
%}

probe kernel.function("ip_finish_output@net/ipv4/ip_output.c")
{
  if(execname() == "ping") {
    name = show_dev($skb);
    printf("pp=%s,dev_name=%s\n",pp(),name);
  }
}
```

guruモードを使うので、`-g`オプションを付けて、systemtapを実行します。

```console:systemtapの実行
[root@server stap]# stap -vg ping.stp
```

デフォルトGWに対してpingを1回実行しました。

```console:pingの実行
[root@server stap]# ping -c 1 192.168.3.1
```

eth0という名前のデバイスが出力されていることがわかります。

```console:systemtapの実行結果
[root@server stap]# stap -vg ping.stp
-snip-
pp=kernel.function("ip_finish_output@net/ipv4/ip_output.c:267"),dev_name=eth0
```

#7 統計情報をカウントアップする方法
カーネル内で使用している統計情報を任意の場所でカウントアップしてみます。
どのような事にやくに立つのか不明ですが。

ここでは、icmp_rcv関数が実行されたら、ICMP_MIB_INERRORSという統計情報を
カウントアップしてみます。
ICMP_MIB_INERRORSという統計情報は、本来、以下のようにエラー発生時に
カウントアップするためのものですが、icmp_rcv関数が呼び出されたら、
ICMP_MIB_INERRORSをカウントアップしてみます。


```console:カーネルソースコード(icmp.cより抜粋）
/*
 *      Deal with incoming ICMP packets.
 */
int icmp_rcv(struct sk_buff *skb)
{

-中略-

error:
        ICMP_INC_STATS_BH(net, ICMP_MIB_INERRORS);
        goto drop;
}
```

icmp_rcv関数が呼ばれたら、統計情報をカウントアップするスクリプトです。

```coinsole:スクリプト
[root@server stap]# cat ping.stp
#!/usr/bin/stap

%{
#include <net/ip.h>
#include <net/icmp.h>
#include <linux/skbuff.h>
%}

private function inc_stat(tmp:long) %{
  struct sk_buff *tmp,*skb;
  struct rtable *rt;
  struct net *net;

  skb = (struct sk_buff *)STAP_ARG_tmp;
  rt = skb_rtable(skb);
  net = dev_net(rt->dst.dev);
  ICMP_INC_STATS_BH(net, ICMP_MIB_INERRORS);
%}

probe kernel.function("icmp_rcv@net/ipv4/icmp.c")
{
  inc_stat($skb);
  printf("pp=%s\n",pp());
}
```

クライアントからサーバにpingを実行すると、統計情報がカウントアップするのがわかります。
なお、nstatの使い方は、[ここ(nstatコマンドの使い方)](https://qiita.com/hana_shin/items/566eaff2a5bfb45a049a)を参照してください。

```console:統計情報の確認
[root@server ipv4]# nstat -a|grep IcmpInErrors
IcmpInErrors                    1                  0.0
[root@server ipv4]# nstat -a|grep IcmpInErrors
IcmpInErrors                    2                  0.0
[root@server ipv4]# nstat -a|grep IcmpInErrors
IcmpInErrors                    3                  0.0
```

#Z 参考情報
[SystemTap Language Reference](https://sourceware.org/systemtap/langref.pdf)