3. 共有ライブラリ

共有ライブラリは、プログラム起動時にロードされるライブラリです。 共有ライブラリが適切にインストールされると、その後に起動される全てのプログラムは、自動的にその新しい共有ライブラリを使うことになります。 実際には、これよりもはるかに柔軟で洗練されています。なぜなら、Linux における共有ライブラリの実現方法のおかげで、次のことが可能となるからです。

3.1. 約束ごと

これらの望ましい特性すべてを共有ライブラリがサポートするためには、多くの慣習と指針に従わなければなりません。 ライブラリの名前、特に「soname」と「real name」の違いについて (及びそれらがどのように相互作用するかについて) 学ばなければなりません。 また、それらがファイルシステム内のどの場所に置かれるべきであるかについても、知っておかなければなりません。

3.1.1. 共有ライブラリ名

全ての共有ライブラリは「soname」と呼ばれる特別な名前を持っています。 soname は、「lib」というプレフィックス、ライブラリの名前、「.so」という語句で構成され、さらに後ろに、ピリオドと、インターフェース変更時に必ず増加するバージョン番号、が続きます (特別な例外として、最下層の C ライブラリは「lib」では始まりません)。 完全記述の soname は、そのライブラリ自身が含まれるディレクトリをプレフィックスとして含んでいます。 実際のシステムでは、完全記述の soname は、共有ライブラリの「real name」への単なるシンボリックリンクになっています。

全ての共有ライブラリには「real name」(実際のライブラリコードを含むファイルの名前) があります。real name は、soname に、ピリオド、マイナー番号、もう一つのピリオド、リリース番号、を加えたものです。 最後のピリオドとリリース番号は任意です。 マイナー番号とリリース番号は、ライブラリのどのバージョンがインストールされているかを正確に示すもので、設定管理に役立ちます。 これらの番号は (同じにしてくれれば話が分かりやすくなるのですが) 文書中でライブラリを説明する際に用いられている番号と同じではないかもしれない、という点に注意してください。

加えて、ライブラリ要求時にコンパイラが使用する名前というものもあります (「linker name」と呼ぼうと思います)。 この名前は、単純に一切のバージョン番号を取り除いた soname です。

共有ライブラリを管理する鍵となるのは、これらの名前の使い分けです。 必要とする共有ライブラリの一覧表を内部に持つときには、プログラムは、必要とする soname のみをリストアップするようにします。 逆に、共有ライブラリを作成するときには、(より詳細なバージョン情報を持つ) 特定のファイル名を持つライブラリのみを作成するようにします。 新しいバージョンのライブラリをインストールするときには、二、三の特別なディレクトリのうちの一つにそれをインストールし、それから ldconfig(8) プログラムを実行します。 ldconfig は、既に存在するファイルを調べ、real name へのシンボリックリンクとして soname 群を作成します。 同様にして、キャッシュファイル /etc/ld.so.cache も作成します (すぐに説明します)。

ldconfig は linker name を作成しません。 通常、この作成作業はライブラリインストール時におこない、単純に、「最新の」soname もしくは最新の real name へのシンボリックリンクとして linker name を作成します。 ほとんどの場合において、ライブラリを更新したら、リンク時にそれを自動的に利用したいと思うでしょうから、soname へのシンボリックリンクとして linker name を作っておくことをお勧めします。 なぜ ldconfig が自動的に linker name を作成しないのかを、私は H. J. Lu に尋ねました。 彼の説明は、基本的には、「ライブラリの最新バージョンを使ってコードを実行したいと思われるかも知れませんが、そうではなく、(おそらく互換性のない) 古いライブラリをリンクしながらの開発作業を希望されることもありうるでしょう」、というものでした。 そのため、ldconfig は、プログラムをどのライブラリにリンクさせたいのかということについては、何の仮定もおこなわないのです。 ですから、ライブラリとしてリンカに使わせるものを実際に更新するためには、インストーラがシンボリックリンクを個別に変更しなければならないのです。

例えば、/usr/lib/libreadline.so.3 は完全記述の soname であり、これは ldconfig が /usr/lib/libreadline.so.3.0 というような何らかの real name に対するシンボリックリンクとして作成するものです。 /usr/lib/libreadline.so という linker name も作成するべきであり、それは、/usr/lib/libreadline.so.3 を参照するシンボリックリンクとなるでしょう。

