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 もしくは最新の realname への単なるシンボリックリンクとして、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 Starndard; 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 モジュール用に使われますが、 それらは通常 DL ライブラリ (これもあとで説明します) としてロードされる、ということに注意してください。

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 の実行は、ライブラリインストール時に パッケージ管理ツールによっておこなわれるステップの一つであることが多いです。 それ以降、起動時には、動的ローダは実際に /etc/ld.so.cache ファイルを使い、 必要とするライブラリをロードします。

3.3. 環境変数

様々な環境変数がこの処理手順を制御できます。事実、 この処理手順をオーバーライドするのに使える環境変数が存在します。 例えば、この特殊な実行を、一時的に他のライブラリで代替することができます。 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 の ``なぜ LD_LIBRARY_PATH はいけないのか'' を参照してください。

実際には、ローディング処理手順を制御する環境変数は ほかにもたくさん存在します。それらの名前は、LD_ や RTLD_ ではじまります。それらのほとんどは、ローダ処理の低レベルなデバッグや、 特殊化されたケイパビリティを実装するためのものです。 ほとんどは、十分にドキュメント化されていません。 それらについて知る必要があるならば、学習するもっともよい方法は、 ソースコードを読むことです。

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

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

共有ライブラリの作成は簡単です。 まずはじめに、gcc の -fPIC フラグ (これは、共有ライブラリに必要とされる ``位置独立コード (position independent code)'' の生成を可能にします) を使って、 共有ライブラリに組み込まれることになるオブジェクトファイルを作成します。 それから、次の形式を使って共有ライブラリを作成してください――

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

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

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

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

もしもこれができないならば (例えば、あなたが /usr/lib を変更する権利を持っていないなど)、 事態を制御するために環境変数を使用することができます。 まずはじめに、どこかに共有ライブラリを作成する必要があるでしょう。 それから、必要なシンボリックリンク、特に soname から real name へのシンボリックリンク (同様に、バージョン番号をまったく指定しないユーザのために、 バージョン番号のない soname 、つまり、``.so'' で終わる soname からのシンボリックリンクも) 設定する必要があるでしょう。 もっとも簡単な方法は、次のコマンドラインを実行することです――

 ldconfig -n directory_with_shared_libraries

それから、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 $*

ldd(1) を使えば、 あるプログラムによって使用されている共有ライブラリのリストを 調べることができます。 例えば、次のようにタイプすれば、 ls によって使用されている共有ライブラリを確認することができます――
  ldd /bin/ls
一般的には、プログラムの依存する soname のリストが、 名前解決後のディレクトリ名と共に得られます。実際には全ての場合において、 少なくとも二つの依存要素を見ることになるでしょう――

注意――信用できないプログラムに対して ldd を実行しては いけません。これについては、 ldd(1) のマニュアルで明確に述べられています。 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. private データメンバを追加または削除する。というのは、 この作業は全サブクラスのサイズと配置を変更するだろうからです。

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

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

  8. インライン関数がおこなっていたことを変更する。 ただし、古いバージョンが動作し続けている場合を除く。

  9. ポータブルなプログラムのメンバ関数のアクセス権 (すなわち、 public, protected または private) を変更する。というのは、 アクセス権を関数名に押し込んでしまうコンパイラも存在するからです。

C++ ライブラリの開発者は、この長ったらしいリストを手にして、 バイナリ互換性をなくすことになってしまう時折の更新作業以上のことを 特に考慮しなければなりません。 幸いにして、Unix ライクなシステム (Linux を含みます) では一つのライブラリの複数のバージョンを同時にロードすることができるので、 ディスクスペースを失うことにはなりますが、 ユーザは古いライブラリを必要とする ``古い'' プログラムをその後も実行することが可能ではあります。