読解の対象

本演習では読解の対象をC言語で記述されたソースコードとしました。

なぜC言語か

本演習では読解の対象をC言語で記述されたソースコードとします。C言語は実世界で広く使われていて、しかも高度な言語機能が欠けているため読解が容易です。

実世界でのC言語の利用

多くのFOSSがC言語で記述されているます。C言語で記述された著名なFOSSにはlinuxカーネルやapacheウェブサーバ、openssl暗号化ツールキット、ruby言語処理系、Xウィンドウシステムなどがあります。最新のバージョンではC++言語へ移行しましたが、以前はgccコンパイラコレクションもC言語で記述されていました。

読解を阻害する高度な言語機能の欠如

プログラミング言語の機能の多くが、ソースコードを書くことの支援を目的とします。以降に延べる通りそのような機能のなかには、ソースコードを読むことを困難にするものがあります。C言語には、そのような機能が欠けているため、開発者の負担が大きく、記述は冗長になります。その一方で 読解者の負担は低く、冗長な記述は読解の手掛りとなります。

_images/lang.svg

「読解を阻害する高度な言語機能」というのは筆者の経験に基づく偏見かもしれません。筆者の読解経験のほとんどがC言語で記述されたソースコードに対するものです。対象の言語に慣れることや、文字列検索よりも高度な読解支援ツールを用いることで、紹介した機能を阻害とは感じなくなるかもしれません。しかしそれを差し引いても、読解の手始めとして、C言語で記述されたソースコードを読むのが適当だと考えます。

独立した複数の名前空間とその操作

ソースコード中に出てきた名前がどの変数を指すのか、あるいはどの関数を指すのか一意に特定できるよう、言語処理系は名前の集合(名前空間)を管理しています。多くのプログラミング言語がモジュールあるいはライブラリ毎独立した名前空間を定義する機能を提供します。独立した名前空間があればモジュールの作者は、他のモジュール中の名前との衝突を心配することなく、関数や変数に好きな名前をつけることができます。開発者にとって便利な言語機能です。

あるモジュール(A)にて別のモジュール(DB)を利用することを記述すると、DB中の名前をAの名前空間に輸入できる、という機能を持つ言語があります。

次に示すのはruby言語での例です。

# A.rb
require 'DB'
require 'FILE'
# ...
h = open()
# ...
# DB.rb
# module DB
def open()
  ...
  return handle
end

A.rbにてモジュールDBをrequireして、DBの名前をAに輸入しています。輸入した結果、DB中で定義されたopenという名前で参照できる関数を、 Aからもそのままopenという名前で参照できるようになります。

ソースコードを読む立場からすると、openという名前をみてもその名前から出自が即座にわかりません。A.rb, DBモジュール、FILEモジュールから探す必要があります。

基本的にC言語にはライブラリ毎に独立した名前空間はありません。汎用ライブラリの作者は、自作のライブラリと他のライブラリが同じ1つの実行ファイルにリンクされたときに名前が衝突することを避けるために、自作ライブラリには、ライブラリ名をプレフィックスとする名前を使うという慣例が浸透しています。結果として、C言語では名前が長くなる傾向にあります。開発者の負担が増しますが、読解者は検索の手間無く名前だけから多くの情報を得ることができます。

libgtkというGUIライブラリがあります。ライブラリが公開する関数、マクロ、型には、gtk_, GTK_, Gtkというlibgtkに所属しているとすぐわかるプレフィックスがついています。従ってC言語で書かれたソースコードを読んでいて、それらのプレフィックスで始まる名前が出てくれば、その定義箇所をみつけるまでもなく、それはなにかGUIに関係あるということを推測できます。

rubyの例(A.rb)をC言語で書くと、きっと次のようになるでしょう。

/* A.c */
#include "DB.h"
#include "FILE.h"

# ...
h = db_open();
# ...

関数の多重定義

C++言語やJava言語では同じ名前で引数の型や個数が異なる関数を定義できます。次のようなソースコードを読んでいる、という状況を考えます。

  // ここに関心が湧いた。
  return add(3);
  // ...

