ソースコード以外の情報源

コードリーディングのモデルにて、コードリーディングにおける基底知識が重要であることを説明しました。ここではコードリーディングを実施せずに得られる基底知識について説明します。

  • 文章
  • プログラムの実行結果
  • 実行環境

文章

本演習のように訓練としてソースコードを読むこと自体を目的にソースコードを読む、というのは特別です。通常は、何かソフトウェアの動作を理解する、といった目的がありソースコードを読むのは手段です。別の手段で目的を達成できるのであれば、その手段を使うことを考えるべきでしょう。自然言語で書かれた文章を読むというのはその手段の一つです。

ドメイン知識

ソースコードを読む前に、そのソフトウェアが対象とするドメイン知識を仕入れておくのが良いでしょう。たとえば会計処理を支援するソフトウェアについて、会計の知識無しにソースコードを読むのは、不可能ではないにしても現実的ではありません。会計について学んでおけば、コード全体で何を実現しようとしているのかを推測したり、型名、変数名、関数名からその役割を知ることができます。

特に公開されている仕様を実装したソフトウェアや公開されている仕様に立脚したソフトウェアについては、その仕様書が大変参考になります。ただし仕様書によっては1000ページを越えるものがあります。対象ソフトウェアの包括的な理解が目的ではなく、局所的な疑問を解決するだけであれば全体を読む必要は無いかもしれません。仕様書先頭の方に記載されているであろう仕様の目的に目を通したら、ソースコードを読みはじめます。意味がわからないがしかし重要と思われる単語が出てきたところで、その単語の意味を仕様書で調べるというように、ソースコードと仕様書を交互に読むのが良いでしょう。そのためにも、ソースコードを読み始める前に仕様書を入手して手近に置いておきましょう。

仕様書が絶対正しいわけではないことに注意して下さい。仕様書自体が改訂されることがあります。また相互互換性のためといった理由で、仕様書の記述からわざと逸脱して実装した箇所もあります。そういった箇所はたいていソースコード中にコメントがありますが、気になるところはソースコードと仕様書を照し合せて下さい。

ハードウェア、例えばCPUの仕様書はそのベンダーのウェブサイトから入手できることがあります。

プロトコルを含むネットワーク処理の仕様はRFCとして公開されていることがあります。RFCに準拠するよう作成したソフトウェアであれば、そのソースコードのコメント中に、あるいは付属ドキュメント中で、参照したRFCについて記載があるはずです。bindネームサーバやopenldapのようにソースコードツリーのなかに rfc をそのままコピーして同梱しているものもあります。

次に示すのは linuxカーネルのipv6プロトコルスタック実装中(net/ipv6/udp.c)から抜粋した例です。

if (uh->check == 0) {
        /* RFC 2460 section 8.1 says that we SHOULD log
           this error. Well, it is reasonable.
         */
        LIMIT_NETDEBUG(KERN_INFO "IPv6: udp checksum is 0\n");
        goto discard;
}

この条件分岐中の処理について、RFC 2460に由来することをの延べています。

case AF_INET6:
        if (addr_len < SIN6_LEN_RFC2133)
                return -EINVAL;
        daddr = &sin6->sin6_addr;
        break;

アドレスの長さの上限が RFC 2133 に由来する定数のようです。

次に示すのはipv4プロトコルスタック実装中(net/ipv4/tcp_minisocks.c)にあるRFCから逸脱していることを説明した箇所です。

/* Out of window segment.

   All the segments are ACKed immediately.

   The only exception is new SYN. We accept it, if it is
   not old duplicate and we are not in danger to be killed
   by delayed old duplicates. RFC check is that it has
   newer sequence number works at rates <40Mbit/sec.
   However, if paws works, it is reliable AND even more,
   we even may relax silly seq space cutoff.

   RED-PEN: we violate main RFC requirement, if this SYN will appear
   old duplicate (i.e. we receive RST in reply to SYN-ACK),
   we must return socket to time-wait state. It is not good,
   but not fatal yet.
 */

