カーネルを再構築することなくカーネルの動作を深く詳細に確認できると便利です。今回紹介するSystemTapを使うと、ちょっとしたスクリプト言語を書くだけで、カーネル上の特定の処理をフックし、必要な情報を収集・分析できます。
SystemTapがあるとうれしい理由
SystemTapは実行中のカーネルの処理をフックして、必要に応じて情報を収集し、出力するツールです。
カーネルのデバッグにおける代表的な手法と言えば「printk()
の差し込み」です。printk()
関連の関数を使えば多くの問題の状況を把握できますし、その情報がそのまま解決につながる例だってたくさんあります[1]。
いわゆる「printk()
デバッグ」は強力な手法ではあるものの、いくつかの弊害も存在します。
printk()
を差し込むためにカーネルやモジュールをビルドし直さなくてはならない
- 環境に合わせたカーネルやモジュールのビルド方法を調べる必要がある
- カーネルを作り直した場合は再起動が必要
- 自分でビルドしたカーネルやモジュールだと現象が発生しない
printk()
を入れる適切な場所を探るために何度もカーネル・モジュールの再構築を繰り返す
printk()
を入れると現象が発生しない
- いろいろなところに
printk()
を入れすぎた結果、本当に見たかったログが流れてしまう
- ログをメモリに貯めて現象が起きたあとにダンプするようにしたら現象が発生しない
- そんな感じでトライアンドエラーを繰り返したら、本来起きていた現象がコードを元に戻そうが何しようがまったく発生しなくなった
要するに「なんでもかんでもprintk()
だけで調査しようとするな」という話ではあります。その「printk()
以外の方法」のひとつが、今回紹介するSystemTapです。
SystemTapは「STPスクリプト」と呼ばれる独自言語のスクリプトを利用して記述します。SystemTapのコマンドであるstap
コマンドがSTPスクリプトとカーネルのデバッグ情報から、C言語のコードを生成して、それをカーネルモジュールとしてビルド・ロードし、その結果を標準出力や独自のログバッファなどに保存するのです。
カーネル側で使用している要素技術は第443回でも紹介している「Livepatch」とほぼ同じです。むしろSystemTapはLivepatchよりもはるかに古くから存在する仕組みであり、Livepatchの先輩にあたる存在と言えるでしょう。
カーネルモジュールとして必要なコードをカーネルに挿入し、情報を取得します。よって第526回や第528回のようなカーネルそのものをビルドし再起動することは不要ですし、モジュールビルドする側のスペックもそこまで高いものは不要です。モジュールをアンロードすれば動作を停止しますし、出力をオンメモリのログバッファにしてしまえば、ログ出力による影響もある程度は抑えられます。
STPスクリプトのフォーマットを覚える必要はありますが、シンプルな書式なのでそこまで難しくはないでしょう。むしろカーネルデバッグをするのですから、Linuxカーネルに関する知識やそのコードを読み解く能力のほうが重要です。とは言え低レイヤーの話なので、数々のコンポーネントが複雑に絡み合う上位レイヤーに比べるとカーネルのコードは、はるかに「わかりやすい」です。
SystemTapに関してはRed Hatのドキュメントが充実しています。本記事を一通り読んでなんとなくUbuntuにおけるSystemTapの導入方法が掴めたら、「SystemTapビギナーズガイド」や「SystemTapタップセットリファレンス」を読むと良いでしょう。
UbuntuにSystemTapをインストールする
Ubuntuで必要なのは「SystemTap本体」と「対象カーネルのデバッグ情報」のふたつです。これらに関してはUbuntuのWikiページに情報がまとまっています。言い方を変えるとこれらをインストールしてしまえば、SystemTapを利用できます[2]。
さらにカーネルモジュールを自前でビルド・ロードするため、SecureBootもオフにしておくと良いでしょう。
まずはカーネルのデバッグ情報をインストールします。残念ながらデバッグ情報付きのカーネルは公式リポジトリには存在しません。おそらく用途が限定的であることと、公式ミラーリポジトリのサイズをあまり大きくしないようにするためでしょう。
よって公式のデバッグシンボル付きパッケージリポジトリを導入します。
これでリポジトリの導入は完了です。ちなみにこのリポジトリではカーネル以外のデバッグ情報付きパッケージも提供しています。もし特定のパッケージのデバッグを行いたい際にも役に立つでしょう[3]。
現在起動しているカーネルのデバッグシンボルパッケージをインストールしましょう。
それなりのサイズなのでインストールに時間がかかるかもしれません。
インストールされたカーネルイメージやモジュールを確認してみましょう。「with debug_info
」の表示があるはずです。
それに対して通常のカーネルモジュールには「with debug_info
」がありません。
これでデバッグ情報の準備は整いました。あとはSystemTap本体もインストールしておきます。
ちなみにsystemtap-docパッケージもインストールしておくと、膨大なマニュアル類もマシン上のmanコマンド経由で参照できます。使い方を学びたいなら、一緒にインストールしておくと良いでしょう。
Kernel 5.0以降の対応
Ubuntu 18.04 LTSのHWE用やUbuntu 19.04ではLinux Kenel 5.0が採用されています。しかしながらSystemTapはバージョン4.1以降でのみKernel 5.0に対応しており、Ubuntuのそれぞれのリリースのリポジトリで提供されているSystemTapのバージョンである3.1や4.0ではKernel 5.0に対応していません。
よってSystemtapを使おうとすると次のようにエラーとなります。
もしKenrel 5.0以降のデバッグを行いたいならSystemTapの4.1以降をインストールしてください。ソースコードからビルドする方法が確実ではありますが、開発版19.10にある4.1パッケージをそのままコピーしたPPAも用意してあります。
PPA版のSystemTapは次の方法でインストールできます。
これで19.10やUbuntu 18.04 LTSでKernel 5.0が動いている場合も、SystemTapを利用できるはずです。
プローブポイントのリストアップ
さて準備も整ったところでSystemTapを使ってみましょう。SystemTapの基本は「プローブポイントを指定し、そこで任意のコードを実行する」ことです。
プローブポイントとはカーネル内で処理をフックできる箇所です。代表的なのは関数が呼び出されたタイミングと関数を出ていくタイミングですが、他にもいろいろなプローブポイントの指定方法が存在します。
試しに特定の関数のプローブポイントをリストアップしてみましょう。たとえば次のコマンドは、カーネル内に存在する「acpi_
」で始まる関数のうち、プローブポイントとして指定できる関数のリストと具体的なコードの場所です。
特定のカーネルモジュールすべての関数を表示したい場合は、次のように実行します。次の例だとbtrfsモジュールが対象です。
「-l
」の代わりに「-L
」を使うと、参照できる変数と型も表示してくれます。
システムコールもプローブポイントです。
仮想ファイルシステムの操作をトリガーにしたいのならvfsを使うと良いでしょう。
その他のプローブポイントについてはsystemtap-docパッケージ付属のstapprobesマニュアルを参照してください。
プローブポイントにトリガーをセットする
プローブポイントにトリガーをセットすることで、そのプローブポイントが呼ばれたら登録されているトリガーを呼び出します。
stapコマンドはSTPファイル経由でのスクリプトの実行だけでなく、「-e
」オプションを渡すことでコマンドラインに直接イベントを書けます。
上記の例では仮想ファイルシステムでのread()
呼び出しが発生したら、その旨のメッセージを表示し(printf()
)、処理を終了します(exit()
)。たとえばexit()
がないと、何か読み込みを行うたびにメッセージが表示され続けることになります。
「-v
」オプションを付けると、上記のように「Pass…」のメッセージが表示されます。Pass 1でスクリプトをパースして、Pass 2でシンボル情報の解決を行い、Pass 3でそれらの情報をC言語のファイルに落とし込み、Pass 4でコンパイルし、Pass 5で作成したカーネルモジュールをロードします。各ステージの詳細はstapのマニュアルのPROCESSINGの項を参照してください。ちなみに「-p 数字
」でそのPassで処理を止め、Passによっては生成物を表示します。どんなコードを生成しているか確認したいときに便利です。
結果を見るとすぐに「read performed」が表示されていることがわかりますね。これは誰かが「read()
」を呼び出したためです。
より詳細なSTPスクリプトは、ファイルに記述することになります。たとえば以下のようなtest1.stpファイルを作成してみます。
上記はsys_read()
が「呼び出された」ときに実行されるコードです。まずfonction(FOO).call
のようにプローブポイントの後ろに「.call
」と付けることで、「インラインでない関数の呼び出し」のみに限定されます。「.inline
」なら「インラインな関数のみ」、「.exported
」なら「エクスポートされている関数のみ」となります。今回の例だと「.call
」のありなしで結果は変わりません。
「%s -> %s
」の書式でプローブポイントとなる関数を表示しています。「thread_indent()
」は現在のタスクを表示したあとに、呼び出しに応じてインデントするよう空白文字を差し込むヘルパー関数です。今回は1回呼び出されたらすぐに終了するのであまり意味はありません。「probefunc()
」はプローブポイントの関数名です。このあたりの情報は「man function::関数名
」などで詳しい情報を取得できます。
「$$params
」はプローブポイントの引数を展開する変数です。つまり「%s args [%s]
」は「関数名 args [引数リスト]
となります。
実際に実行してみましょう。
thread_indent()
の結果として「stapio」が表示されていますが、これはstapコマンドから呼び出される子プロセスです。つまり自分自身のsys_read()
を検知しているわけですね。
逆に関数から戻るときは「.call
」の代わりに「.return
」と指定すればフックできます。
「$$return
」は「$$args
」と同じように、今度は戻り値が格納されている変数となります。ちなみに「.return
」のプローブポイントは指定した関数が「戻ったあと」の場所にセットされます。上記の内容をtest2.stpとして保存して、今度は「-v
」オプションなしで実行してみましょう。
stapioプロセスからのシステムコールとしてsys_read()
が呼ばれ、その戻り値は0x12(正の値なので12バイトの読み込み)であることがわかります。
情報表示の例としては、他にもカーネル側のバックトレースを表示するprint_backtrace()
や、現在のレジスタをダンプするprint_regs()
なども存在します。詳細は「man function::関数名
」でマニュアルを参照してください。
ここまで読んだかたなら、おおよそ想像がついていることでしょう。ええ、そうです。SystemTapを使ったデバッグでも、スクリプト作成者のスキル次第で結局「printf()
」に頼ることになります。
そこで次回は、ネットワーク関連の具体的な事例を元に「printf()
」には(そこまで)頼ることなく、問題の要因を探る方法を紹介します。