int add(int x, int y)
{
    return x + y;
}

int add(int x)
{
    return x + 1;
}

...

ここで、関数呼び出し(add)に遭遇し、その定義に関心を持ったとします。

“add”という単純な文字列検索の結果だけでは関数定義箇所を決定できません。add(int x)とadd(int x, int y)の2つの定義が検索にひっかかります。2つの定義のどちらが呼び出されたかを知るには、呼び出し元でaddに渡されている実引数と検索結果中の仮引数を見比べる必要があります。

C言語には関数の多重定義機能が無いので、関数名を検索するだけで容易に定義に辿りつけます。呼び出し元の実引数を確認する手間はありません。

C++言語の持つ演算子の多重定義機能も、関数の多重定義同様にその定義を探すのに演算子のオペランドの型を確認する必要があります。

匿名関数

名前の無い関数を定義できる言語があります。次に示すのはJavaScriptの例です。

(function (x, y) { return x + y; }) (2, 3);
=> 5

名前を検索して名前と結合した関数定義を見るまでも無く、その場所に定義が記述されているという点で、匿名関数を使ったコードは読みやすそうです。しかし長時間の読解においては、逆に読み難くなる原因となり得ます。

匿名関数に関心を持ったとしても、それを名前で指し示すことができません。読解中、ソースコードから得られた様々な知見を多数記憶しておく必要があります。記憶しきれない場合はメモを取ります。名前が無いと、存在を記憶に維持しておいたり、メモをとるのが難しくなります。逆に名前があると、覚えるのが容易になり、名前で定義箇所を検索することができます。C言語には匿名関数は無く、型、関数、変数にはその定義を参照するための名前がつきます。

ただしC言語でも、関数への参照をポインタ変数に代入することができます。

...
foo->f();
...

この関数呼び出しの詳細を知るにはfが何を指しているか辿る必要があります。

foo->f = fptr1;
...
fptr1 = do_something_important;
...
void do_something_important()
{
        ...
}

ポインタ変数への代入箇所を起点にデータフローを逆方向にたどれば、(名前を持つ)関数定義に行きつくはずですが、たどるのは簡単ではないかもしれません。

ランタイム環境

プログラム言語がもたらす抽象化には、それを支える「裏方」がいます。インタプリタ型の言語であれば、そのインタプリタが「裏方」の実態です。コンパイル型の言語であれば、ランタイムライブラリと呼ばれるプログラム構築時に暗黙に連結されるライブラリが「裏方」の実態となります。

最近利用されている多くのプログラミング言語において、メモリ管理が抽象化されています。確保したメモリオブジェクトの開放について、陽に記述する必要がありません。プログラム実行中様々なタイミングで起動する、ガベージコレクタ(GC)という「裏方」がどこからも参照されていないメモリオブジェクトをみつけて、再利用できるよう回収してくれます。

GCを持つ言語を使えば、メモリオブジェクトを陽に書く必要は無くなるので、まず記述量が減ります。また開放し忘れや二重開放によるバグを気にする必要が減ります。

ところが読解する立場からすると、GCによって読解時の手掛かりが減ってしまいます。GCが無い言語で記述されたソースコード中であれば、メモリオブジェクト開放の指示が明記されているはずです。メモリオブジェクト開放の指示を目にしたら、以降それを指していた変数を使うつもりが無いという開発者の意図を知ることができます。

C言語自体にはGCありません。言語が提供する抽象化の度合いが低いので、GCに限らずランタイムライブラリが軽量です。他の言語であれば記述せずに済んだ処理を記述する必要があります。その記述が読解の上で手掛かりとなります。

ソースコードの位置付け

みなさんが読もうとしているソースコードを本演習でどのように位置付けについているかを説明します。

みなさんが関心があるのはプログラムの動作や振舞いです。インタプリタで実行されることを前提としたプログラムであれば、プログラムとソースコードは同じ物を指します。しかし本演習で対象としているのはC言語です。C言語で記述されたプログラムは、ソースコードからコンパイルされたバイナリコードです。逆アセンブルすることでバイナリコードを直接的に解析して関心を満すこともできますが、大変なコストがかかります。そこでバイナリコードの元となったソースコードを読むことになります。