if (th->syn && !th->rst && !th->ack && !paws_reject &&
    (after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt) ||
     (tmp_opt.saw_tstamp &&
      (s32)(tcptw->tw_ts_recent - tmp_opt.rcv_tsval) < 0))) {
        u32 isn = tcptw->tw_snd_nxt + 65535 + 2;
        if (isn == 0)
                isn++;
        TCP_SKB_CB(skb)->when = isn;
        return TCP_TW_SYN;
}

アルゴリズム、データ構造

特別なアルゴリズムやデータ構造を使っている場合、ソースコード中でその実装にあたり参照した論文や書籍について言及していることがあります。ソースコードを読む前に読んでおくべきです。

次に示すのは高可用クラスター(パッケージ名corosync)のソースコードからの引用です。クラスターノード間での同報通信において、通信の順序が全てのノードで同じになる必要があります。そこでtotemと呼ばれるアルゴリズムが用いられています。コメント中にTotemの出典が記載されています。

/*
 * The first version of this code was based upon Yair Amir's PhD thesis:
 *  http://www.cs.jhu.edu/~yairamir/phd.ps) (ch4,5).
 *
 * The current version of totemsrp implements the Totem protocol specified in:
 *  http://citeseer.ist.psu.edu/amir95totem.html
 ...
 */

 /* 出典: corosync/exec/totemsrp.c */

ソースコードツリーやパッケージに同梱されたドキュメント

プログラム自体に説明が内蔵されていたり、あるいはソースコードツリーにドキュメントが同梱されていることもあります。

ソフトウェアがパッケージとして入手できる場合、ソースコードツリー中のドキュメントはパッケージに含まれている、あるいは別のパッケージとして入手できるかもしれません。多くのソフトウェアにおいて、マニュアルが実行に必須ではないので、ディスクの占有領域を少しでも減らせるようにマニュアル部分だけを独立したパッケージとしているソフトウェアもあります。パッケージA*に対して、 *A-docs、A-manual といった名前がついています。

パッケージ自体の説明を見る方法をパッケージ情報の問合せにて紹介しました。例を挙げます:

rpm -qi gcc
Name        : gcc
Version     : 4.7.2
Release     : 2.fc17
Architecture: x86_64
Install Date: 2012年10月01日 10時55分58秒
Group       : Development/Languages
Size        : 33801653
License     : GPLv3+ and GPLv3+ with exceptions and GPLv2+ with exceptions and LGPLv2+ and BSD
Signature   : RSA/SHA256, 2012年09月25日 05時20分45秒, Key ID 50e94c991aca3465
Source RPM  : gcc-4.7.2-2.fc17.src.rpm
Build Date  : 2012年09月22日 01時37分18秒
Build Host  : x86-03.phx2.fedoraproject.org
Relocations : (not relocatable)
Packager    : Fedora Project
Vendor      : Fedora Project
URL         : http://gcc.gnu.org
Summary     : Various compilers (C, C++, Objective-C, Java, ...)
Description :
The gcc package contains the GNU Compiler Collection version 4.7.
You'll need this package in order to compile C code.

オンラインマニュアル(man)

専用の閲覧プログラムで読むことを想定したマニュアルがソフトウェアに付属している場合があります。

主要な閲覧プログラムにmanとinfoがあります。manコマンドの方がより一般的です。

manコマンドは引数で指定したトピックについて、ページャーで表示します。

man [セクション番号] TOPIC

TOPICにはコマンド名、システムコール名、ライブラリ関数名、設定ファイル名などを指定します。ある関数名に対して同盟のコマンド名が存在する場合があります。たとえばprintfという標準Cライブラリの関数と同名のコマンドが/usr/bin/printfにあります。セクション番号を指定することで2つを区別できます。manのオンラインマニュアルをmanで見るとセクション番号の説明を読めます。以下に引用します。

