ソースコードを読むための技術

$Id: readingcode.html,v 1.13 2003/12/06 00:01:08 aamine Exp $

御意見・御感想は aamine@loveruby.net 青木峰郎まで。 リンクは自由にどうぞ。

この文書を書いた動機

ソースコードを読みなさい、あるいは読んでいく、という話題は わりとあるけども、どう読んだらいいか書いたものは見たことがない。 プログラマならプログラムの読みかたは当然わかっているものだということなのだろうか。

しかし自分には人の書いたプログラムを読むことがそんなに簡単な ことだとは思えない。プログラムを書くのと同じくらい、読むことに だって技術や定石があるはずだし、必要だと思う。そこで、コードを どう読むべきか、とりあえずは C 言語を前提として、無意識のうちに やっていることを明示的に整理してみる。

方針の決定

読むプログラムが決まった。さてどうするか。なんの方針もなくただ ただ main から読んでいってもコードが言おうとしていることは理解 できないだろう。まずコードを読む目的を明確に決め、それにだけ 集中するようにする。全てを読まなければいけないときでも、パスを 分けて部分ごとに読む。

解析の手法

大雑把に言って解析手法は静的な手法と動的な手法に分類できる。 静的な手法とは、ソースコードそれ自体を読むこと。 動的な手法とは、デバッガなどを使って実行時の動きを追うことだ。

基本的に解析は動的解析から始めるのがよい。 静的解析とは、多かれ少なかれ、プログラムの動作を予想することである。 対して動的解析で見るのは事実である。 まず事実を見ておいたほうが方向付けがしやすいし、間違いも減る。 最適化する前にプロファイルを取れ、というのと似ているだろうか。 事件解決はまず現場から、というのでもよい。

静的な解析

対象プログラムを使う

これがなければ始まらない。そもそもそのプログラムがどういうものなのか、 どういう動作をするのか知っておく。

ドキュメントを読む

これも前項と似ていて、まず仕様を知っておこうということ。 また内部構造を解説したドキュメントが付いていたらそれもぜひ見ておきたい。 「HACKING」「TOUR」などという名前のファイルがあったら要チェック。

ディレクトリ構造を読む

どういう方針でディレクトリが分割されているのか見る。 そのプログラムがどういう作りになっているのか、 どういうパートがあるのか、概要を把握する。 それぞれのモジュールがどういう関係にあるのか確かめる。


ファイル構成を読む

ファイルの中に入っている関数(名)も合わせて見ながら、 どういう方針でファイルが分割されているのか見る。 ファイル名は衰えないコメントのようなものであり、注目すべきである。

また関数名の名前付けルールについてもあたりをつけておきたい。 C のプログラムなら extern 関数にはたいていプリフィクスを 使っているはずで、これは関数の種類を見分けるのに使える。また オブジェクト指向式のプログラムだと関数の所属情報がプリフィクスに 入っていることがあり、貴重な情報になる。(例: rb_str_push)

略語の調査

わかりにくい略語があればリストアップしておいて早めに調べる。 例えば「GC」と書いてあった場合、それが garbage collection なのか graphic context なのかでずいぶん話が違ってしまう。 英語だと単語の頭文字をとるとか、母音をなくすとかが多い。 特に対象プログラムの分野で有名な略語は問答無用で使われるので あらかじめチェックしておく。

筆者の記憶にある中から一つ例を挙げよう。 とある Lisp 処理系で、 プログラム全体で「blt」というプリフィクスが使われているのだが、 これが何を表しているのかわからなくて困ったことがある。 これは実は built-in function (組み込み関数) のことであった。 わかってみると単純なことだが、これがわかるのとわからないのでは ずいぶん難易度が違う。

データ構造を知る

プログラムなんて、データ構造がどうなっているのか分かれば もう半分勝ったようなものだ。コードを書くときも、 コードに逐一コメントを付けるよりデータ構造 (だけ) を 解説するほうがはるかに役に立つ。 (と、何かの本に書いてあったんだけど、なんだっけ?)

※ 追記:『プログラム書法』だった。以下、同書の p.168 より引用する。 「プログラムに解説をつけるための、もっとも効果的な方法の一つは、 単にデータの割り付けかたをくわしく説明する、というものである。 おもな変数について、その値としてはどんなものが可能かを示し、 それが変って行くようすを説明すれば、それだけでプログラムの解説は、 ずいぶん進んだといってよい。」
ちなみにこの本の原書は 1974 年に出版されている。