_images/target.svg

ここまでプログラムと書いてきたものはより正確には、実行ファイルとそれにリンクされたライブラリに分類されます。演習に使うGNU/Linuxの環境では、実行ファイルは主に/usr/bin以下に、ライブラリは/usr/libあるいは/usr/lib64以下にlib*.soの名前でインストールされています。

ライブラリは、画面描画、暗号化処理など様々な「便利な」機能を提供します。実行ファイルとライブラリをリンクすれば、実行ファイルは自身でその機能を持たなくとも、ライブラリの提供する機能を活用できます。ライブラリは複数リンクすることができます。

実行ファイルあるいはライブラリは1つ以上のソースコードファイルから成ります。各ソースコードファイルはコンパイラによってオブジェクトファイルにコンパイルされます。オブジェクトファイルの拡張子は.oとなります。リンカによってオブジェクトファイルはリンクされて実行ファイルあるいはライブラリとなります。実行ファイルにライブラリをリンクする処理もリンカが担当します。コンパイル、リンクとさらに後述するソースコード生成処理を合せてビルド処理と呼びます。

GNU/Linuxシステムではコンパイラ、リンカとしてgccというコマンドが広く使われています。実行ファイルの名前は、通常開発者が指定します。特に名前を指定しないとa.outという名前になります。逆にa.outといった場合、実行ファイルの総称となります。

C言語の場合ソースコードファイルの拡張子は.cあるいは.hとなります。特に.hのことをヘッダファイルと呼びます。ヘッダファイルには定数値や変数あるいは関数宣言が記述されていて、複数の.cファイルから参照します。ヘッダファイルには、複数の.cファイルから共通に参照したいものだけが記述されているはずです。

ビルド処理の行程でソースコードを生成する場合もあります。例えばソフトウェアにある多数の機能のうち、どれを有効にしてビルド結果に組込むか、ということを利用者が目的に応じて調整可能なソフトウェアがあります。そのような調整を目的としたツールがソースコードツリーに組込まれており、調整の結果そのツールはヘッダファイルを生成します。ヘッダファイルの内容はコンパイラに読み込まれ、コンパイル処理に反映されます。図中ではr.cがr.c.inから、A.hがA.h.inから生成されるように描きましたが、一例にすぎません。ソースコード生成処理の元になるデータを何とするか、というのはソフトウェアそれぞれに違います。

プログラムに対して疑問が湧き、ソースコードを調査する場合、そのプログラムの疑問に関連のある.cファイルや.hファイルを特定する、特定できないまでも多数あるソースコードファイルからその部分集合に調査対象を絞り込むという作業が必要となります。ソースコード生成ツールやリンカ、コンパイラが実施した処理を遡ることになります。

ソースコードの入手と配置

ソースコードが無ければソースコードを読むことはできません。従って関心のあるソフトウェアについてソースコードを入手できるかどうか、できるならどこからどうやって入手するのかを調査するというのがコードリーディングの第0歩となります。そして調査をもとに実際にソースコードを入手して、ソースコードを読める状態にして、ようやく第1歩となります。

入手先

本演習で対象とするのはFOSSなので、ソースコードの入手については心配いりません。ソースコードは、アップストリームプロジェクトからとってくることもできます。Fedoraの開発プロジェクトが維持するリポジトリからも入手できます。

_images/srcrepo.svg

アップストリームプロジェクトからの入手

アップストリームプロジェクトからソースコードを入手する方が良いケースがあります。

  1. そもそもFedora経由でソフトウェアが提供されていない。
  2. 最新版が必要である(Fedora経由でソースコードを入手しても、それは古いバージョンである)。
  3. ソースコードを読むにあたりFedoraで施された調整を排除したい。

1.が理由でアップストリームプロジェクトからソースコードを入手するには、プロジェクト名などを検索語にウェブ検索でソースコード入手先を探す必要があります。2., 3.の場合、rpmコマンドでアップストリームプロジェクトのURLを知ることができます。そのURLから入手方法がわかるはずです。

