次のページ 前のページ 目次へ

6. リンク

あらかじめお断りしておきますが、この章の構成は複雑になっています。 互換性のないバイナリの形式が二つあること、 static なリンクと共有ライブ ラリを使う場合があること、「リンク」という言葉が「コンパイルの後に行う 作業」と「コンパイルされたプログラムが呼び出されたときに行われること」 という二つの意味で用いられること(「ロード(load)」という言葉も使われま す。ただし逆の意味で) などが原因です。でも今あなたが読んだばかりの文よ りごちゃごちゃしている部分は少ないはずですから、それほど心配しないでく ださい。

多少なりとも混乱を少なくするために、実行時に行われることは「動的ロード」 と呼ぶことにして、その内容は次の章に書きます。同じ内容を「動的リンク」 と書いている文書もありますが、この文書では「動的リンク」は用いま せん。要するに、この章ではコンパイルの最終段階として行うリンクに ついてのみを扱います。

6.1 共有ライブラリ対 static なライブラリ

プログラム作成の最終段階は「リンク」と呼ばれます。全ての部品を結合 して、足りないものがないかどうか調べる作業です。「ファイルを開く」といっ たような類の作業は、多くのプログラムで行われます。したがってこのよ うな機能を持つ「部品」はライブラリの形で提供されています。普通の Linux システムでは、ライブラリは /lib/usr/lib にありま す(他の場所にあることもままありますが)。

static なライブラリを用いる場合は、リンカはプログラムが必要とする部品 を探し、出力する実行ファイルにその部品をコピーします。共有ライブラリの 場合は違った作業が行われます。リンカは出力ファイルに「このプログラムが 実行されるときには、まずこれこれのライブラリがロードされていないといけ ませんよ」といったメッセージを埋め込みます。したがって明らかに共有ライ ブラリを用いる方が実行ファイルのサイズは小さくなります。また消費するメ モリやディスク容量も小さくなります。 Linux におけるのデフォルトの振る 舞いでは、共有ライブラリがあればそちらを用い、なければ static なリンク を行います。実行ファイルを共有ライブラリ形式にしたいのに static になっ てしまった場合は、正しい位置に共有ライブラリのファイルがあって(a.out では *.sa、 ELF では *.so です)、それらが読み込み可能になっ ているかどうかをチェックしてください。

Linux では static なライブラリは libname.a といったような名前を持っ ており、共有ライブラリは libname.so.x.y.z となっています。 x.y.z はバージョン番号を示します。共有ライブラリにはリンクが張ら れることが多く、これは重要な機能を持っています。また a.out を利用する 設定では .sa という拡張子を持ったファイルもあるはずです。標準ライ ブラリは共有形式のものと static な形式の両方が含まれています。

あるプログラムがどのような共有ライブラリを必要とするかを調べるには ldd コマンド(List Dynamic Dependencies)を用います。

$ ldd /usr/bin/lynx
        libncurses.so.1 => /usr/lib/libncurses.so.1.9.6
        libc.so.5 => /lib/libc.so.5.2.18

これは WWW ブラウザのプログラム lynx が libc.so.5 (C ライブラリ) と libncurses.so.1 (端末制御ライブラリ) を用いていることを示して います。もし利用する共有ライブラリがなければ、 ldd の表示は 「statically linked]か「statically linked (ELF)」となります。

6.2 ライブラリに尋ねる(sin() はどこにいるの?)

nm libraryname とすると libraryname が参照している シンボルのリストが表示されます。 static なライブラリにも共有ライブラリ にも有効です。例えば tcgetattr() が定義されているライブラリが知り たい場合としましょう。この場合はまず以下を実行します。

$ nm libncurses.so.1 |grep tcget
         U tcgetattr

U は「定義されていない(undefined)」ことを意味します。したがって ncurses ライブラリでは tcgetattr を用いていますが、定義はしていないこ とにことになります。続いて以下のように実行します。

