hana_shinのLinux技術ブログ

Linuxの技術情報を掲載しています。特にネットワークをメインに掲載していきます。

SystemTapの使い方(User-Space Probing)



1 User-Space Probingとは?

SystemTapカーネルだけでなく、ユーザプログラムに対しても使うことができます。ここでは、ユーザプログラムに対するSystemTapの使い方を説明します。

他にもSystemTapの記事を作成したので参考にしてください。
SystemTapの使い方 - hana_shinのLinux技術ブログ

2 検証環境

AlmaLinuxの版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 8.6 (Sky Tiger)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
4.18.0-372.9.1.el8.x86_64

systemtapパッケージをインストールします。

[root@server ~]# dnf -y install systemtap

stapコマンドの版数を確認します。

[root@server ~]# stap -V
Systemtap translator/driver (version 4.7/0.186/0.187, rpm 4.7-1.el8)
Copyright (C) 2005-2022 Red Hat, Inc. and others
This is free software; see the source for copying conditions.
tested kernel versions: 2.6.32 ... 5.18-rc3
enabled features: AVAHI BOOST_STRING_REF DYNINST BPF JAVA PYTHON3 LIBRPM LIBSQLITE3 LIBVIRT LIBXML2 NLS NSS READLINE MONITOR_LIBS

3 debuginfoパッケージのインストール

kernel-debuginfoパッケージをインストールするため、リポジトリを追加します。

[root@server ~]# dnf config-manager --add-repo=https://repo.almalinux.org/vault/8.6/BaseOS/debug/x86_64/

kernel-debuginfoパッケージをインストールします。

[root@server ~]# dnf install kernel-debuginfo-4.18.0-372.9.1.el8

kernel-develパッケージをインストールするため、リポジトリを追加します。

[root@server ~]# dnf config-manager --add-repo=https://repo.almalinux.org/vault/8.6/BaseOS/x86_64/os/

kernel-develパッケージをインストールします。

[root@server ~]# dnf install kernel-devel-4.18.0-372.9.1.el8.x86_64

4 ユーザプログラムにプローブを設定する方法

4.1 テストプログラムの作成

テストプログラム(以降TP)を作成します。

[root@server ~]# cat tp.c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