lsコマンドを例にアップストリームプロジェクトのURL知る方法を先に説明しておきます。lsコマンドを対象としていますが、ファイルシステム上のパスがわかれば、同じ手順でURLがわかるはずです。

まずlsコマンドがファイルシステムどこにあるかを確認します:

$ which ls
/usr/bin/ls

/usr/bin/lsにあることがわかりました。このファイルがどのパッケージに所属するか調べます:

$ rpm -qf /usr/bin/ls
coreutils-8.15-7.fc17.x86_64

coreutilsというパッケージであることがわかります。このパッケージの詳細情報を調べます:

$ rpm -qi coreutils-8.15-7.fc17.x86_64
Name        : coreutils
Version     : 8.15
Release     : 7.fc17
Architecture: x86_64
Install Date: 2012年07月29日 02時57分14秒
Group       : System Environment/Base
Size        : 14003106
License     : GPLv3+
Signature   : RSA/SHA256, 2012年07月15日 02時17分38秒, Key ID 50e94c991aca3465
Source RPM  : coreutils-8.15-7.fc17.src.rpm
Build Date  : 2012年07月13日 20時55分52秒
Build Host  : x86-14.phx2.fedoraproject.org
Relocations : (not relocatable)
Packager    : Fedora Project
Vendor      : Fedora Project
URL         : http://www.gnu.org/software/coreutils/
Summary     : A set of basic GNU tools commonly used in shell scripts
Description :
These are the GNU core utilities.  This package is the combination of
the old GNU fileutils, sh-utils, and textutils packages.

URLの項目にhttp://www.gnu.org/software/coreutilsとあります。これがアップストリームプロジェクトのウェブページを指すURLとなります。

多くのアップストリームプロジェクトにおいて、開発中のソースコードが格納されたバージョン管理システム(VCS)のリポジトリが公開されています。git,svn(subversion)あるいはcvsといったコマンドを使うと、VCSリポジトリからソースコードを引き出すことができます。

以下はGNU helloのアップストリームプロジェクトが公開しているgitリポジトリからソースコードを取り出すコマンドラインです:

$ git clone git://git.savannah.gnu.org/hello.git

このコマンドラインで現在のディレクトリにhelloの名前でソースコードが配置されます。helloはgnulibというライブラリに依存しています。以下のコマンドラインで依存するライブラリも取り寄せることができます。

$ cd hello
$ git submodule init
$ git submodule update

(依存するライブラリを取り寄せることができるかどうか、というのはプロジェクトによって異なります。)

開発中のソースコードを外部開示せず、開発しているソフトウェアの動作が安定したタイミングで、あるいは定期的に、バージョン番号をつけて「リリース」する、という方法でソースコードを公開しているアップストリームプロジェクトもあります。bindネームサーバやbashシェルインタプリタがその例です。

この場合、ウェブブラウザなどでダウンロードすることになります。ダウンロードは1つのファイルにアーカイブされた上に圧縮されています。ソースコードを読むにはこれを伸張、解凍する必要があります。主要な拡張子と伸張、解凍のコマンドラインを延べます。

foo.tar.gz:

tar zxvf foo.tar.gz

foo.tar.bz2:

tar jxvf foo.tar.bz2

foo.tar.xz:

tar Jxvf foo.tar.xz

Fedoraの開発プロジェクトが維持するリポジトリからの入手

Fedoraから入手する場合手順は複雑になりますが、どこから入手すれば良いかということを調べる手間は要りません。

おおまかな手順は次のようになります。

  1. ソースパッケージリポジトリの設定変更
  2. 目的のソフトウェアに対するパッケージ名の特定(説明済み)
  3. ソースパッケージのダウンロード
  4. 依存パッケージのインストール
  5. ソースパッケージのインストール
  6. ソースコードツリーの合成

準備として、ソースパッケージリポジトリの設定を変更する必要があります。管理者権限が必要となります。/etc/yum.repos.d/fedora.repoの[fedora-source]のセクションにあるenableのフィールドを1に設定して保存します:

[fedora-source]
name=Fedora $releasever - Source
failovermethod=priority
...
enabled=1
...