$ nm libc.so.5 | grep tcget
00010fe8 T __tcgetattr
00010fe8 W tcgetattr
00068718 T tcgetpgrp

W は「弱い定義(weak) 」であることを示します。すなわちこのシンボ ルは定義されてはいますが、他のライブラリの定義によって上書きされること を示しているのです。通常の定義は T で示されます(tcgetpgrp がそうです)。

ところでこの節のタイトルに対する解答は libm(so|a) です。 <math.h> で定義されている関数の本体は全て math ライブラ リにあります。したがってこのような関数を用いるには、リンクの際に -lm が必要になるわけです。

6.3 ファイルを探す

ld: Output file requires shared library `libfoo.so.1`

ld などのプログラムにおけるファイル検索の方法はバージョンによって異な ります。しかし全てのバージョンにおいて、/usr/lib は検索対象に 入っています。もしここ以外のディレクトリをライブラリ検索の対象にしたけ れば、 gcc や ld などの -L オプションを用いてください。

これで見つからなければ、正しいファイルがそのディレクトリにあるかを確認 してください。 a.out では -lfoo を指定してリンクすると、 ld はま ず libfoo.sa (共有ライブラリ)を探し、失敗すると libfoo.a (static ライブラリ)を検索します。 ELF の場合は libfoo.solibfoo.a の順に探します。 libfoo.so は通常 libfoo.so.x へのシンボリックリンクとなっています。

6.4 自分のライブラリを作る

バージョン管理

プログラムと同様、ライブラリにもバグはつきもので、時間とともに修正 されていきます。また新たな機能が追加されたり、関数の仕様が変更されたり 古いものが削除されたりもします。これはライブラリを使用するプログラムに は問題です。もし古い仕様に基づいている場合はどうすれば良いのでしょうか?

したがってライブラリにはバージョン管理を用います。ライブラリに対して行っ た変更を「小さい(minor) 」ものと「大きい(major)」ものに分けます。 minor なな変更では、そのライブラリを用いるプログラムがちゃんと動くことを保証 することにします。ライブラリのバージョンはファイル名でわかるようにしま す。(本当の事を言うとこれは ELF には当てはまりません。理由は後述。) libfoo.so.1.2 は major バージョンが 1 で、 minor バージョンが 2 であることを示します。 minor バージョンは少々違った構造を持つこともあ ります。 libc では「パッチレベル」を minor バージョンに追加し、ライブ ラリの名前を libc.so.5.2.18 のようにしています。 ASCII 端末で表示 可能な文字なら、英字でも _ でもつけてかまいません。

ELF と a.out の大きな違いの一つは、共有ライブラリの作り方にあります。 まず簡単な ELF のほうから見ることにしましょう。

ELF って結局なに?

ELF (Executable and Linking Format)はもともと USL(UNIX System Laboratories)で開発されたバイナリ形式で、現在では Solaris と System V Release 4 で用いられています。 ELF は以前 Linux で用いられていた a.out 形式よりも柔軟性に富んでいたので、 GCC と C ライブラリの開発者たちは昨 年に ELF を Linux の標準バイナリ形式としても採用することに決めました。

だからなんだって?

