strace でシステムコールを観察する
strace は、あるコマンドが内部でどのシステムコールを、どんな引数で呼び、何を返したかを順番に表示するツールです。プログラムのソースが手元になくても、OSとどうやり取りしているかを外から覗けるのが強みです。各行は「呼び出し名(引数) = 戻り値」の形で並び、失敗した呼び出しには errno の名前(例: ENOENT)も添えられます。-e trace=open,read のように対象を絞ったり、-f で子プロセスまで追跡できます。「設定ファイルをどこから読もうとして失敗したのか」といった障害調査でも非常に役立ちます。
プログラムが内部で何をしているかを知りたいとき、ソースコードを読むのが正攻法だ。だが、ソースが手元にない、あるいは膨大すぎて追いきれない、ということもある。
そんなときに頼りになるのがstraceだ。
straceは、あるコマンドが実行中にどんなシステムコール(syscall)を、どんな引数で呼び、何を返したかを、時間順にすべて表示してくれるツールだ。
プログラムとカーネルのあいだで交わされる「会話」を、外からそっくり盗み聞きするようなものだと考えると分かりやすい。
中身がブラックボックスに見えるプログラムでも、「OSとどんなやり取りをしているか」という観点からなら、その振る舞いを客観的に観察できる。
しかも、ソースを書き換えたりデバッガを仕込んだりしなくても使える手軽さが大きな魅力だ。
📖 一行一行の読み解き方
使い方は拍子抜けするほど単純で、調べたいコマンドの前にstraceを付けるだけだ。たとえばstrace ./helloのように実行する。
すると画面には、そのプログラムが発行したシステムコールが一行ずつ流れていく。各行は基本的に「呼び出し名(引数の並び) = 戻り値」という決まった形をしている。
実例で見てみよう。execve("./hello", ["./hello"], ...) = 0 はプログラムの起動そのもの。openat(...) = 3 はファイルを開いてファイルディスクリプタ(fd)の3番を得たこと。write(1, "Hello, World!\n", 14) = 14 は1番(標準出力)へ14バイトを書き込んで14バイト処理できたこと、を表す。
プログラムの起動から終了まで、実際に発行された生のシステムコールが順番に見えるため、「このプログラムは結局どんな順で、カーネルに何を頼んだのか」が一目で追える。
最初は見慣れない呼び出しが大量に並んで圧倒されるが、自分が書いたコードに対応する行(writeやopenなど)を拾い読みするところから慣れていくとよい。
❌ 失敗した呼び出しの見つけ方
straceがとりわけ威力を発揮するのは、失敗した呼び出しを見つけるときだ。
システムコールが失敗すると戻り値は-1になり、その理由はerrno(errno)に記録される。straceは気の利いたことに、その-1のうしろに、errnoの名前とその意味する英文まで添えて表示してくれる。
たとえば openat("/etc/myapp.conf", O_RDONLY) = -1 ENOENT (No such file or directory) という一行を見れば、「このプログラムは/etc/myapp.confという設定ファイルを読もうとしたが、そのファイルが存在せず失敗した」と即座に分かる。
アプリが原因不明で起動しない、設定が反映されない、ライブラリが見つからない。こうした障害の調査で、「どのファイルを、どこから読もうとして、なぜ失敗したのか」を、ソースを一行も追わずに突き止められる。これは実務で計り知れないほど重宝する。
エラーメッセージが不親切なプログラムでも、straceをかければ本当の失敗箇所が浮かび上がる、ということは少なくない。
🎯 情報の洪水から知りたい行を絞る
実際のプログラムは起動するだけで何百ものシステムコールを呼ぶため、そのまま流すと情報の洪水になり、肝心の行を見失う。そこで対象を絞り込むオプションが役立つ。
-e trace=openat,read のように書くと、指定した種類のシステムコールだけを表示できる。
ファイル関連だけを見たいなら-e trace=file、ネットワーク関連だけなら-e trace=networkといった、まとまった指定も可能だ。
-fを付けると、forkなどで生まれた子プロセスまで追跡対象に含める。前章で見たfork→exec→waitの流れを観察したいときは、この-fが欠かせない。
ほかにも目的ごとに道具がそろっている。出力を画面ではなくファイルに保存して後でじっくり読みたいときは-o ログファイル名を、各呼び出しにどれだけ時間がかかったかを計測したいときは-Tを、システムコールごとの回数や所要時間を集計したいときは-cを使う。
これらを組み合わせて、知りたい情報だけを浮かび上がらせるのが、straceを使いこなすコツだ。
🪜 ltrace と strace は見ている層が違う
よく似たツールにltraceがある。両者は見ている層が違う。
straceがカーネルへのシステムコールを表示するのに対し、ltraceはC標準ライブラリ(libc)の関数呼び出しを表示する。
たとえば自作プログラムがprintfを呼ぶ様子はltraceで見え、その結果として実際に発行されるwriteはstraceで見える、という棲み分けだ。
両者を見比べると、ライブラリ関数という上の層と、システムコールという下の層の関係が立体的に理解できる。
straceが効く典型的な使いどころをまとめておく。起動に失敗するプログラムの原因究明、設定ファイルや共有ライブラリをどこから探しているかの確認、想定外に遅い処理でどのシステムコールが時間を食っているかの特定、そして「このプログラムは外部とどんな通信をしているのか」という挙動の調査だ。
すでに動いているプロセスを途中から覗きたいときは、-p プロセス番号 のように対象のPIDを指定してアタッチすることもできる。
一点だけ注意がある。straceで監視されたプログラムはシステムコールごとに横から覗かれるぶん動作が遅くなるため、時間に敏感な処理の本番計測には向かない。あくまで挙動の調査や原因究明の道具と心得よう。
出力に出てきた呼び出し名で正確な仕様が知りたくなったら、その名前をそのままman 2で引けば、引数や戻り値、設定されうるerrnoの意味を一次情報として確認できる。
straceで現象をつかみ、man 2で仕様を裏取りする。この往復が調査の王道だ。