/etc/yum.repos.d/fedora-updates.repoの[updates-source]のセクションにあるenableのフィールドを1に設定して保存します:

[updates-source]
name=Fedora $releasever - Updates Source
failovermethod=priority
...
enabled=1
...

アップストリームプロジェクトからの入手にて説明した手順に従い関心のあるファイルに対するパッケージの名前を調べます。そのパッケージの名前を引数に–sourceオプションをつけてyumdownloader を実行します。coreutils-8.15-7.fc17.x86_64を例に実行してみます:

$ yumdownloader --source coreutils-8.15-7.fc17.x86_64
読み込んだプラグイン:auto-update-debuginfo, presto, priorities, refresh-packagekit
coreutils-8.15-7.fc17.src.rpm                            | 4.8 MB     00:06

coreutils-8.15-7.fc17.src.rpmという名前のファイルがダウンロードされました。これがソースパッケージファイルです。src.rpmという拡張子を持ちます。

次にソースコードツリーの合成に必要となるソフトウェアをインストールします。入手したソースパッケージファイルを引数にyum-builddepを呼び出します。インストールには管理者権限が必要となります:

# yum-builddep coreutils-8.15-7.fc17.src.rpm

ここからの手順が複雑です。ソースパッケージをインストールします:

$ rpm -ivh coreutils-8.15-7.fc17.src.rpm

すると ~/rpmbuild 以下のソースコードツリーを合成するのに必要となるファイル群が配置されます。

いよいよソースコードツリーを合成します。パッケージの名前からバージョン番号等と取り去った先頭部分に .spec をつけた名前を持つファイルが~/rpmbuild/SPECSにあるはずです。これを引数にrpmbuild -bpを呼び出します:

$ rpmbuild -bp ~/rpmbuild/SPECS/coreutils.spec

これで ~/rpmbuild/BUILD 以下にソースコードツリーが合成されているはずです。

配置先

繰り返しになりますが、ソースコードなくしてコードリーディングはできません。入手したソースコードは必要なときにすぐに参照できるよう適切な場所に配置しておきます。

筆者が勤務先で、業務に関係のあるソースコードを全て入手して配置したファイルシステムを保守しています。そこでは以下のスキームに従い各ソースコードツリーを配置しています:

/srv/sources/sources/[a-zA-Z0-9]/パッケージ名/バージョン名/pre-build/ソースコードツリー

/srv/sources以下を分散ファイルツリー(NFS)として同僚と共有しています。

演習では、入手先に応じて3つのディレクトリを使うことにします。

  • ~/upstream
  • ~/released
  • ~/fedora

ソースコードツリーの構成

入手したソースコードを含むディレクトリとその配下にあるサブディレクトリとファイルを指してソースコードツリーと言います。

ソースコードツリーにはC言語で記述されたソースコードファイル以外にも様々なものが含まれています。ソースコードツリーに何が含まれていて、どのような構成になっているか、というのはソフトウェアによって異なります。

以下に典型的に含まれているのを列挙します。

ドキュメント
  • ライセンス
  • そのソフトウェアについての簡単な説明をしたファイル
  • ビルド、セルフテストとインストールの手順を説明したファイル
  • 典型的な使い方を説明したユーザーマニュアル
  • (ライブラリであれば)APIの詳細を説明したリファレンスマニュアル
  • 改変履歴
  • ...
ビルドスクリプト
ビルド処理用のツールへの入力
テストスイート
プログラムが期待通りの動作をするかどうかをセルフテストに使う補助プログラム群とテストケース
プログラムが利用するデータ
  • 画像ファイル
  • フォントファイル
  • 設定ファイル(のサンプル)
  • ...
翻訳カタログ
プログラムが出力するメッセージやラベルの各国語別の翻訳
ソースコードファイル(狭義のソースコード)
  • .cファイルや.hファイル
  • ビルドスクリプトによってソースコードファイルを自動生成する場合、その入力

GNU helloの例

GNU helloはFOSSの開発者に標準的なソースコードツリーの構成を例示することを目的としたフリーソフトウェアです。多くの重要ソフトウェアがこの構成にある程度従っているので参考になります。