※ 追記2:『Cプログラミング診断室』でも似たようなコメントを発見した。 以下、同書の p.78 より引用する。 「フローチャートは禁止しましょう。フローチャートは、 制御の流れを「もろ」に書けてしまうのでよくありません。 プログラムは、データを処理するためにあり、データの違いによって 制御の流れが変更されます。あくまでも、データが主体です。 変数、引数などのデータをどう定義するかで、プログラムの組みやすさは 大幅に改良されます。データ構造がどうなっているかの図のほうが、 フローチャートよりはるかに役立ちます。データの意味だけは、 しっかり書きましょう。」

閑話休題。 C でデータ構造を作るならもちろん struct か union を使うはずだ。 そういう重要構造はヘッダファイルで定義されていることが多い。 もちろん内部構造は .c で定義されることもあるし動的に構築される データ構造もあるので、最終的には関数を読んでいかないとわからない。 それでもまずはヘッダファイルを読むべきだろう。ヘッダファイルを 読むときにもやはりファイル名は重要である。例えば言語処理系で frame.h というファイルがあったら、たぶんスタックフレームの定義だ。

データ構造を予測する時は構造体メンバに注目する。構造体の定義中に next というポインタがあればリンクリストだろうと想像できる。 同様に、parent・children・sibling と言った要素があれば十中八九 ツリーだ。

関数同士の呼び出し関係を把握する

関数名の次に重要な情報。 特に関数の数が多い場合はこれが重要である。 このへんはツールを活用したい。 図にしてくれるツールがあればそれが一番いいが、 なければ特に重要な部分だけでいいので自分で図を書いておくといい。 図に凝る必要はないので、裏紙にざっと描けば十分だろう。

ちなみにこのこの呼び出しの関係を図にしたもののことを コールグラフ (call graph) と言うことがある。 ソースコードに書いてある呼び出し関係を そのまま図にしたのが静的なコールグラフ (static call graph) で、 実際に動作させたときに呼び出した関数だけを書いた図が 動的なコールグラフ (dynamic call graph) である。

ただ、検索した感じでは、日本語の文章だと「コールグラフ」 は暗黙のうちに dynamic call graph を指し、 static call graph は「関数呼び出し関係」と言うことが多いようだ。 だが static と dynamic で対になっているほうがわかりやすいので 筆者は動的コールグラフ・静的コールグラフと呼ぶことにしている。

関数を読む

動作を読んでいく。関数関連図を見て、パートごとに読んで理解していくのが いい。古典的なトップダウン式プログラムなら main から読むのがいいだろう し、GUI みたいにメインループに入ってぐるぐる、なプログラムならコールバッ クを順番に把握していくのがいいだろう。このへんはとにかく関数がどういう 関係で構築されているかによる。

またここでもシンボルの定義位置を調べたりするためにツールを活用する (後 述)。また前述のとおり、関数や変数が定義されているファイル名それ自体も、 カテゴリを示してくれるので重要な情報である。

それと、読んでいくときはまず「何を読まないか」を考えたほうがいい。 どんなに工夫しても全部を全部読むのでは時間がかかりすぎる。 エラー処理や不要な分岐、あまり使われない場合分けなどは全部飛ばすようにする。 ガード文がうまく使われているとこのステップが非常に楽である。 逆に言うと、こういうところを楽にするために例外だとかガード文を使うのだ。

好みに書き換えてみる

これは「この段階でやる」という類のものではなくて手法の一つである。 人間の頭というのは不思議なもので、できるだけ身体のいろんな場所を 使いながらやったことは記憶に残りやすい。パソコンのキーボードより 原稿用紙のほうがいい、という人が少なからずいるのは、単なる懐古趣味ではなく そういうことも関係しているのではないかと思う。

そういうわけで、単にモニタで読むというのは非常に身体に残りにくいので、 書き換えながら読む。そうするとわりと早く身体がコードに馴染んでくることが 多い。気にくわない名前やコードがあったら書き換える。わかりずらい略語は メモるだけでなく省略しない語に置換してしまってもよい。

ただし、当然のことだが書き換えるときはオリジナルのソースは別に残し、 途中で辻褄が合わないと思ったら元のソースを見て確認すること。でないと 自分の単純ミスで何時間も悩む羽目になる。