3.1.2. ファイルシステム配置

共有ライブラリはファイルシステム内のどこかに配置しなければなりません。 ほとんどのオープンソースソフトウェアは、GNU 規約に従う傾向があります。 詳細は info:standards#Directory_Variables にある info ファイルドキュメントを参照してください。 GNU 規約は、ソースコードを配布するとき、デフォルト設定時にはライブラリを全て /usr/local/lib にインストールするよう推奨しています (コマンドを全て /usr/local/bin に配置するようにとも推奨しています)。 また、これらのデフォルト設定をオーバーライドしたり、インストールルーチンを呼び出したりする際の約束ごとも定義しています。

ファイルシステム階層規約 (Filesystem Hierarchy Standard; FHS) は、ディストリビューションにおいて何をどこにインストールすべきかを説明しています (http://www.pathname.com/fhs/ を参照してください)。FHS によると、ほとんどのライブラリは /usr/lib にインストールし、起動時に必要とされるライブラリは /lib に、システムの一部ではないライブラリは /usr/local/lib にインストールするのが良い、ということになります。

実際には、これら二つの規約間に矛盾はありません。 GNU 規約は、ソースコード開発者のためのデフォルト設定を推奨しているのであり、一方で FHS は、ディストリビュータ (通常、システムパッケージ管理システムによりソースコードのデフォルト設定を選択的にオーバーライドする人々) のためのデフォルト設定を推奨しているのです。 実際にこれはうまく機能しています。あなたがダウンロードした「最新の」(おそらくバグだらけの!) ソースコードは、自動的に自分自身を「ローカルな」ディレクトリ (/usr/local) にインストールします。 そしてコードが成熟してきたら、ディストリビューション用の標準的な位置にコードを配置するため、パッケージ管理ツールでデフォルト設定を簡単にオーバーライドできます。 あなたのライブラリが、ライブラリ経由でしか呼び出されることのないプログラムを呼び出しているのならば、それらのプログラムを /usr/local/libexec (あるディストリビューションでは /usr/libexec になります) に配置するべきです。 厄介なのは、Red Hat から派生したシステムがデフォルト設定では /usr/local/lib をライブラリ検索対象に含めていないということです。 /etc/ld.so.conf に関する下記の議論を参照してください。 他の標準的なライブラリ配置場所としては、X Window System 用の /usr/X11R6/lib があります。/lib/security は PAM モジュール用に使われますが、通常、PAM モジュール群は動的ライブラリ (これもあとで説明します) としてロードされるので注意してください。

3.2. ライブラリはどのように使われるか

GNU glibc ベースのシステム (全ての Linux システムが含まれます) では、ELF バイナリ実行ファイルを起動すると、自動的にプログラムローダがロードされ、実行されます。 Linux システムでは、このローダは /lib/ld-linux.so.X (X にはバージョン番号が入ります) という名前です。 このローダは、プログラムによって使用されるその他の全ての共有ライブラリを順次探し出し、ロードします。

検索対象となるディレクトリのリストは、/etc/ld.so.conf ファイル内に記述されています。 Red Hat から派生しているディストリビューションの多くは、通常 /etc/ld.so.conf ファイル内に /usr/local/lib を含めていません。私はこれをバグだと考えており、また、/usr/local/lib/etc/ld.so.conf に追加することは、Red Hat から派生しているシステム上で多くのプログラムを走らせるのに必要な、共通の「修正」だと思っています。

ライブラリ内の幾つかの関数をオーバーライドしたいだけで、残りはそのままにしておきたいならば、オーバーライドするライブラリ (.o ファイル) の名前を /etc/ld.so.preload に入れることができます。 これらの「先行ロード」ライブラリは、標準集合よりも先行します。 この先行ロードファイルは、典型的には緊急用のパッチとして使われます。 通常、ディストリビューションが配布される際、このようなファイルは含まれないでしょう。

プログラム起動時にこれら全てのディレクトリを検索するのは、とても非効率的なので、実際にはキャッシュ処理がおこなわれます。 ldconfig(8) プログラムはデフォルトで /etc/ld.so.conf ファイルを読み込み、適切なシンボリックリンクを動的リンクディレクトリ内に作成します (これにより、標準的な慣習に沿うことになります)。 それから、あとで他のプログラムから利用されることになるキャッシュを /etc/ld.so.cache に書き込みます。 これにより、ライブラリへのアクセスが非常に速くなります。 結果として言えることは、DLL を追加したとき、または、DLL を削除したり、DLL ディレクトリのセットを変更したりしたときには必ず、ldconfig を実行しなければならない、ということです。 ldconfig の実行は、ライブラリインストール時にパッケージ管理ツールによっておこなわれるステップの一つであることが多いです。 ldconfig 以降、動的ローダは起動時、実際には /etc/ld.so.cache ファイルを使い、必要とするライブラリをロードすることになります。

話は変わりますが、FreeBSD は、このキャッシュ用のファイル名が少し異なります。 FreeBSD では、ELF キャッシュは /var/run/ld-elf.so.hints で、a.out キャッシュは /var/run/ld.so.hints になります。 これらのファイルもまた ldconfig(8) により更新されるので、場所の違いが問題となるのは、幾つかの特別な状況においてのみでしょう。

3.3. 環境変数

様々な環境変数により、共有ライブラリのロード処理を制御できます。 ロード処理をオーバーライドするための環境変数が存在するのです。

3.3.1. LD_LIBRARY_PATH

特定のプログラムを実行する際、一時的に別のライブラリを代替的に使用することができます。Linux では、環境変数 LD_LIBRARY_PATH に、標準的なディレクトリ群に先立ってライブラリ検索対象とすべきディレクトリ群を、コロンで区切って並べます。 これは、新しいライブラリをデバッグするときや、特殊な目的のために非標準的なライブラリを使用するときなどに便利です。 環境変数 LD_PRELOAD には、ちょうど /etc/ld.so.preload でやっているのと同じように、共有ライブラリ群を、標準集合をオーバーライドする関数群と共に列挙します。 これらの機能は、ローダ /lib/ld-linux.so により実装されています。 LD_LIBRARY_PATH は多くの Unix 系システムで機能しますが、全てのシステムにおいて機能するわけではないので注意しましょう。 例えば、HP-UX でも同じ機能を利用できますが、それは SHLIB_PATH ですし、AIX では LIBPATH となります (構文は同じで、コロン区切りのリストです)。

LD_LIBRARY_PATH は開発やテストには便利ですが、一般ユーザに日常的に使用させようとして、インストール処理で変更すべきではありません。 この理由については、http://www.visi.com/~barr/ldpath.html の「Why LD_LIBRARY_PATH is bad」(なぜ LD_LIBRARY_PATH はいけないのか) を参照してください。 とは言うものの、LD_LIBRARY_PATH は、開発やテスト、そして、他の方法では回避できない問題に対処するためには、やはり便利です。 LD_LIBRARY_PATH 環境変数を設定したくない場合、Linux では、プログラムローダを直接起動して引数を与えることもできます。 例えば、次のようにすると、指定の実行ファイルが、環境変数 LD_LIBRARY_PATH の内容ではなく、与えられた PATH を使用して実行されます。

  /lib/ld-linux.so.2 --library-path PATH EXECUTABLE

引数を与えずに ld-linux.so を実行すると、使い方についてのヘルプが表示されます。 しかし、もう一度言いますが、普段はこれを使わないようにしてください。 これらの機能は全てデバッグのためにあるのです。

3.3.3. その他の環境変数

実際には、ロード処理を制御する環境変数は他にもたくさん存在します。それらの名前は LD_ や RTLD_ ではじまり、そのほとんどが、ローダ処理の低レベルなデバッグや特殊な機能を実装するためのものです。 ほとんどのものにはまだ文書がありません。 これらについて調べたいなら、(gcc の一部である) ローダのソースコードを読むのが一番です。

特別な対応をおこなっていない場合、動的にリンクされるライブラリに対する制御をユーザに許可してしまうと、setuid/setgid プログラムは悲惨なことになるでしょう。そのため、(プログラム起動時にプログラムの残りの部分をロードする) GNU ローダは、これらの環境変数 (及び類似の環境変数) を無視するか、もしくは、これらの環境変数の効果を大幅に制限します。 ローダは、プログラムの信任証 (credential) を調べることによって、そのプログラムが setuid もしくは setgid されているかどうかを確認します。もしも uid (実ユーザ ID) と euid (実効ユーザ ID) が異なるか、もしくは gid (実グループ ID) と egid (実効グループ ID) が異なるなら、ローダはそのプログラムが setuid/setgid されている (もしくはそのようなプログラムから起動された) と推定し、リンク処理を制御する能力を大幅に制限します。 GNU glibc ライブラリのソースコードを読めば、このことを確認することができるでしょう。 特に、elf/rtld.c ファイルと sysdeps/generic/dl-sysdep.c ファイルを参照してください。 これらのことから、「uid と gid を euid と egid に等しくしてからプログラムを呼べば、これらの環境変数が完全に機能する」、ということが分かります。 他の Unix 系システムでは、別の方法でこの問題に対処しますが、「setuid/setgid プログラムは、設定されている環境変数群によって過度に影響を受けるべきではない」、という考え方は同じです。

3.4. 共有ライブラリの作成

共有ライブラリの作成は簡単です。 まずはじめに、共有ライブラリに組み込むオブジェクトファイルを、gcc の -fPIC または -fpic フラグを使って作成します。 -fPIC と -fpic オプションにより、共有ライブラリに対する要求事項である「位置独立コード (position independent code)」を生成することができます。 二つのオプションの違いについては、後の説明を参照してください。 soname は、gcc の -Wl オプションを使って指定します。 -Wl オプションは、オプション群 (今回の例では -soname リンカオプション) をリンカに渡すためのものです。-Wl の後ろのカンマはタイプミスではありません。 -Wl オプションには、エスケープされていないスペースを含めてはならないのです (訳注:-Wl の直後に続く「,-soname,your_soname」という文字列は、カンマの部分で分割されてからリンカに渡されます。 つまり、リンカには「-soname your_soname」というオプションが渡されます)。 次の書式を用いて共有ライブラリを作成してください。

  gcc -shared -Wl,-soname,your_soname \
      -o library_name file_list library_list

二つのオブジェクトファイル (a.o と b.o) を作成し、これら両方のオブジェクトファイルを含む共有ライブラリを作成する例を挙げます。 下記のコンパイル処理では、オブジェクトファイルにデバッグ情報 (-g) が含まれることになり、また、警告メッセージ (-Wall) も表示されます。これらのオプションは、共有ライブラリ作成に必須ではありませんが、お勧めします。 このコンパイル処理で、(-c オプションにより) オブジェクトファイルが生成されます。 なお、-fPIC オプションの指定は必須です。

  gcc -fPIC -g -c -Wall a.c
  gcc -fPIC -g -c -Wall b.c
  gcc -shared -Wl,-soname,libmystuff.so.1 \
      -o libmystuff.so.1.0.1 a.o b.o -lc

注意すべきことが幾つかあります。

開発中は、「他の多くのプログラムからも利用されているライブラリを修正してしまうかもしれない」という潜在的な問題があります。 開発段階のライブラリを対象にしてテストをおこなっている特定のアプリケーション以外には、その開発段階のライブラリを使わせたくないでしょう。 使用することになると思われるリンクオプションは、ld の「-rpath」です。 これは、コンパイルする個々のプログラムの、実行時のライブラリ検索パスを指定するものです。 gcc からは、次のように指定することで、-rpath オプションを渡すことができます。

  -Wl,-rpath,$(DEFAULT_LIB_INSTALL_PATH)

ライブラリのクライアントプログラムをビルドするときにこのオプションを使えば、そのプログラムが当該ライブラリを隠蔽してしまうような他の技術と競合していないこと、または、そのような技術を使用していないこと、を確認する必要があることを除いて、LD_LIBRARY_PATH (次で説明します) について悩む必要がなくなります。

3.5. 共有ライブラリのインストールと使用

共有ライブラリを作成したら、それをインストールしたくなることでしょう。 簡単な方法は、標準的なディレクトリ (例えば /usr/lib など) の一つに、そのライブラリをコピーし、ldconfig(8) を実行することです。

まず、共有ライブラリをどこかに作成する必要があるでしょう。 それから、必要なシンボリックリンク、特に soname から real name へのリンクを作成する必要もあるでしょう (同様に、バージョン番号を全く指定しないユーザのため、バージョン番号の無い soname, つまり「.so」で終わる soname からのリンクも必要です)。 最も簡単な方法は、次のとおり実行することです。

  ldconfig -n directory_with_shared_libraries

最後に、プログラムをコンパイルするときに、使おうとしている全ての静的ライブラリと共有ライブラリについて、リンカに教えてやる必要があります。 これは、-l オプションと -L オプションを使っておこないます。

標準的な場所にライブラリをインストールできない、もしくはインストールしたくないなら (例えばあなたが /usr/lib を変更する権限を持っていないなど)、別の手順を踏まなければなりません。 このような場合、ライブラリをどこかにインストールしておき、そのライブラリを見つけるために十分な量の情報をプログラムに与えなければなりません。 これをおこなうには、幾つか方法があります。 単純な場合では、gcc の -L フラグを使用できます。 また、特に、ある特定のプログラム以外は非標準的な場所に置いてあるライブラリを利用しないというのであれば、(上で説明した)「-rpath」を使うこともできます。 環境変数を使っていろいろ制御することもできます。 特に、LD_LIBRARY_PATH を設定することができます。 LD_LIBRARY_PATH には、共有ライブラリを検索するときに通常の場所に先立って検索対象とするディレクトリ群を、コロンで区切って列挙します。 bash をお使いでしたら、次の方法で my_program を実行できます。

  LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH my_program

幾つかの関数を選択的にオーバーライドしたいだけならば、オーバーライドするオブジェクトファイルを作成して LD_PRELOAD を設定するだけで実現できます。 このオブジェクトファイル内の関数により、対象となっている関数だけがオーバーライドされます (他の関数は元のままです)。

通常、ライブラリの更新は、何も気にせずにおこなうことができます。 もしも API に変更があるならば、ライブラリ作成者により soname も変更されていることでしょう。 そういうわけで、一つのシステム上に複数のライブラリが存在でき、各プログラムに対するライブラリが正しく選択されるのです。 しかし、もしも、ライブラリの更新で soname は変更されなかったのにプログラムがうまく動かなくなってしまった、ということがあるのならば、古いライブラリをどこかにコピーし、そのプログラムの名称を変更する (古い名前に「.orig」を付け足すなど) ことによって、そのプログラムが古いバージョンのほうのライブラリを使うように強制することができます。 これらの作業をおこなったあと、利用するライブラリを再設定して実際の (名称変更された) プログラムを呼び出す、小さな「ラッパー」スクリプトを作成してください。 番号付けの約束ごとのおかげで、同一ディレクトリ内に複数のバージョンのライブラリを置くこともできますが、お望みなら、古いライブラリをそれ独自の特別な場所に置くこともできます。 ラッパースクリプトは、次のようなものになるでしょう。

  #!/bin/sh
  export LD_LIBRARY_PATH=/usr/local/my_lib:$LD_LIBRARY_PATH
  exec /usr/bin/my_program.orig "$@"

自分でプログラムを書くときには、この方法に依存しないようにしてください。 ライブラリの後方互換性を持たせるか、もしくは、互換性の無い変更をおこなうときは必ず soname のバージョン番号を増やすよう、気を払ってください。 この方法は、最悪の場合の問題を扱うための「緊急時」対応なのです。

ldd(1) を使えば、プログラムによって使用されている共有ライブラリのリストを見ることができます。 例えば、次のようにタイプすれば、ls によって使用される共有ライブラリを確認できます。

  ldd /bin/ls

通常、プログラムが依存する各 soname について、それぞれどのディレクトリに存在するかが特定され、 その特定されたディレクトリ名とともに、当該 soname 群のリストが表示されます。 実質的に全ての状況において、少なくとも二つの依存情報が表示されるでしょう。

注意:信頼できないプログラムに対して ldd を実行してはいけません。 ldd(1) のマニュアルで明確に述べられているとおり、ldd は、(幾つかの状況においては) 特別な環境変数 (ELF オブジェクトの場合は LD_TRACE_LOADED_OBJECTS) を設定してプログラムを実行することにより、動作しています。 信頼できないプログラムが、(ldd 情報を単に表示するかわりに) 任意のコードを ldd ユーザに強制実行させてしまうことも可能かもしれません。

3.6. 互換性のないライブラリ

新しいバージョンのライブラリが古いものとのバイナリ互換性を持たないときには、soname を変更する必要があります。 C 言語では、ライブラリがバイナリ互換ではなくなってしまう四つの基本的な原因があります。

  1. 関数の挙動が変更され、元々の仕様に適合しなくなる

  2. エクスポートされているデータが変更される (例外:構造体がライブラリ内でのみアロケートされる場合に限り、構造体の末尾に任意のメンバーを追加することには問題がない)

  3. エクスポートされている関数が削除される

  4. エクスポートされている関数のインターフェースが変更される

これらの原因を回避できれば、ライブラリをバイナリ互換に保つことができます。 別の言い方をすると、これらの変更を避ければ、Application Binary Interface (ABI) 互換を保つことができる、ということです。 例えば、古い関数を削除せずに新しい関数を追加したい、というのは問題ないでしょう。 構造体の末尾にのみメンバーを追加し、その構造体をアロケートするのはライブラリ内だけとし (アプリケーションにはアロケートさせない)、追加のメンバーをオプション扱いにする (ライブラリがそのメンバーを設定する)、などの操作をおこなって生じる変更が、古いプログラムに対して影響を与えないことが確かな場合にのみ、メンバーを追加することができます。 もしもユーザが構造体を配列で使っているならば、その構造体を拡張することはほとんどできないので十分注意してください。

C++ (および、テンプレートやディスパッチされるメソッドをコンパイル時に組み込むという方法でサポートするその他の言語) では、状況はより複雑になります。 上記に述べたことが全て当てはまる上、さらに多くの注意点があります。 幾つかの情報が、コンパイルされるコード内に「隠蔽された状態で」組み込まれるという点にその原因があるのですが、このために、C++ が通常どのように実装されているかを知らない人には理解しにくい依存問題が、引き起こされてしまうのです。 正確に言えば、これは「新しい」問題ではありません。単に、コンパイル済みの C++ コードが、人によっては驚くかもしれない動作をして問題を引き起こす、というだけの話です。 次のリストは、バイナリ互換を維持するために C++ 内でやってはいけない項目のリスト (おそらく完全ではありませんが) であり、Troll Tech 社テクニカル FAQ で公開されているものです。

  1. 仮想関数の再実装を追加する (古いバイナリが元の実装を呼び出すのが安全ではない場合)。 コンパイラは SuperClass::virtualFunction() 呼出しをコンパイル時に評価するため (リンク時ではない)。

  2. 仮想メンバ関数を追加または削除する。 これにより全サブクラスの仮想関数テーブルのサイズとレイアウトが変更されてしまうため。

  3. インラインメンバ関数経由でアクセス可能なデータメンバを移動させたり、データメンバの型を変更する。

  4. クラス階層を変更する。ただし、リーフ (訳注:下位クラスを持たないクラス) の新規追加を除く。

  5. プライベートデータを追加、または削除する。 これにより全サブクラスのサイズとレイアウトが変更されてしまうため。

  6. public もしくは protected メンバ関数がインライン関数でない場合に、それらを削除する。

  7. public もしくは protected メンバ関数をインライン化する。

  8. インライン関数の挙動を変更する。ただし、古いバージョンが機能し続ける場合を除く。

  9. 可搬性を持たせたいプログラム内のメンバ関数のアクセス権 (すなわち、public, protected または private) を変更する。 アクセス権情報を関数名に組み入れるコンパイラもあるため。

この長いリストのことを考えると (訳注:=こんな長いリストの全項目を守り続けることは無理だろうから)、特に C++ ライブラリの開発者は、バイナリ互換性を維持できない更新を頻繁に実施することを計画しておく必要があります。 幸いにして、Unix 系システム (Linux を含みます) では、一つのライブラリの複数のバージョンを同時にロードすることができるので、ディスクスペースを消費することにはなりますが、古いライブラリを必要とする「古い」プログラムを引き続き実行することが可能です。