プログラマが知るべき97のこと/リンカは魔法のプログラムではない

コンパイルが必要な言語においては、ソースコードを静的にリンクされた実行ファイルに変換するプロセスが発生します。愕然とするのは、そのプロセスについてごく浅い認識しか持っていないプログラマが意外に多いということです(つい最近も、それを思い知らされる出来事がありました)。読者の中にも、こんな認識の人がいるのではないでしょうか。

  1. ソースコードを編集する。
  2. ソースコードをコンパイルすると、オブジェクトファイルが作成される。
  3. 何やら不思議な処理が行われる。
  4. 実行ファイルができる。

ステップ3で行われるのは、もちろん「リンク」です。何を当たり前のことを、と言う人もいるかもしれません。わざわざこんなバカげたことを言うのには理由があります。私は何十年もテクニカルサポートの仕事をしてきたのですが、次のような質問を受けることが実に多いのです。

  1. リンカが「defが2度以上定義されている」というメッセージを出すが、どうすればいいか。
  2. リンカが「abc は未解決シンボルである」というメッセージを出すが、どうすればいいか。
  3. 実行ファイルが異常に大きくなってしまう。その理由がわからない。

こういう質問をする人の話を聞いていると、「・・・みたいです」、「どういうわけか」という類の言い回しが多いのに気づきます。困惑しているというのがよくわかります。こういう言葉を使うのは、リンクのプロセスを何だか得体の知れない、一種の「魔法」と思っているからでしょう。どうやら超人的な知識と技術を持った専門家にしか理解できないものと捉えているようです。コンパイルの話だと、こういう話し方をする人はまずいないのです。コンパイラなら、少なくとも何をしているか、だいたいのことはわかると,思っているのでしょう。

リンカは実はさほど難しいプログラムではありません。むしろ単純でわかりやすいプログラムです。することといえば、オブジェクトファイルのコードセクション、データセクションを連結し、定義されているシンボルと参照を接続し、ライブラリから未解決シンボルを抽出し、実行ファイルを書き出す、それだけです。とても単純で、魔法でも何でもありません。なぜリンカが難しく感じるかといえば、処理の結果できるファイルのフォーマットが驚くほど複雑で、そのままで解読することは難しいからでしょう。だからと言って、リンカ自体が複雑な処理をしているというわけではないのです。

それでは、リンカが、「defが2度以上定義されている」というメッセージを出すのはなぜでしょうか。プログラミング言語の中には、C、C++、Dなどのように、「宣言」と「定義」の両方を行う、というものが数多くあります。「宣言」は、通常、以下のように、ヘッダファイルで行われます。

extern int iii;

この宣言により、シンボルiiiへの外部参照が生成されることになります。一方、「定義」の場合は、実際にシンボルに対応する記憶領域が確保されます。定義は、通常は実装ファイルの中で、以下のように行われます。

int iii = 3;

シンボル1つにつき、定義はいくつまで可能なのでしょうか。映画「ハイランダー悪魔の戦士」ふうに言えば、「1つでしかあり得ない」ということになります。では、以下のように、iii の定義が実装ファイルに複数あった場合にはどうなるのでしょうか。

// File a.c
int iii = 3;
// File b.c
double iii(int x) { return 3.7; }

この場合は、リンカが「iii が2度以上定義されている」というエラーメッセージを出すことになります。

定義は「1つでしかあり得ない」ものですが、一方で「絶対に1つは必要」なものです。もし出力が宣言のみで、一度も定義されていなければ、リンカは「iii は未解決シンボルである」というエラーメッセージを出します。

実行ファイルが異常に大きくなる理由は、リンカがオプションで生成するマップファイルを見るとわかります。マップファイルは、実行ファイル中の全シンボルとそのアドレスの単なるリストです。マップファイルを見れば、どんなライブラリモジュールがリンクされているか、またそれぞれのモジュールのサイズはどれくらいかもわかります。つまり、実行ファイルのサイズが大きくなっている原因がどのモジュールにあるかがわかるわけです。時には「なぜこのライブラリモジュールがリンクされているのか理由がまったくわからない」というものが見つかることもあります。理由を知るには、そのモジュールをいったんライブラリから削除して、リンクをし直してみる、という方法が有効です。そうすれば、「シンボルが定義されていない」というエラーが出るでしょう。そのエラーを見れば、モジュールがどこで参照されているのかがわかるはずです。

リンカがメッセージを出した時、その原因がすぐにはわからないことも確かにあります。しかし、それでも、リンカが特に複雑な処理をしているわけではないということは常に念頭に置くべきでしょう。処理自体は簡単だけれども、その結果できるものが複雑で、詳しく検証するのがなかなか面倒ということなのです。