書き換えて動かす

前項と似ているが、こちらは実際にプログラムを動かしてみる。例えば 動作のわかりにくいところでパラメータやコードをちょっとだけ変えて 動かしてみる。そうすると当然動きが変わるから、コードがどういう意味 なのか類推できる。

これまた言うまでもないが、オリジナルのバイナリは残しておいて 同じことを両方にやってみるべきである。

名前の大切さ

ここまで書いてきて気付いたのだが、ソースコードの読解とは 「名前」の調査であるようだ。ファイル名・関数名・変数名・型名・ メンバ名など、プログラムは名前のかたまりだ。名前はプログラムを 抽象化する最大の武器なのであたりまえと言えばあたりまえだが、 この点を意識して読むとかなり効率が違うのではなかろうか。

歴史を読む

プログラムにはたいてい変更個所の履歴を書いた文書が付いている。 例えば GNU のソフトウェアだと必ず ChangeLog というファイルがある。 これは「プログラムがそうなっている理由」を知るのには最高に役に立つ。

また CVS や SCCS のようなバージョン管理システムを使っていて しかもそれにアクセスできる場合は、ChangeLog 以上に利用価値が高い。 CVS を例に取ると、特定の行を最後に変更した場所を表示する cvs annotate、指定した版からの差分を取る cvs diff などが便利だ。

さらに、開発用のメーリングリストやニュースがある場合はその過去ログを 入手してすぐに検索できるようにしておく。変更の理由がズバリ載っている ことが多いからだ。もちろん Web 上で検索できるならそれでもいい。

動的解析用ツール

動的解析ではデバッガやオブジェクトインスペクタを使って解析する。 例えば、実際にコードがどこを通ってどういうデータ構造を作るか、 なんていうことは、頭の中で考えるよりも実際にプログラムを動かして 結果を見るほうが早い。総合的なツールとしては次のようなものが使える。

他に、printf デバッグは非常に原始的な動的解析である。

また DDD (data display debugger) を使うとデータ構造を絵にして見せてくれる。 DDD は各種デバッガのフロントエンドとして構築されていて、 GUI を提供するのと同時に、いろいろなものを視覚化できるのである。 例えば http://www.gnu.org/software/ddd/all.jpg を見るとリンクトリストが視覚化されていることがわかる。

一般的には、テキストで書き出してから 後述の graphviz などを使って絵にするのがよいだろう。

また関数呼び出しの追跡 (プログラムのトレース) については ctrace というツールがある。 Linux では ltrace というのもあり、共有ライブラリ関数の呼び出しを追跡できる 筆者は未確認だが、Solaris 用の同様のツールで sotrace というものもあるそうだ。

特にシステムコールに限るなら strace, ktrace, truss も挙げられる。 Linux だと strace、BSD だと ktrace、Solaris だと truss だ。

ctrace
http://www.vicente.org/ctrace/
ltrace
http://packages.debian.org/unstable/utils/ltrace.html
strace
http://www.liacs.nl/~wichert/strace/
ktrace
(OS 添付)
truss
(OS 添付)

さらに、トレースはテキストで出力するだけでなく視覚化もできる、 という意見をいただいた。詳しいことは以下の本にのっているとのこと。

"Programming Languages" Ravi Sethi, Tom Stone; Addison-Wesley Pub Co; ISBN: 0201590654; 2nd edition (February 1996)
邦訳:『プログラミング言語の概念と構造』新装版、Ravi Sethi / Tom Stone、 ピアソン・エデュケーション、2002

この話が載っているのは p145 4.4「駆動は入れ子になった存続期間を持つ」のあたり。 「駆動木」という名前で動的コールグラフの話が出てくる。 「駆動木」は activation tree の訳らしい。

静的解析用ツール

使ってみた感じは global が一番汎用的に使えてよさそうだ。 ビューアは変に独自のを作られるより HTML を出力してもらって ブラウズはウェブブラウザに任せるのが便利だと思う。

gonzui

http://gonzui.sourceforge.net

様々な言語に対応しているソースコード検索エンジン。 ファイル内のインクリメンタル検索、ソースブラウズなどが可能。

global

http://www.gnu.org/software/global/