ソースコードツリーの構成の説明を目的としているので、含まれているソースコードファイルに特別な意味はありません。C言語の有名なサンプルプログラムとして”hello, worldn”を表示する短いプログラムがあります。

#include <stdio.h>

int main(void)
{
    printf("hello, world\n");
    return 0;
}

これを拡張したものがhelloに含まれています。

以下は、helloのソースコードツリーに対するtreeコマンドの出力です。括弧の中にファイルとディレクトリの役割りを追記してあります。

$ tree hello
hello
|-- AUTHORS                 (ドキュメント)
|-- COPYING                 (ドキュメント)
|-- ChangeLog               (ドキュメント)
|-- ChangeLog.O             (ドキュメント)
|-- GNUmakefile             (ビルドスクリプト)
|-- Makefile.am             (ビルドスクリプト)
|-- NEWS                    (ドキュメント)
|-- README                  (ドキュメント)
|-- README-alpha            (ドキュメント)
|-- README-dev              (ドキュメント)
|-- README-release          (ドキュメント)
|-- THANKS                  (ドキュメント)
|-- TODO                    (ドキュメント)
|-- bootstrap               (ビルドスクリプト)
|-- bootstrap.conf          (ビルドスクリプト)
|-- build-aux               (ビルドスクリプト)
|   |-- config.rpath
|   |-- gendocs.sh
|   |-- useless-if-before-free
|   `-- vc-list-files
|-- cfg.mk                  (ビルドスクリプト)
|-- configure.ac            (ビルドスクリプト)
|-- contrib
|   |-- ChangeLog
|   |-- Makefile.am
|   |-- de_franconian_po.txt
|   `-- evolution.txt
|-- doc                     (ドキュメント)
|   |-- ChangeLog
|   |-- Makefile.am
|   |-- gendocs_template
|   `-- hello.texi
|-- gnulib                  (プロジェクト外部で開発されているライブラリ)
|-- man                     (ドキュメント(自動生成?))
|   |-- ChangeLog
|   `-- Makefile.am
|-- po                      (翻訳カタログ群)
|   |-- ChangeLog
|   |-- LINGUAS
|   |-- Makevars.template
|   |-- POTFILES.in
|   |-- Rules-quot
|   |-- bg.po
|   |-- boldquot.sed
|   |-- ca.po
|   |-- da.po
|   |-- de.po
|   |-- el.po
|   |-- en@boldquot.header
|   |-- en@boldquot.po
|   |-- en@quot.header
|   |-- en@quot.po
|   |-- eo.po
|   |-- es.po
|   |-- et.po
|   |-- eu.po
|   |-- fa.po
|   |-- fi.po
|   |-- fr.po
|   |-- ga.po
|   |-- gl.po
|   |-- he.po
|   |-- hr.po
|   |-- hu.po
|   |-- id.po
|   |-- insert-header.sin
|   |-- it.po
|   |-- ja.po
|   |-- ka.po
|   |-- ko.po
|   |-- lv.po
|   |-- ms.po
|   |-- nb.po
|   |-- nl.po
|   |-- nn.po
|   |-- pl.po
|   |-- pt.po
|   |-- pt_BR.po
|   |-- quot.sed
|   |-- remove-potcdate.sin
|   |-- rm.po
|   |-- ro.po
|   |-- ru.po
|   |-- sk.po
|   |-- sl.po
|   |-- sr.po
|   |-- sv.po
|   |-- th.po
|   |-- tr.po
|   |-- uk.po
|   |-- vi.po
|   |-- zh_CN.po
|   `-- zh_TW.po
|-- src
|   |-- ChangeLog           (ドキュメント)
|   |-- Makefile.am         (ビルドスクリプト)
|   |-- hello.c             (ソースコードファイル)
|   `-- system.h            (ソースコードファイル)
`-- tests                   (テストスイート)
    |-- ChangeLog
    |-- Makefile.am
    |-- greeting-1
    |-- greeting-2
    |-- hello-1
    |-- last-1
    `-- traditional-1

8 directories, 98 files