この節は `/news-archives/comp.sys.sun.misc' にある文書から抜粋した ものです。

ELF(Executable Linking Format)は SVR4 に導入された「進歩した最新の」 オブジェクトファイル形式です。 ELF はユーザによる拡張が可能であり、 straight COFF よりもずっと強力です。 ELF では、オブジェクト ファイルを任意の長さを持ったセクションからなるものします(決まったサ イズの要素からなる配列とはみなされません)。これらのセクションは(COFF とは異なり) 決まった場所に置く必要がなく、また順番も任意です。ユーザがオブジェクト ファイルに新たなデータを導入したければ、新しいセクションを追加するだけ で良いのです。 ELF にはこれまでのものよりずっと強力なデバッグ支援用の形式も導入されて います。これは DWARF(Debugging With Attibute Record Format)と呼ばれ ています。現在のところ linux ではこの機能は完全にはサポートされていま せん(しかし作業は着々と継続中です)。 DWARF DIE(Debugging Information Entries)のリンクリストは ELF バイナリの中の .debug という セクションに収められています。小さく、サイズも固定されたデバッグ情報と 異なり、DWARF DIE はそれぞれ任意の長さの複雑な属性を持ち、スコープに依 存したツリー形式のプログラムデータとして記述されています。 DIE は COFF の .debug セクションでは不可能であったような、巨大な情報(C++ の継承関 係リストなど)を保有することができるのです。

ELF ファイルは SVR4 (Solaris 2.0 ?)の ELF アクセスライブラリを通じて アクセスされます。このライブラリは ELF で取り扱いが厄介になっている部 分に対して、簡単で高速なインターフェースを提供しています。このライブラ リを用いれば、 ELF ファイルの実体そのものを見なくても済みます。 Elf ファ イルとしてアクセスされた UNIX のファイルは、最初に elf_open() コールを 実行すれば、後はその中身に elf_foobar() コールでアクセスすることができ ます。今まで COFF ユーザが強制されてきたように、実際のディスク上の位置 を求めてさまよう必要はもう無いのです。

ELF の有利/不利な点、および a.out のシステムを ELF システムにアップグ レードする際に必要な内容は ELF-HOWTO に記述されています。ここにカット & ペーストするつもりはありません。 ELF-HOWTO は、この文書と同じと ころにあるはずです。

ELF 共有ライブラリ

libfoo.so のような共有ライブラリを作成するための基本的な手順 は以下のようになります。

$ gcc -fPIC -c *.c
$ gcc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0 *.o
$ ln -s libfoo.so.1.0 libfoo.so.1
$ ln -s libfoo.so.1 libfoo.so
$ LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH ; export LD_LIBRARY_PATH

これによって libfoo.so.1.0 という名前の共有ライブラリができ、ld のためのリンク(libfoo.so)とダイナミックローダのためのリンク (libfoo.so.1)が作成されます。テストするにはカレントディレクトリ を LD_LIBRARY_PATH に追加します。

ライブラリがちゃんと動いたら、これを移動する必要があります。 /usr/local/lib あたりが適当でしょう。上で作ったようなリンクも それぞれ作り直す必要があります。 libfoo.so.1libfoo.so.1.0 のリンクは ldconfig によって常に最新のものに更 新されます。 通常 ldconfig はブートプロセスの一部で実行されているはずです。 libfoo.so のリンクはマニュアルで更新する必要があります。几帳面な 方はライブラリの全て(ヘッダファイルなども含む)を同時にアップデートし たくなるでしょうが、その場合は libfoo.so -> libfoo.so.1 とい うリンクを張っておき、 ldconfig が両者を同時に細心にしてくれるようにし ておくのが最も簡単でしょう。そうしない人は、後にあらゆる種類 の不可思議な現象に見舞われることになるでしょう。後で「聞いてないよ」なん て言わないように!

$ su
# cp libfoo.so.1.0 /usr/local/lib
# /sbin/ldconfig
# ( cd /usr/local/lib ; ln -s libfoo.so.1 libfoo.so )

バージョンの番号付けと soname、シンボリックリンク

各々のライブラリは soname という情報を持っています。リンカがライ ブラリを検索する際にこの soname を見つけると、実行バイナリにはライブラ リのファイル名ではなく soname が埋め込まれます。するとプログラムの実行 時に、動的ローダはリンク時に用いられたファイルではなく soname で指定されるファイルを探索します。例えば libfoo.so という ライブラリが libbar.so という soname を持つことも可能で、すると libfoo.so にリンクされたプログラムは実行時に libbar.so の方 を検索します。

意味の無い機能だと思いますか?実は同じライブラリの複数バージョンをシス テムに共存させるための鍵となる機能なのです。 Linux におけるライブラリ 命名法のデファクト・スタンダードは libfoo.so.1.2 といったようなも ので、これに対する soname は libfoo.so.1 となります。このライブラ リが標準のライブラリディレクトリ(例えば /usr/lib)に置かれる と、 ldconfiglibfoo.so.1 -> libfoo.so.1.2 というシン ボリックリンクを作り、実行時に適当なファイルが利用されるようにします。 また libfoo.so -> libfoo.so.1 というリンクも必要で、これにより ld はリンク時に用いるべき正しい soname を見つけることができるようになりま す。

ライブラリのバグを修正したり新機能を追加(今までのプログラムに影響を与 えない範囲で)したりしたときには、 soname はそのままにしてファイル名を 変更するのです。ライブラリの上位互換性がなくなったときには soname の番 号を一つ増やします。この場合新しいバージョンのライブラリはファイル名が libfoo.so.2.0、 soname が libfoo.so.2 となります。 libfoo.so のリンクも新しいバージョンへ張りなおせばライブラリの更 新に伴う手続きがすべて完了したことになります。

絶対にこの規則でライブラリの命名を行わなければならないわけではありませ んが、良い慣習ですので利用する方が良いと思います。 ELF ではライブラリ の命名に関しても柔軟性がありますから、人がうんざりするほど複雑な命名ルー ルを使うことだってできますが、実際にそうするかどうかはまた別の話ですよね。

実行方法をまとめます。伝統に従い major なアップグレードは互換性が失わ れたとき、 minor なアップグレードはそうでないときということにしましょ う。この場合は以下のようにリンクしてください。

gcc -shared -Wl,-soname,libfoo.so.major -o libfoo.so.major.minor

これでうまく動くはずです。

a.out、汝古き形式よ

ライブラリが ELF へ移行した主な理由は、共有ライブラリを簡単に作れ るという点にありました。しかし a.out でも作成が不可能なわけではありません。 ftp://tsx-11.mit.edu/pub/linux/packages/GCC/src/tools-2.17.tar.gz を入手して、パッケージの中にある 20 ページのドキュメントを読みましょう。 私は一方の党派に偏るつもりはありませんが、でも今まで書いてきた文章で、 私がもう a.out を使うつもりが無いことは明らかでしょうかね :-)

ZMAGIC と QMAGIC

古い a.out の実行バイナリは ZMAGIC と呼ばれます。 QMAGIC は ZMAGIC と 似ていますが、最初のページをマップしない点が違っています。 したがって 0〜4096 番地がどこにもマッピングされてないため、NULL ポインタ から変数を参照するという間違いを見つけやすくなっています(0 番地をアク セスすると、トラップされるわけです)。 また別の利点としてバイナリが少々(1K くらい)小さくなります。

非常に古いリンカでは ZMAGIC のみに、やや古いものは両方に、最近のものは QMAGIC のみに対応しています。カーネルは両方のフォーマットを実行させる ことができますので、実際には気にする必要はありません。

プログラムが QMAGIC かどうかは、`file' コマンドを実行することによって 表示されます。