C 言語用。 クロスリファレンス、関数定義元の検索など、高機能。 同梱ツール htags を使うと HTML で出力できる。

不満なところ。グローバル変数と関数ポインタについても定義元を 示してほしい。あと htags で HTML を出力するとき予約語にタグを つけたりできるんだけども、ここでタグを直接出力するのではなく CSS を使って指定できるようにしてほしい。手元ではそのように 改造して使っている。グローバルに定義されている文字列定数を 書き変えるだけで変更できた。(ただし CSS 名は決め打ち)

さらに言えばマクロの中で定義された関数とかローカル変数まで カバーしてくれると完璧なのだが、そこまでやるとほとんど C コンパイラと同じかそれ以上の解析が必要になってしまうので 無理は言わないことにする。

cscope

http://cscope.sourceforge.net

C/C++/Java 用。 Curses ベースのソースコードビューア。独自コマンド体系なのが どうも面倒くさい。 そういうわけであんまり真面目に使ってないが、機能はかなり豊富だ。 global より cscope のほうが便利という人も多いようである。

ctags

http://ctags.sourceforge.net

基本的に C 言語用。vi のためのタグファイルを生成してくれる。 タグファイルというのは、関数や変数の位置を記録して、 そこに一発でジャンプするためのログのこと。 ちなみに、御存知だと思うが、Emacs 用の類似ツールは etags と言う。

lxr

http://lxr.sourceforge.net

Linux のソースコード読みを支援するために開発されたツール。 名前は Linux Cross Referencer から来ている。CGI として使うもの。 スタンドアロンツールが欲しかったのであまり真面目に使わなかったが、 これはこれでなかなか便利そうだ。

doxygen

http://www.stack.nl/~dimitri/doxygen/

基本的にはドキュメント生成ツール。コメントにドキュメントを書いておくと それを取り出して各種フォーマットで出力してくれる。 ソースコードを HTML などで出力できる機能もあり、それがわりと高機能だ。 ただしソースの HTML 化だけに使うには設定があまりに面倒である。

cxref

http://www.gedanken.demon.co.uk/cxref/

C cross referencing & Documentation tool、の名前のとおり クロスリファレンスとドキュメント生成のためのツール。それ用の コメントを付けとかないとドキュメントは生成されない。基本的には これはドキュメント生成のツールであって、クロスリファレンスは そのオマケみたいなもの。ソース読みが目的なら上記のどれかを使うほうがいい。

cflow

http://wh58-508.st.uni-magdeburg.de/sparemint/html/packages/cflow.html

昔から UNIX に付いてるツール。 C 言語の関数の呼び出し関係をテキストで表示してくれる。 今となってはメチャクチャ便利というものではないが、 シンプルだし、パイプへの出力に使えるので持ってて損はない。

また Fortran 用の静的解析ツールをまとめたページを 教えていただいたのでリンクしておく。

http://www.fortranlib.com/freesoft.htm

余談: コールグラフの視覚化について

コールグラフを図にしてくれるツールが なかなか見付からなかったのだが、つい先日情報をいただいた。

SXT

http://sxt.freeservers.com

単体で関数の関係を視覚化できる。 対応言語は C、dBASE、Fortran、Java、Lisp。 筆者が求めていたものにはこれが一番近いようだ。

VCG

http://rw4.cs.uni-sb.de/users/sander/html/gsvcg1.html

簡単な記述言語みたいなものを渡すとグラフを描いてくれるツール。 最近の cflow や bison だとこの VCG 向けの言語を直接吐いてくれる。 つまり、全自動で関数や構造体や規則の関係を視覚化できる。

graphviz

http://www.research.att.com/sw/tools/graphviz/

dot という言語でグラフを表現したものを渡すと、グラフを描いて 様々なフォーマットで出力してくれる。(PostScript, png, jpeg などなど)

これを使って実験的に作ってみたグラフが以下のもの。 ruby インタプリタの中心部だ。

ところで、この文書の以前の版ではコールグラフ視覚化の トピックにこだわりすぎていたように思う。 確かに、コールグラフを視覚化すると時によっては それ一発で動作が理解できるようなこともあるのだが、 必ずしも役立つとは限らない。 コールグラフが効果を発揮するのは、関数の数がそれなりにあり、 しかも関数呼び出しが深いときだ。逆に言うと、そういうときでないと役立たない。