1 実行プログラムまたはシェルのコマンド2 システムコール (カーネルが提供する関数)3 ライブラリコール (システムライブラリに含まれる関数)4 スペシャルファイル (通常 /dev に置かれている)5 ファイルのフォーマットとその約束事。例えば /etc/passwd など6 ゲーム7 マクロのパッケージとその約束事。例えば man(7), groff(7) など8 システム管理用のコマンド (通常は root 専用)9 カーネルルーチン [非標準]

manコマンドを使って閲覧できるTOPICのマニュアルのことを「TOPICのmanページ」と呼ぶことがあります。特に TOPIC(N) と表記した場合、セクションNにあるTOPICのmanページを指します。

正確にTOPICの名称がわからない場合、-kをつけてキーワード検索ができます。例:

$ man -k compiler
B (3pm)              - The Perl Compiler Backend
B::Deparse (3pm)     - Perl compiler backend to produce perl code
cgcc (1)             - Compiler wrapper to run Sparse after compiling
checkmodule (8)      - SELinux policy module compiler
checkpolicy (8)      - SELinux policy compiler
compile_et (1)       - error table compiler
...

manコマンドでオンラインマニュアルを閲覧するには、オンラインマニュアルの元となるデータがシステムにインストールされていることが前提となります。データの実体はファイルであり、「TOPIC名.セクション番号」というファイル名を付けることが慣例となっています。ソースコードツリーなどを入手してシステムにインストールされていない生のmanページデータ、すなわち「TOPIC名.セクション番号」という名前を持ったファイルを読みたい場合、以下の通り-lオプションを使います。

$ man -l ファイル名

オンラインマニュアル(info)

infoパッケージに含まれるinfoコマンドで閲覧できるオンラインマニュアルがあります。manコマンドと比べてその数は少ないのですが、gccコンパイラコレクション、gdbデバッガ、glibc Cライブラリといった極めて重要なソフトウェアのオンラインマニュアルがinfo用に用意されています。

$ info [TOPIC]

で閲覧できます。TOPICを指定しない場合、システムにインストールされたinfo形式のドキュメントより抽出したTOPICの一覧が提示されます。そのなかからTOPICを選んで読むことができます。

manページと異なりinfo形式のドキュメントは複数ページからなり、さらにハイパーテキスト化されています。カーソルキーでドキュメント中の1ページ内を移動します。ページ末尾でスペースキーを押すと次のページに進みます。バックスペースを押すと前のページに戻ります。*ではじまる文字列はリンクになっていて、カーソルをそこに移動させてリターンキーを押すとその文字列で指定されたトピックの説明に移動できます。

ヘルプメッセージ

引数として–helpあるいは-hあるいはhelpを与えて実行すると説明を表示するプログラムがあります:

$ ls --help
使用法: ls [オプション]... [ファイル]...
FILE に関する情報を一覧表示します (デフォルトは現在のディレクトリ)。
-cftuvSUX または --sort が指定されない限り、要素はアルファベット順で並べ替えられます。

長いオプションに必須の引数は短いオプションにも必須です。
  -a, --all                  . で始まる要素を無視しない
  -A, --almost-all           . および .. を一覧表示しない
  ...

Fedoraに含まれるソフトウェアの多くはこの慣例に従っています。ソースコードを読んででも解決したい疑問の解決のための情報としては不十分ですが、無いより余程ましです。

ヘルプメッセージの良いところは、それがそのまま痕跡文字列として使える点です。ただし画面に表示されている説明が翻訳文字列の可能性があります。翻訳前のメッセージであればソースコード中に埋め込まれていて痕跡文字列として使えるかもしれません。ところが翻訳後のメッセージはソースコードとは別の翻訳カタログファイルに由来しているかもしれません。LANG環境変数を変更した環境でプログラムを実行することで翻訳メッセージの使用を回避できます。

$ LANG=C ls --help
Usage: ls [OPTION]... [FILE]...
List information about the FILEs (the current directory by default).
Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.

Mandatory arguments to long options are mandatory for short options too.
  -a, --all                  do not ignore entries starting with .
  -A, --almost-all           do not list implied . and ..
  ...