int func1(int x)
{
  int fd;

  fd = open("/tmp/test1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
  printf("func1\n");
  close(fd);
  return 1;
}

int func2(int x)
{
  int fd;

  fd = open("/tmp/test2.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
  printf("func2\n");
  close(fd);

  return 2;
}

int main(int argc, char *argv[])
{
  int ret;

  for(;;) {
    ret = func1(10);
    sleep(2);

    ret = func2(20);
    sleep(2);
  }
  return ret;
}

-gオプションを付けて、TPをコンパイルします。

[root@server ~]# gcc -Wall -g -o tp tp.c

実行ファイルを確認します。

[root@server ~]# ls -l tp*
-rwxr-xr-x. 1 root root 21080  1月 17 20:26 tp
-rw-r--r--. 1 root root   516  1月 17 20:25 tp.c

4.2 関数の入り口にプローブを設定する方法

func1()の入り口に設定したプローブが実行されると、pp()を実行するスクリプトを作成します。

[root@server ~]# cat tp.stp
#!/usr/bin/stap

probe process("/root/tp").function("func1")
{
  printf("pp=%s\n", pp())
}

スクリプトを実行します。

[root@server ~]# stap -vg tp.stp
Pass 1: parsed user script and 485 library scripts using 295516virt/96596res/18164shr/80232data kb, in 340usr/100sys/455real ms.
Pass 2: analyzed script: 1 probe, 1 function, 0 embeds, 0 globals using 297100virt/99344res/19144shr/81816data kb, in 10usr/0sys/15real ms.
Pass 3: using cached /root/.systemtap/cache/9a/stap_9aac9b9924135443eec26a77b23d2146_1168.c
Pass 4: using cached /root/.systemtap/cache/9a/stap_9aac9b9924135443eec26a77b23d2146_1168.ko
Pass 5: starting run.

もう1つターミナルを開いてTPを実行します。

[root@server ~]# ./tp

SystemTapの実行結果を確認すると、プローブの情報(func1()がtp.cファイルの6行目に定義されている)が表示されていることがわかります。

[root@server ~]# stap -vg tp.stp
-snip-
pp=process("/root/tp").function("func1@/root/tp.c:6")
pp=process("/root/tp").function("func1@/root/tp.c:6")

ファイルを確認すると、tp.cの6行目にfunc1()が定義されていることがわかります。

[root@server ~]# less -N /root/tp.c
-snip-
      5
      6 int func1(int x)
      7 {

4.3 関数から復帰する場所にプローブを設定する方法

func2()から復帰する場所に設定したプローブが実行されると、pp()の実行とfunc2()の戻り値を表示するスクリプトを作成します。

[root@server ~]# cat tp.stp
#!/usr/bin/stap

probe process("/root/tp").function("func2").return
{
  printf("pp=%s, ret=%d\n", pp(), $return)
}

スクリプトを実行します。

[root@server ~]# stap -vg tp.stp

もう1つターミナルを開いてTPを実行します。

[root@server ~]# ./tp

SystemTapの実行結果を確認すると、プローブの情報(func2()がtp.cファイルの16行目に定義されている)とfunc2()の戻り値(2)が表示されていることがわかります。

[root@server ~]# stap -vg tp.stp
-snip-
pp=process("/root/tp").function("func2@/root/tp.c:16").return, ret=2
pp=process("/root/tp").function("func2@/root/tp.c:16").return, ret=2

4.4 ファイルの指定した場所にプローブを設定する方法

プローブを設定する場所を確認するため、lessコマンドでtp.cを参照します。ここでは、tp.cの10行目にプローブを設定します。

[root@server ~]# less -N /root/tp.c
-snip-
      9
     10   fd = open("/tmp/test1.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);
     11   printf("func1\n");

tp.cの10行目に設定したプローブが実行されると、pp()を実行するスクリプトを作成します。

[root@server ~]# cat tp.stp
#!/usr/bin/stap

probe process("/root/tp").statement("func1@/root/tp.c:10")
{
  printf("pp=%s\n", pp())
}

スクリプトを実行します。

[root@server ~]# stap -vg tp.stp

もう1つターミナルを開いてTPを実行します。

[root@server ~]# ./tp

SystemTapの実行結果を確認すると、プローブの情報(設定したプローブがtp.cの10行目)が表示されていることがわかります。

[root@server ~]# stap -vg tp.stp
-snip-
pp=process("/root/tp").statement("func1@/root/tp.c:10")
pp=process("/root/tp").statement("func1@/root/tp.c:10")

5 httpdにプローブを設定する方法

httpdソースコードにプローブを設定してみます。

5.1 debuginfoパッケージのインストール

httpdパッケージと同じ版数のhttpd-debuginfoパッケージをインストールするため、まず、httpdパッケージの版数を確認します。

[root@server ~]# rpm -qa|grep httpd
almalinux-logos-httpd-84.5-1.el8.noarch
httpd-tools-2.4.37-51.module_el8.7.0+3281+01e58653.x86_64
httpd-filesystem-2.4.37-51.module_el8.7.0+3281+01e58653.noarch
httpd-2.4.37-51.module_el8.7.0+3281+01e58653.x86_64

appstream-debuginfoリポジトリを有効にします。

[root@server ~]# dnf config-manager --enable appstream-debuginfo

httpd-debuginfoパッケージをインストールします。

[root@server ~]# dnf install httpd-debuginfo-2.4.37-51.module_el8.7.0+3281+01e58653

httpd-debuginfoパッケージの版数を確認します。

[root@server ~]# rpm -qa|grep httpd
httpd-debuginfo-2.4.37-51.module_el8.7.0+3281+01e58653.x86_64
almalinux-logos-httpd-84.5-1.el8.noarch
httpd-tools-2.4.37-51.module_el8.7.0+3281+01e58653.x86_64
httpd-filesystem-2.4.37-51.module_el8.7.0+3281+01e58653.noarch
httpd-debugsource-2.4.37-51.module_el8.7.0+3281+01e58653.x86_64
httpd-2.4.37-51.module_el8.7.0+3281+01e58653.x86_64

5.2 httpdのmain()にプローブを設定する方法

whichコマンドを実行して、SystemTapスクリプトに記述するhttpdのパスを確認します。

[root@server ~]# which httpd
/usr/sbin/httpd

httpdプログラムのmain()入り口にプローブを設定します。そして、プローブが実行されると、pp()を実行するスクリプトを作成します。

[root@server ~]# cat httpd.stp
#!/usr/bin/stap

probe process("/usr/sbin/httpd").function("main")
{
  printf("pp=%s\n", pp())
}

スクリプトを実行します。

[root@server ~]# stap -vg httpd.stp
Pass 1: parsed user script and 485 library scripts using 295520virt/96560res/18132shr/80236data kb, in 330usr/60sys/394real ms.
Pass 2: analyzed script: 1 probe, 1 function, 0 embeds, 0 globals using 300668virt/103140res/19312shr/85384data kb, in 50usr/0sys/51real ms.
Pass 3: using cached /root/.systemtap/cache/47/stap_47ccd673e95239f6187c55d0d22ba59f_1332.c
Pass 4: using cached /root/.systemtap/cache/47/stap_47ccd673e95239f6187c55d0d22ba59f_1332.ko
Pass 5: starting run.

httpdサービスを起動します。

[root@server ~]# systemctl start httpd

SystemTapの実行結果を確認すると、プローブの情報(main()がmain.cファイルの466行目に定義されている)が表示されていることがわかります。

[root@server ~]# stap -vg httpd.stp
-snip-
pp=process("/usr/sbin/httpd").function("main@/usr/src/debug/httpd-2.4.37-51.module_el8.7.0+3281+01e58653.x86_64/server/main.c:466")

httpdのソースファイルを確認すると、main()がmain.cファイルの466行目に定義されていることがわかります。

[root@server ~]# less -N /usr/src/debug/httpd-2.4.37-51.module_el8.7.0+3281+01e58653.x86_64/server/main.c
-snip-
    465
    466 int main(int argc, const char * const argv[])
    467 {

6 動作中のhttpdプロセスにプローブを設定する方法

動作中のhttpdプロセスにプローブを設定する方法について説明します。psコマンドを実行して、プローブを設定するプロセスのPIDを確認します。ここでは、PID=10066 のhttpdに対してプローブポイントを設定してみます。

[root@server ~]# ps -C httpd
    PID TTY          TIME CMD
  10066 ?        00:00:00 httpd
  10067 ?        00:00:00 httpd
  10068 ?        00:00:00 httpd
  10069 ?        00:00:00 httpd
  10070 ?        00:00:00 httpd

プローブを設定する場所を確認するため、lessコマンドでmpm_common.cを参照します。mpm_common.cのap_wait_or_timeout()関数入り口にプローブを設定します。

[root@server ~]# less -N /usr/src/debug/httpd-2.4.37-51.module_el8.7.0+3281+01e58653.x86_64/server/mpm_common.c
-snip-
    177 AP_DECLARE(void) ap_wait_or_timeout(apr_exit_why_e *status, int *exitcode,
    178                                     apr_proc_t *ret, apr_pool_t *p,
    179                                     server_rec *s)
    180 {

ap_wait_or_timeout()入り口にプローブを設定します。そして、プローブが実行されるとap_wait_or_timeoutI()への引数の値を表示するスクリプトを作成します。

[root@server ~]# cat httpd.stp
#!/usr/bin/stap

probe process(10066).function("ap_wait_or_timeout")
{
  printf("%s\n", $$parms)
}

SystemTapの実行結果を確認すると、ap_wait_or_timeout()への引数の値が表示されていることがわかります。

[root@server ~]# stap -vg httpd.stp
Pass 1: parsed user script and 485 library scripts using 295516virt/96620res/18192shr/80232data kb, in 260usr/330sys/613real ms.
Pass 2: analyzed script: 1 probe, 5 functions, 0 embeds, 0 globals using 300664virt/103216res/19372shr/85380data kb, in 60usr/0sys/75real ms.
Pass 3: using cached /root/.systemtap/cache/ff/stap_ffe11129161d317f17c1f7beecc71453_1899.c
Pass 4: using cached /root/.systemtap/cache/ff/stap_ffe11129161d317f17c1f7beecc71453_1899.ko
Pass 5: starting run.
status=0x7ffc2c5e3898 exitcode=0x7ffc2c5e389c ret=0x7ffc2c5e38a0 p=0x5651d4e4ba18 s=0x5651d4e74cc0
status=0x7ffc2c5e3898 exitcode=0x7ffc2c5e389c ret=0x7ffc2c5e38a0 p=0x5651d4e4ba18 s=0x5651d4e74cc0
-snip-

7 ライブラリ関数にプローブを設定する方法

glibc等の共有ライブラリ関数にプローブを設定する方法について説明します。

7.1 debuginfoパッケージのインストール

[root@server ~]# dnf install glibc-debuginfo-2.28-189.1.el8.x86_64

7.2 事前準備

mallocを呼び出すテストプログラム(以降TP)を作成します。

[root@server ~]# cat tp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
  char *ptr;

  for(;;) {
    printf("malloc\n");
    ptr = malloc(100);
    free(ptr);
    sleep(2);
  }
  return 0;
}

TPをコンパイルします。

[root@server ~]# gcc -Wall -g -o tp tp.c

lddコマンドを実行してTPがリンクする共有ライブラリのパスを確認します。

[root@server ~]# ldd tp
        linux-vdso.so.1 (0x00007ffef25ac000)
        libc.so.6 => /lib64/libc.so.6 (0x00007ff6b2060000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ff6b2425000)

7.3 動作確認

malloc()入り口にプローブを設定します。そして、TPがmalloc()を呼び出すと、pp()を実行するスクリプトを作成します。

[root@server ~]# cat lib.stp
#!/usr/bin/stap

probe process("/lib64/libc.so.6").function("malloc")
{
  if(execname()=="tp"){
    printf("pp=%s\n", pp());
  }
}