SystemTapのクロス開発環境での使い方

# TL;DR

SystemTapをクロス開発環境でビルドして使う方法のメモ。

- `stap`コマンドをホストPC用にコンパイル
- `staprun`コマンドをターゲット用にコンパイル
- `stap`コマンドでsystemtapスクリプトから`.ko`ファイルをビルド  
(クロスコンパイラ、ターゲットのLinuxカーネルビルドツリーが必要)
- `.ko`ファイルをターゲットにインストールし、`staprun`コマンドでload -> probe実行

PC等のセルフホスト環境では、`stap`コマンドはSystemTapのフロントエンドコマンドとして、ビルドからloadまでワンストップで実行してくれる。
クロス開発の場合、通常ビルドはホストPCで、`.ko`ファイルのloadはターゲットで実行しなければならない。
このようなケースのために、load等のランタイム処理は`staprun`コマンドに分離してくれているので、`staprun`コマンドのみクロスコンパイルすればよい。
そのための`configure`オプションとして、`--disable-translator`が用意されている。


# SystemTapとは
独自スクリプト言語で記述したprobe処理を、ローダブルカーネルモジュールにビルドしてloadすることで、カーネルを再ビルドすること無くカーネルを動的に解析するための仕組み。
kprobeをDSLで抽象度を上げたもの、と考えると理解しやすいかも。