ところで、もし翻訳メッセージを表示する仕掛けに関心があるならLANGが痕跡文字列となりますね。

ソースコードツリー中のドキュメント

専用の閲覧プログラムを必要としないテキストファイルやhtmlファイル(以下ドキュメントファイル)がソースコードツリー中に含まれている場合があります。パッケージによっては、パッケージの一部としてそれらのファイルがインストールされている場合/usr/share/doc/パッケージ名以下に配置されます。

パッケージにドキュメントが収録されていない場合、ソースコードツリーを確認してみましょう。掘り出し物がみつかるかもしれません。

ソースコードツリー中のどこにどのようなドキュメントがあるか、というのはソフトウェア毎に異なりますが、典型的なものとしてGNU helloの例が参考になります。

文章全般、特にソースコードツリー中のドキュメントを参照するときは、それが保守されているかどうかに注意して下さい。ソースコードについて開発が大きく先行してしまい、その説明が記載された文章が追い付いていない、あるいは放置されている、ということがあります。良く保守されていたとしても間違いがあるかもしれません。最終的にはソースコードで動作を確認する必要がある、ということになります。

プログラムの実行結果

ログ、エラーあるいは警告メッセージなどプログラムを実行したときに得られる情報があります。これを実行時情報と呼ぶことにします。実行時情報は読解を進める上で貴重な手掛かりとなります。逆に得られた実行時情報の意味を調べるために読解することもあります。ここではFedora上で動作するプログラムの典型的な実行時情報の情報源を紹介します。

実行時情報があれば読解の負担は大幅に減ります。しかし十分な実行時情報を得られないまま読解を進めないといけないこともあります。例えば特別な環境やハードウェアを必要とするため実行が困難な場合や再現性の低い障害について、その原因を調べる場合があてはあります。

実行時情報はある入力を指定して実行すると得られるものです。入力や実行状況によって情報が変わる可能性があります。特定の実行時情報に頼りすぎて斜め読みをするとソースコードの意味を誤解してしまうことがあります。

標準出力と標準エラー出力

コマンドラインインターフェイスを想定したプログラムをターミナル上で動作するシェルから起動すると、その実体(プロセス)には2つのファイルが関連付けられています。

標準出力(stdout)
正常に処理が進行した場合に対する、結果の出力先
標準エラー出力(stderr)
異常が発生した場合に対する、エラーや警告メッセージなどの出力先

特に指定しない限り、stdout, stderrはプログラムを起動したターミナルと関連付けられています。手動で実行した場合、ユーザはその出力を見ることになります。

printfは標準出力への書き出しに使う代表的な関数です。

printf("%s\n", "something normal data");

fprintfは標準エラー出力への書き出しに使う代表的な関数です。

fprintf(stderr, "%s\n", "something error message");

出力を保存するにはシェルのリダイレクト記号を使います:

標準出力を保存する。
標準エラー出力の内容はそのままターミナルに出る。
# ./a.out > /tmp/stdout.txt

標準エラー出力の保存する。
標準出力の内容はそのままターミナルに出る。
# ./a.out 2> /tmp/stderr.txt

標準出力と標準エラー出力の内容を別々のファイルに保存する。
# ./a.out > /tmp/stdout.txt 2> /tmp/stderr.txt

標準出力を捨てて標準エラー出力の内容を保存する。
# ./a.out > /dev/null 2> /tmp/stderr.txt

標準出力、標準エラー出力の両方をまとめて保存する。
# ./a.out > /tmp/stdout+err.txt 2>&1

終了ステータス

プロセスは終了時、int型の値を一つだけそのプロセスを起動したプロセス(親プロセス)に伝えることができます。この整数値を終了ステータスと呼びます。main関数の返り値あるいはexit関数(あるいは_exitシステムコール)の引数が終了ステータスとなります。

/* foo.c */
void
func(void)
{
      /* ... */
      exit(42);
}