ファイルの配置

a.out の共有ライブラリは二つのファイルと一つのシンボリックリンクから構 成されます。この文書でこれまで使ってきた「foo」ライブラリを例にとりま すと、 libfoo.salibfoo.so.1.2 が実際のファイル、 libfoo.so.1libfoo.so.1.2 へのシンボリックリンクです。 これらの役割はどんなものでしょうか?

コンパイルの際に、 ldlibfoo.sa を探します。このファイル はライブラリの「stub」ファイルと呼ばれ、外から参照できるデータと 実行時リンクに必要な関数へのポインタとを保持しています。

実行時には、ダイナミックローダは libfoo.so.1 を探索します。これは 実ファイルではなくシンボリックリンクになっていて、バグフィックスなどに よるアップデートの際に、このライブラリを使っていたプログラムに問題が生 じないようにしています。新しいバージョンのライブラリ (libfoo.so.1.3 など)があれば、 ldconfig を実行することによって リンク先を変更でき、このライブラリを用いていたプログラムには影響を与え なくてすみます。

DLL ライブラリ(トートロジーだということは承知しています :-p)は static なライブラリに比べて大きくなる場合が多いです。 DLL ライブラリに は将来の拡張に備えて、「hole」という形式の領域が確保されています。この hole 領域が実際にはディスクを消費しないようにすることもできます。単に cp するか、あるいは makehole コマンドを用います。(a.out で は)アドレスは固定されているので、ライブラリ構築後に strip することも できます。なお ELF ライブラリは strip してはいけません!

