SystemTapを使ってみる

2024年1月16日

SystemTapはRedhatを中心として作られたフリーソフトで、主にカーネルの動作確認や解析などに使用できるツールです。2005年に発表されて以来、2011年にはメジャーdistroで動作するようになり、現在bpftraceと並んで不動の地位を得ています。

効能の割にそれほど情報がないこともあり、Ubuntu 22.04での使用感などを含め、とりあえず記事にしてみました。

目的

Ubuntu 22.04でSystemTapを使用し、Linuxカーネルの解析をしてみる

背景

従来Linuxカーネルの動きを知りたければ、ドキュメントで分からないことがあればソースを見て、それでも分からなければカーネルのソースコードを直接いじってprintkを挿入してちまちまログ出力させて、それをビルドしては配備して動かす(再起動する)感じでした。環境を整えるだけでも一苦労だし、ビルド自体も非常に時間がかかる作業でした。アプリならデバッガを使用してブレークポイントで止めて見るのもすぐですが、カーネルでは気軽に出来ないからです。

SystemTapを使用すると、この作業を従来よりはるかに楽に出来るようになります。

インストール

カーネルをいじりたい人がこの辺で詰まっていては話にならないということなのか、この辺のドキュメントもあまり更新する人がいません。本家の公式ドキュメントはRedhat一色で、.debのデの字もなく(wikiにはある)…Ubuntu側の資料を漁っても、以下のようなあまりメンテされてなさそうなものしかありません。

https://wiki.ubuntu.com/Kernel/Systemtap

流れは実際書いてあるとおりなのですが…まずインストールするパッケージ、公式リポジトリのパッケージをそのまま使っていても、バージョンが古くて動きません。ガチの最新夜間ビルドになりますが、PPAのリポジトリを使った方がいいです。

https://launchpad.net/%7Eubuntu-support-team/+archive/ubuntu/systemtap

また、カーネルのデバッグ情報(dbgsym)が必要になるのですが、リポジトリのうち現在securityはありませんし、apt-keyを使用した署名検証用情報の管理がdeperecatedだと怒られます。

上記を考慮した修正済手順は以下のとおりです。

# systemtap本体のインストール
sudo add-apt-repository ppa:ubuntu-support-team/systemtap
sudo apt update
sudo apt install -y systemtap gcc # サーバー版の場合(コードの先の文章参照)

# kernel dbgsymのインストール
sudo apt-key --keyring /etc/apt/trusted.gpg.d/ddebs-keyring.gpg adv --keyserver keyserver.ubuntu.com --recv-keys C8CAB6595FDFF622
# ↑keyringの名前は適当です
codename=$(lsb_release -c | awk  '{print $2}')
sudo tee /etc/apt/sources.list.d/ddebs.list << EOF
deb http://ddebs.ubuntu.com/ ${codename}      main restricted universe multiverse
#deb http://ddebs.ubuntu.com/ ${codename}-security main restricted universe multiverse
deb http://ddebs.ubuntu.com/ ${codename}-updates  main restricted universe multiverse
deb http://ddebs.ubuntu.com/ ${codename}-proposed main restricted universe multiverse
EOF
sudo apt update
sudo apt install -y linux-image-$(uname -r)-dbgsym

インストール手順の中にgccが含まれていますが、Ubuntuの場合、Desktop版とServer版でカーネルのバージョンが違っており、Desktop版の場合はデフォルトでインスールされるgcc-11ではなく、gcc-12にする必要があります。

使い方

systemtapは基本的に所定のイベントにhookしてその際に何をするかを指定する、という使い方です。

今回はsystemtapで予め用意しているvfs.readというイベント(fs/read_write.cvfs_read()です)をhookして、一度だけ変数とstack traceの出力をしてみます。