int
main(int argc, char** argv)
{
      /* ... */
      func();
      /* ... */
      return 17;
}

ターミナル上で動作するシェルから対話的に起動したプロセスの終了ステータスは、$?に格納されています。:

$ gcc foo.c
$ ./a.out
$ echo $?
17

終了ステータスの意味はプログラムによって様々で、正確な意味を知るにはそのプログラムのソースコードを調べる必要があります。ただ慣例として処理が「成功」したことを表現するのに0を使います。

ログ

httpdやsendmailのようにデーモンプログラムとしてバックグランドで動作することを想定したプログラムは、通常起動直後にターミナルとの接続を破棄します。結果的に標準出力や標準エラーへの書き込みはしないか、しても捨てられます。かわりにシステムログへログを残します。ログを残すにはsyslog関数を使います。

syslog(priority, "%s", "this is log message\n");

日時、プログラム名とともにログは /var/log/messages へ記録されます。このファイルを見るには管理者権限が必要となることに注意して下さい。

# cat /var/log/messages
...
Nov  7 01:04:16 localhost NetworkManager[631]: <info> dhclient started with pid 16694
Nov  7 01:04:16 localhost NetworkManager[631]: <info> Activation (em1) Stage 3 of 5 (IP Configure Start) complete.
Nov  7 01:04:16 localhost dhclient[16694]: Internet Systems Consortium DHCP Client 4.2.4-P2
Nov  7 01:04:16 localhost dhclient[16694]: Copyright 2004-2012 Internet Systems Consortium.
Nov  7 01:04:16 localhost dhclient[16694]: All rights reserved.

loggerコマンドを使うとターミナルから/var/log/messagesへ書き込むことができます。

$ /usr/bin/logger -t name-of-this-program "EXAMPLE"
# tail -1 /var/log/messages
Nov  7 02:57:12 localhost name-of-this-program: EXAMPLE

ログファイルを独自に持つプログラムもあります。独自のログファイルはたいてい/var/log以下に配置されます。

設定ファイル、コマンドラインオプションあるいは環境変数を指定することで、通常より詳細なログを出力する 「デバッグモード」(あるいは「冗長(verbose)メッセージモード」)を持っているプログラムもあります。デバッグモードを有効にすることで、痕跡文字列がみつかるかもしれません。ただしデバッグモードの存在がドキュメント等に記載されていない可能性があります。デバッグモードの存在とそれを有効にする方法をみつけるためにソースコードを読む必要に迫られるかもしれません。

実行環境

調査の対象となるプログラムの実行時の実体であるプロセスはどういったもので、どのような環境で実行されるでしょうか。プロセスから直接的に詳細な実行時情報を得られない場合であっても、プロセスを取り囲む環境から荒い実行時情報を取り出すことができます。この情報を活用するには、プロセスやその実行環境に対する理解が必要となります。

linuxカーネルとlibc

_images/runtime-overview.svg

linuxカーネル上では複数のプロセスを動作させることができます。linuxカーネルは互いに独立した、しかもハードウェアを独占しているかのような環境をプロセスに提供します。複数のプロセスが同時にハードウェアへアクセスしようとすると、それを調停します。

linuxカーネルは、プロセスに様々な機能を提供します。その機能を呼び出すための仕掛けをシステムコールと言います。機能にはそれぞれ名前がついて、それを特にシステムコールを経由して呼び出す機能であることを強調して、「名前」システムコールと呼ぶこともあります。たとえばopenという機能をopenシステムコールと呼ぶことがあります。さらにややこしいことにそれらの機能を総称してシステムコールと呼ぶこともあります。

システムコールを通してプロセスは、別のプロセスを起動したり (fork, clone, execve) 、ファイルを読み書きしたり (open, read, lseek, write, close...) 、ネットワーク通信 (socket, recvmsg, sendmsg, listen, bind, connect...) をしたりできます。現状でlinuxには300以上のシステムコールがあります。

straceコマンドを使うと、あるプロセスがどのようなシステムコールを起動しているのか、をリアルタイムで観察することができます:

# strace ./a.out

とするとa.outが呼び出すシステムコールがターミナル上に表示されます。a.outがその中で何をしているか、ということはわかりませんが、どのファイルから入力を得ているか、どのファイルに結果を出力しているか、といったプロセスと「外界」とのやりとりを知ることができます。次に示すのは/bin/echoを引数とした実行例です:

$ strace /bin/echo "hello"
execve("/bin/echo", ["/bin/echo", "hello"], [/* 57 vars */]) = 0
brk(0)                                  = 0xdb3000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f168738e000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
...
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f168738d000
write(1, "hello\n", 6hello
)                  = 6
close(1)                                = 0
munmap(0x7f168738d000, 4096)            = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

オプション-o FILE名とするとstraceの結果をファイルに保存できます:

strace -o /tmp/log.strace /usr/bin/pwd

出力をlessで見たい場合、リダイレクトとパイプを活用します:

strace -o /tmp/log.strace /usr/bin/pwd 2>&1 | less

幸いなことにシステムコールは良く文章化されていて(man-pagesパッケージがインストールされていれば) manコマンドで読むことできます。manコマンドについては詳細は「ドメイン知識」で説明します。

仕掛けとしてのシステムコールはハードウェアによって異なります。移植性を考えるとカーネルの機能を呼び出すために、ハードウェア毎に異なる仕掛けをアプリケーションプログラムに内蔵するのは好ましくありません。プロセスとカーネルの間にあるlibcが、ハードウェア毎の差異を隠蔽して、C言語の関数呼び出しの形式でシステムコールを起動できるラッパーを提供します。特に指示を与えない限り実行ファイルを作ろうとするとリンカがlibcをアプリケーションプログラムにリンクします。システムコールの種類によってはラッパーが少し凝ったことをやっているもありますが、基本形にラッパーとシステムコールを区別する必要がありません。そこでそのラッパーを指してシステムコールと呼ぶ場合があります。

システムコールのラッパー以外に、libcには標準Cライブラリの実装が含まれています。標準Cライブラリには書式付き出力に使うprintf, fprintf、文字列処理関数群 (strcmp, strcpy, strstr,...) 、メモリアロケータ (malloc, free,...) などが含まれています。libcも良く文章化されていて、man及びinfoコマンドで見ることができます。

なんらかの理由でシステムコールが失敗すると、その理由がint型の大域変数errno[1]に格納されます。とりうる理由の種類はシステムコール毎に異なります。システムコールに対するmanページ中に説明があります。

なおセクション2、すなわちシステムコールに関するmanページ群は、man-pagesパッケージに含まれています。

[1]マルチスレッドプログラムの場合、変数ではなくスレッド毎に独立した値をもてるような仕掛けを隠蔽したマクロとなります。

プロセス

プロセスには様々な要素がありますが、知っておくとすぐに役に立つものを3つ紹介します。

_images/runtime-process.svg

プロセスID(pid)

カーネルは自身が管理する複数のプロセスについて、それぞれを一意に指し示すことができるよう、プロセス作成時に整数(pid)を割り振っています。その目的から、ある瞬間に同じpidを持つプロセスはシステム上に二つと存在しません。

pidがわかると、それを指定してプロセスを終了させたり(kill)、デバッガやstraceをアタッチしたり(gdb -p PID, strace -p PID)できます。

psコマンドを使うとシステムで動作するプロセスの一覧を見ることができます:

$ ps ax
  PID TTY      STAT   TIME COMMAND
    1 ?        Ss     0:03 /usr/lib/systemd/systemd --system --deserialize 40
    2 ?        S      0:00 [kthreadd]
    3 ?        S      0:01 [ksoftirqd/0]
    5 ?        S<     0:00 [kworker/0:0H]
    7 ?        S<     0:00 [kworker/u:0H]
    8 ?        S      0:09 [migration/0]