「libc-lite」とは?

libc-lite は libc ライブラリの軽量版です。フロッピーでの利用に適し、 UNIX の基本的なタスクのほとんどをカバーしています。 libc-lite には curses、 dgm、 termcap などのコードは入っていません。もしお使いのシス テムの /lib/libc.so.4 がこの lite 版へのリンクでしたら、 full サイズの版に置き換えた方が良いでしょう。

リンク:よくある問題

リンクの際に障害が起こったら私に教えてください!私が手助けできることは あまり無いかもしれませんが、同じのがたくさんきたらここに載せることはで きます...

共有形式でリンクしたいのに static になってしまう

ld が共有ライブラリを検索できるよう、それぞれリンクがちゃんと張ら れているか確認してください。 ELF の場合はシンボリックリンク libfoo.so が、 a.out では libfoo.sa が実ファイルへ張られていな ければなりません。この問題は ELF binutils を 2.5 から 2.6 に更新したと きに非常に多く報告されました。以前のバージョンではライブラリの検索を 「賢く」行っており、全てのリンクがなくても動作することがありました。新 しいバージョンでは他のアーキテクチャとの整合性をとるために、この機能を 削除しました。また推測を誤ると、より深刻な問題を引き起こす可能性が生じ ることも削除された理由の一つです。

DLL のツール `mkimage が libgcc の検索に失敗します

libc.so.4.5.x 以上では、 libgcc は共有ライブラリではなくなりまし た。したがって問題が生じた行の「-lgcc」は「`gcc -print-libgcc-file-name`」に置き換えてください。バッククォート「`」を お忘れなく。

また /usr/lib/libgcc* は全て削除してください。こちらも重要で す。

__NEEDS_SHRLIB_libc_4 multiply defined というメッセージが出る

これは上と同種の問題です。

DLL を再構築するときに ``Assersion failure'' というメッセージがでる

この謎のメッセージは、恐らくジャンプテーブルの slot のうちの一つがオー バーフローしてしまったことを意味しています。オリジナルの jump.vars ファイルに予約した領域が小さすぎたことが原因と考えられ ます。問題の個所は getsize によって特定できます(getsize は tools-2.17.tar.gz パッケージにあります)。この場合残念ながら、ライブラ リの major バージョンを上げ、下位互換性をあきらめることが唯一の解決法 となるでしょう。

ld: output file needs shared library libc.so.4

これは libc 以外のライブラリ(X 関係のライブラリなど)を用いており、か つ -g をつけて -static をつけていない場合に生じます。

共有ライブラリに対応した stub ファイル(*.sa)には、通常 _NEEDS_SHRLIB_libc_4 という未定義のシンボルが含まれています。これ は libc.sa stub によって解決されています。 -g が指定されてい ると libg.a または libc.a とのリンクが行われますが、ところが これらのライブラリでは上のシンボルは解決されていないのです。したがって 表題のエラーとなります。

結局 -g をつけてコンパイルするときは -static を追加するか、 あるいはリンクの際に -g を用いないか、が解答です。リンクの際に -g を用いなくても、各々のソースを -g でコンパイルしておけば、 ほとんどの場合は充分なデバッグ情報が得られます。


次のページ 前のページ 目次へ