よく比較されるものとして、perf, ftrace, eBPFなどがある。([比較表](https://sourceware.org/systemtap/wiki/SystemtapDtraceComparison))

PC等のセルフホスト環境では、`stap`コマンドはSystemTapのフロントエンドコマンドとして、ビルドからprobeまでワンストップで実行してくれる。
`stap`コマンドは下記5個のpassからなる。([man stap(1) #PROCESSING](https://sourceware.org/systemtap/man/stap.1.html#lbAZ))

- Pass 1. parse
stpスクリプトから、コメントの除去、`()`や`;`の付与、`$1`など引数の置換などを行う。
Cプログラムのビルドプロセスにおけるプリプロセスに近い。

- Pass 2. elaborate
kernelのデバッグシンボルから、スクリプト内で参照するシンボルが解決可能か確認する。
不要なコードの削除など、最適化も行う。
ただし、あくまでもstpスクリプトレベルでの最適化。

- Pass 3. translate
stpスクリプトをC言語に変換する。
併せてビルドに必要なMakefileも作成する。

- Pass 4. compile
`.c`ファイルをビルドし、`.ko`ファイルを生成する。

- Pass 5. run
`.ko`ファイルをinsmodし、probeした結果を取得して標準出力 or ファイルに出力する。
実際には`stap`コマンドはこの処理を、`staprun`コマンドに委譲している。


クロス開発環境で考えると、Pass 1〜4はホストPCで実施可能。
Pass 5はターゲット上での実行しなければならない。


# QEMU(aarch64)上のlinux上でSystemTapを使うまでの手順

- Host PC : Ubunt 16.04 (x86_64)
- buildroot : 2019.02.4
- linux : 4.19.16
- qemu : 4.0.0
- systemtap : 4.1


## 1. QEMUをコンパイル

特別な手順はなし。

```shell-session
wget https://download.qemu.org/qemu-4.0.0.tar.bz2
tar xf qemu-4.0.0.tar.bz2
mkdir -p build-qemu-4.0.0
cd build-qemu-4.0.0
../qemu-4.0.0/configure --target-list=aarch64-softmmu
make -j$(getconf _NPROCESSORS_ONLN)
cd ..
```

## 2. buildrootでターゲットのlinux / rootfsをコンパイル


```shell-session
wget https://buildroot.org/downloads/buildroot-2019.02.4.tar.bz2
tar xf buildroot-2019.02.4.tar.bz2
cd buildroot-2019.02.4
```

buildrootのconfigを変更。
SystemTap観点で追加が必要なのはELFUTILSぐらい。
あとは効率化のための`CCACHE`やsshのための`DROPBEAR`など。

```diff:configs/qemu_aarch64_virt_defconfig
--- qemu_aarch64_virt_defconfig.org	2019-08-17 13:03:12.239335388 +0900
+++ qemu_aarch64_virt_defconfig	2019-08-17 13:03:44.551346868 +0900
@@ -21,3 +21,14 @@
 BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y
 BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="board/qemu/aarch64-virt/linux.config"
 BR2_LINUX_KERNEL_NEEDS_HOST_OPENSSL=y
+
+BR2_CCACHE=y
+BR2_TOOLCHAIN_BUILDROOT_WCHAR=y
+BR2_TOOLCHAIN_BUILDROOT_CXX=y
+BR2_TARGET_GENERIC_ROOT_PASSWD="password"
+BR2_PACKAGE_STRACE=y
+BR2_PACKAGE_STRESS=y
+BR2_PACKAGE_OPENSSL=y
+BR2_PACKAGE_ELFUTILS=y
+BR2_PACKAGE_DROPBEAR=y
+# BR2_PACKAGE_DROPBEAR_SMALL is not set
```

linuxのconfigも変更。
SystemTapを使うためには、`RELAY`, `KPROBE`, `DEBUG_INFO`, `DEBUG_KERNEL`の追加が必要だった。
`GDB_SCRIPTS`と`FUNCTION_TRACER`はおまけ。

```diff:board/qemu/aarch64-virt/linux.config
--- linux.config.org	2019-08-17 13:08:45.843587678 +0900
+++ linux.config	2019-08-17 13:10:14.427693496 +0900
@@ -4,7 +4,9 @@
 CONFIG_HIGH_RES_TIMERS=y
 CONFIG_TASKSTATS=y
 CONFIG_SCHED_AUTOGROUP=y
+CONFIG_RELAY=y
 CONFIG_PROFILING=y
+CONFIG_KPROBES=y
 CONFIG_MODULES=y
 CONFIG_MODULE_UNLOAD=y
 CONFIG_BLK_DEV_BSGLIB=y
@@ -52,3 +54,7 @@
 CONFIG_EXT4_FS=y
 CONFIG_TMPFS=y
 CONFIG_TMPFS_POSIX_ACL=y
+CONFIG_DEBUG_INFO=y
+CONFIG_GDB_SCRIPTS=y
+CONFIG_DEBUG_KERNEL=y
+CONFIG_FUNCTION_TRACER=y
```

```shell-session
make qemu_aarch64_virt_defconfig
make BR2_JLEVEL=$(getconf _NPROCESSORS_ONLN)
cd ..
```

[公式Wiki](https://sourceware.org/systemtap/wiki/SystemTapWithSelfBuiltKernel)で指定されているカーネルコンフィグは以下の通り。
`CONFIG_DEBUG_INFO_SPLIT`のみ`n`なので注意。

```config
CONFIG_DEBUG_INFO=y        # stpスクリプトで参照するシンボルを解決するために必要。
CONFIG_KPROBES=y           # Kernel空間をprobeするための仕組み。SystemTapはバックエンドでkprobeが使われているため必須。
CONFIG_RELAY=y             # CPUごとのカーネルバッファをユーザ空間に見せるための仕組み。probe結果の出力に使われている。
CONFIG_DEBUG_FS=y          # 上記relay fileはdebugfs(/sys/kernel/debug/systemtap/)上に作られる。
CONFIG_MODULES=y           # SystemTap(kprobe)はLoadable Kernel Moduleにprobe処理を入れることでkernel空間を盗み見る。
CONFIG_MODULE_UNLOAD=y     # 同上。
CONFIG_UPROBES=y           # kprobesのUser空間版。User空間のprobeもするためには必要。
CONFIG_DEBUG_INFO_SPLIT=n  # stapはsplit debug infoに対応していないので、無効にする必要がある。
```

## 3. `stap`コマンドをホストPC用にコンパイル

`stap`コマンドはホストPC用なので、普通にビルドすればいい。
手元の環境では`libdw-dev`パッケージが必要だった。

```shell-session
sudo apt install libdw-dev

wget https://sourceware.org/systemtap/ftp/releases/systemtap-4.1.tar.gz
tar xf systemtap-4.1.tar.gz
mkdir -p build-host-systemtap-4.1/
cd build-host-systemtap-4.1
../systemtap-4.1/configure --prefix=${PWD}/destdir
make -j$(getconf _NPROCESSORS_ONLN)
make install
cd ..
```


## 4. `staprun`コマンドをターゲット用にコンパイル

続いて、`staprun`コマンドをターゲット用にコンパイルする。
基本的には通常のクロスコンパイルだが、`stap`コマンドが不要なので`--disable-translator`オプションをつけておく。

ここではまとめてインストールしているが、個別でインストールする場合は`staprun`だけでなく`stapio`も必要なので注意。
`stapio`は`staprun`から更にフォークされる。
`/sys/kernel/debug/systemtap/${MOD_NAME}/trace${CPU_ID}`からデータを読み出し、標準出力 or ファイルに出力する。


```shell-session
export PATH=${PWD}/buildroot-2019.02.4/output/host/bin/:${PATH}
mkdir -p build-aarch64-systemtap-4.1
cd build-aarch64-systemtap-4.1
../systemtap-4.1/configure \
  --host=aarch64-buildroot-linux-uclibc \
  --with-sysroot=${PWD}/../buildroot-2019.02.4/output/staging \
  --disable-docs \
  --disable-http \
  --disable-server \
  --disable-translator \
  --disable-ssp
make -j$(getconf _NPROCESSORS_ONLN)
DESTDIR=${PWD}/../buildroot-2019.02.4/output/target/ make install
cd ..
```

`staprun`コマンドがbuildrootのrootfsにインストールされたので、イメージを再生成する。

```shell-session
make -C buildroot-2019.02.4/ BR2_JLEVEL=$(getconf _NPROCESSORS_ONLN)
```

## 5. `stap`コマンドで`.ko`ファイルをビルド

PC用でも同じだが、任意のSystemTapスクリプトをビルドするためには、
`stapdev`グループと`stapusr`グループの両方のメンバーである必要がある。([man stap(1) #PERMIMSSIONS](https://sourceware.org/systemtap/man/stap.1.html#lbBE))

```shell-session
sudo adduser $(whoami) stapdev
sudo adduser $(whoami) stapusr
```

下記コマンドで、`stap_hello_world.ko`が生成される。

```shell-session
export PATH=${PWD}/buildroot-2019.02.4/output/host/bin/:${PATH}
build-host-systemtap-4.1/destdir/bin/stap \
  -a arm64 \
  -B CROSS_COMPILE=aarch64-buildroot-linux-uclibc- \
  -r ${PWD}/buildroot-2019.02.4/output/build/linux-4.19.16/ \
  -e 'probe begin { log ("hello " . k) exit () } global k="world" ' \
  -m stap_hello_world
```

## 6. qemuの起動


起動時のオプションは`buildroot-2019.02.4/board/qemu/aarch64-virt/readme.txt`を参考にする。
sshで接続用にTCPポートのフォワードだけ追加している。

```shell-session
build-qemu-4.0.0/aarch64-softmmu/qemu-system-aarch64 \
  -M virt \
  -cpu cortex-a53 \
  -nographic \
  -smp 1 \
  -kernel buildroot-2019.02.4/output/images/Image \
  -append "rootwait root=/dev/vda console=ttyAMA0" \
  -netdev user,id=eth0,hostfwd=tcp::10022-:22 \
  -device virtio-net-device,netdev=eth0 \
  -drive file=buildroot-2019.02.4/output/images/rootfs.ext4,if=none,format=raw,id=hd0 \
  -device virtio-blk-device,drive=hd0 \
  -gdb tcp::4321
```

## 7. `staprun`コマンドでprobe実行

めでたしめでたし。

```shell-session
# scp -P 10022 stap_hello_world.ko root@localhost:
# ssh -p10022 root@localhost /usr/local/bin/staprun stap_hello_world.ko
hello world
# ssh -p10022 root@localhost /usr/local/bin/staprun stap_hello_world.ko k=hoge
hello hoge
```


# 参考

[SystemTap公式](http://sourceware.org/systemtap)
[man stap(1)](https://sourceware.org/systemtap/man/stap.1.html)
[man staprun(8)](https://sourceware.org/systemtap/man/staprun.8.html)
[SystemTap公式git README](https://sourceware.org/git/?p=systemtap.git;a=blob_plain;f=README;hb=HEAD)
[SystemTap公式Wiki Using systemtap with self-built kernels](https://sourceware.org/systemtap/wiki/SystemTapWithSelfBuiltKernel)
[SystemTap公式Wiki CrossCompiling](http://sourceware.org/systemtap/wiki/TipCrossCompiling)
[SystemTap公式Wiki 比較表](https://sourceware.org/systemtap/wiki/SystemtapDtraceComparison)
[Brendan Gregg's Blog Choosing a Linux Tracer (2015)](http://www.brendangregg.com/blog/2015-07-08/choosing-a-linux-tracer.html)
[睡眠不足 perf, ftraceのしくみ](http://mmi.hatenablog.com/entry/2018/03/04/052249)