...
 1857 ?        Sl     0:01 /usr/libexec/mission-control-5
 1867 ?        Sl     1:32 /usr/libexec/ibus-ui-gtk3
 1871 ?        Sl     0:00 /usr/libexec/goa-daemon
 1875 ?        Sl     0:01 /usr/libexec/evolution-addressbook-factory
 1882 ?        Sl     2:06 /usr/libexec/ibus-x11 --kill-daemon
 1923 ?        Sl     1:38 /usr/libexec/ibus-engine-simple
 1947 ?        S      0:00 /usr/libexec/gvfsd-metadata
...
16047 ?        Ss     0:04 sendmail: accepting connections
16084 ?        Ss     0:00 sendmail: Queue runner@01:00:00 for /var/spool/clientmqueue

左端の列がPIDです。

右端の列はそのプロセス自身で特に指定しない限り、そのプロセスで実行中のプログラムの起動に用いたコマンドラインです[2]

[2]main関数に渡される引数argv配列の最初の要素argv[0]を書き換えることで変更できます。

メモリ空間

プログラム(やライブラリ)の実行コードや変数群がメモリ空間に配置されます。

メモリ空間はプロセス毎に独立しています: カーネルに利用の許可を得られれば、メモリ空間をどのように使うかというのはプロセスの自由です。メモリ空間の内訳はファイル/proc/PID/mapsで覗き見ることができます。

次のソースコードから実行ファイルa.outを作りバックグラウンドで実行します。

#include <stdlib.h>