$ sudo stap -v -e 'probe vfs.read {printf("%s\n", $$vars$$); print_backtrace(); exit()}'
Pass 1: parsed user script and 484 library scripts using 115444virt/104804res/7204shr/97500data kb, in 340usr/40sys/378real ms.
Pass 2: analyzed script: 1 probe, 46 functions, 6 embeds, 0 globals using 353656virt/344656res/9044shr/335712data kb, in 2820usr/430sys/3249real ms.
Pass 3: translated to C into "/tmp/stapVbBHbg/stap_6b975271fce17f60d1f6dc7c64601c24_11721_src.c" using 353656virt/345072res/9236shr/335712data kb, in 550usr/140sys/700real ms.
/tmp/stapVbBHbg/stap_6b975271fce17f60d1f6dc7c64601c24_11721_src.o: warning: objtool: _stp_vsprint_memory()+0x24f: call to __get_user_nocheck_1() with UACCESS enabled
/tmp/stapVbBHbg/stap_6b975271fce17f60d1f6dc7c64601c24_11721_src.o: warning: objtool: function___global_kernel_or_user_string_quoted__overload_0.constprop.0()+0x58f: call to __get_user_nocheck_1() with UACCESS enabled
Pass 4: compiled C into "stap_6b975271fce17f60d1f6dc7c64601c24_11721.ko" in 13360usr/700sys/14209real ms.
Pass 5: starting run.
file={.f_u={.fu_llist={.next=0x0}, .fu_rcuhead={.next=0x0, .func=0x0}}, .f_path={.mnt=0xffff8b81c1fef2a0, .dentry=0xffff8b818ae5e900}, .f_inode=0xffff8b81cc622e30, .f_op=0xffffffffc140ce60, .f_lock={<union>={.rlock={.raw_lock={<union>={.val={.counter=0}, <class>={.locked='\000', .pending='\000'}, <class>={.locked_pending=0, .tail=0}}}}}}, .f_write_hint=0, .f_count={.counter=2}, .f_flags=34818, .f_mode=1015839, .f_pos_lock={.owner={.counter=-128087006039296}, .wait_lock={.raw_lock={<union>={.val={.counter=0
 0xffffffffb4d94880 : vfs_read+0x0/0x1a0 [kernel]
 0xffffffffb4d97457 : ksys_read+0x67/0xf0 [kernel]
 0xffffffffb4d974f9 : __x64_sys_read+0x19/0x20 [kernel]
 0xffffffffb57b770c : do_syscall_64+0x5c/0xc0 [kernel]
 0xffffffffb58000da : entry_SYSCALL_64_after_hwframe+0x62/0xcc [kernel]
Pass 5: run completed in 20usr/50sys/1140real ms.
$

printkと比較すると格段の進化を感じます。hookは任意の関数のentry/return(return&inlineはダメ)で仕掛けられます。

あとはRHEL7用なのでかなり古いが、redhatが出しているビギナーズガイド辺りが簡単なので最初に読むと概要が分かります。

https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/7/html-single/systemtap_beginners_guide/index

慣れたら本家のdocumentを漁りましょう。

注意点は、変数に参照したい場合、構造体のメンバを表すのに.(ドット)が使えないこと(文字列連結に使ったらしい)。ポインタであろうとなかろうと一律->で参照してください。あとは変数の最後に$$を付けると、どんな型でもtoStringよろしく勝手に文字列にしてくれるので便利です。

bpftraceとどっちがいいか?

systemtapだとユーザーモードにもアクセスできます。bpftraceも便利らしいのですが、今の所systemtapの方が年季が長い分痒いところに手が届きそうです。どちらもprintkと比べると格段の進歩を見せてくれます。2021年時点の資料だと以下のとおりです。

https://lwn.net/Articles/852112/

まとめ

Linuxカーネルがとても調べやすくなるsystemtapをUbuntu 22.04に導入してみました。今までは一日潰すつもりで調べてた作業が、体感ユーザーアプリと同じくらいの感覚で手を付けられるようになります。