int
main(void)
{
  void *c = malloc(1024);
  while (1);
  return 0;
}
$ gcc empty.c
$ ./a.out &
[1] 5550
$ cat /proc/5550/maps
00400000-00401000 r-xp 00000000 08:02 524331                   /tmp/a.out
00600000-00601000 rw-p 00000000 08:02 524331                   /tmp/a.out
01dc9000-01dea000 rw-p 00000000 00:00 0                        [heap]
3171400000-3171420000 r-xp 00000000 08:02 2228701              /usr/lib64/ld-2.15.so
317161f000-3171620000 r--p 0001f000 08:02 2228701              /usr/lib64/ld-2.15.so
3171620000-3171621000 rw-p 00020000 08:02 2228701              /usr/lib64/ld-2.15.so
3171621000-3171622000 rw-p 00000000 00:00 0
3171800000-31719ac000 r-xp 00000000 08:02 2232931              /usr/lib64/libc-2.15.so
31719ac000-3171bac000 ---p 001ac000 08:02 2232931              /usr/lib64/libc-2.15.so
3171bac000-3171bb0000 r--p 001ac000 08:02 2232931              /usr/lib64/libc-2.15.so
3171bb0000-3171bb2000 rw-p 001b0000 08:02 2232931              /usr/lib64/libc-2.15.so
3171bb2000-3171bb7000 rw-p 00000000 00:00 0
7f045f0b3000-7f045f0b6000 rw-p 00000000 00:00 0
7f045f0d4000-7f045f0d5000 rw-p 00000000 00:00 0
7fff368b5000-7fff368d6000 rw-p 00000000 00:00 0                [stack]
7fff36997000-7fff36998000 r-xp 00000000 00:00 0                [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0        [vsyscall]

メモリ空間は複数の領域に分割されて、異なる目的で利用されています。mapsファイル中では、各行が1つの領域を説明しています。一番左の列、ハイフンで結ばれた2つの数字が、領域のアドレス範囲です。左から2つのめの列が、領域のメモリ保護の状態です。rは読み込み可能を、wは書き出し可能を、xは実行可能を意味します。pは共有されていないことを意味します。

いくつかのコラムを飛ばして一番右のコラムを見て下さい。その領域に実行プログラムあるいはライブラリがロードされている場合、そのファイル名が表示されます(a.out, ld-2.15.so, libc-2.15.so)。同じファイル名が複数回登場しています。これはファイル中の実行コードの部分と、変数群(大域変数やstaticと修飾された変数)の部分が別の箇所にロードされているためです。メモリ保護の状態にxとなっている領域には、ファイル中のプログラムコード部分がロードされています。rwとなっている領域には、変数群の部分がロードされています。

mallocで動的に獲得できるメモリは [heap]と表示された領域に確保されています。[stack]と表示された領域は、ローカル変数や関数呼び出しにおける実引数を配置するのに使う箇所です。[vdso]と[vsyscall]はシステムコールの仕掛けの一部です。

ファイル記述子テーブル

Linuxを含むunix-likeなOSでは、デバイス操作、ネットワーク通信、他のプロセスとの通信(プロセス間通信, IPC)といった処理(I/O処理)をファイルへの読み書きに使うシステムコール群をそのまま使ってカーネルに依頼できます。

これらのシステムコールは、その第一引数に「ファイル記述子(fd)」と呼ばれるint型の整数を一つとります。その整数の値によってシステムコールの対象 —ファイルなのか、デバイスなのか、ネットワーク通信なのか、IPCなのか、さらにファイルであればどのファイルなのか、どのデバイスであればどのデバイスなのか、ネットワーク通信であればIPアドレスとUDP/TCPポート、IPCであれば相手のPID — が何か決まります。

ファイルやデバイスであればopenシステムコールを、ネットワーク通信であればsocketシステムコールあるいはacceptシステムコールを、IPCであればおもにsocketあるいはpipeシステムコールを呼び出すとファイル記述子を得ることができます。ファイル記述子を得たら、以降readやwriteシステムコールで読み書きあるい通信できます。closeシステムコールを呼び出すと、以降、引数で指定したファイル記述子を使わないことをカーネルに伝えることができます。

ファイル記述子の値はプロセス毎に独立しています。pid=P0のプロセスがfd=nでネットワーク通信をしていても、pid=P1のプロセスはfd=nをファイルの読み書きに使っているかもしれません。

ファイル記述子とそれによって決まるI/Oの対象の組は、プロセス毎に用意された表でカーネルによって管理されています。この表をファイルファイル記述子テーブルと言います。

procファイルシステムからファイル記述子テーブルの内容見ることができます。今手元でこの資料を使っているemacsという名前のエディタのpidを調べて、そらにそのファイル記述子テーブルを見てみます:

$ ps ax | grep emacs
 2880 ?        Rl     9:58 emacs
13914 pts/3    S+     0:00 grep --color=auto emacs
$ ls -l /proc/2880/fd
ls -l /proc/2880/fd
合計 0
lr-x------. 1 yamato yamato 64 11月  8 14:50 0 -> /dev/null
lrwx------. 1 yamato yamato 64 11月  8 14:50 1 -> /home/yamato/.xsession-errors
lrwx------. 1 yamato yamato 64 11月  8 14:50 10 -> anon_inode:[eventfd]
lrwx------. 1 yamato yamato 64 11月  8 14:50 11 -> socket:[38064]
lrwx------. 1 yamato yamato 64 11月  8 14:50 12 -> socket:[37310]
lrwx------. 1 yamato yamato 64 11月  8 14:50 13 -> /dev/ptmx
lrwx------. 1 yamato yamato 64 11月  8 14:50 14 -> /dev/ptmx
lrwx------. 1 yamato yamato 64 11月  8 14:50 2 -> /home/yamato/.xsession-errors
lrwx------. 1 yamato yamato 64 11月  8 14:50 3 -> socket:[37275]
lrwx------. 1 yamato yamato 64 11月  8 14:50 4 -> anon_inode:[eventfd]
lrwx------. 1 yamato yamato 64 11月  8 14:50 5 -> socket:[35318]
lrwx------. 1 yamato yamato 64 11月  8 14:50 6 -> anon_inode:[eventfd]
lrwx------. 1 yamato yamato 64 11月  8 14:50 7 -> socket:[39422]
lrwx------. 1 yamato yamato 64 11月  8 14:50 8 -> anon_inode:[eventfd]
lrwx------. 1 yamato yamato 64 11月  8 14:50 9 -> socket:[37988]