Program Library HOWTO David A. Wheeler
dwheeler@dwheeler.com
version 0.75, 31 October 2000 日本語訳 川崎貴彦
takahiko@hakubi.co.jp
0.75-J1 4 Dec 2000 プログラマ用のこの HOWTO では、Linux 上でプログラムライブラリを作成、 使用する方法を論じます。ここには、静的ライブラリ、共有ライブラリ、 動的にロードされるライブラリ、が含まれます。
はじめに プログラマのためのこの HOWTO は、GNU ツールセットを使用している Linux 上でプログラムライブラリを作成、使用する方法を論じます。 ``プログラムライブラリ'' とは、単に、 あとでプログラムに組み込まれることになるコンパイル済みのコード (及びデータ) を含むファイルのことです。プログラムライブラリは、 プログラムを、よりモジュール化し、より速く再コンパイルでき、 より簡単に更新できるものにします。プログラムライブラリは、 三つのタイプ――静的ライブラリ、共有ライブラリ、動的にロードされる (dynamically loaded; DL) ライブラリ――に分類することができます。 この文書は、最初に、静的ライブラリ ――プログラムが実行される前にその実行可能プログラムに組み込まれるライブラリ―― について論じます。それから、共有ライブラリ ――プログラム実行時にロードされ、かつ複数のプログラム間で共有されるライブラリ―― について論じます。最後に、動的にロードされる (dynamically loaded; DL) ライブラリ ――プログラム実行中の任意の時点でロードして使用することが可能なライブラリ―― について論じます。 DL ライブラリは、実際には異なるライブラリ形式というわけではありません (静的ライブラリも共有ライブラリも DL ライブラリとして使用することが可能です)。 その代わりに、プログラマが DL ライブラリをどのように使用するか という点において、違いがあります。HOWTO は、さらに多くの例を挙げている章、 その他の情報源への参照を挙げている章、をもって終了します。 この HOWTO は実行可能ファイルとライブラリのための Executable and Linking Format (ELF) 形式 ――昨今のほとんど全ての Linux ディストリビューションで使用されている形式―― についてのみ論じます。 GNU gcc ツールセットは、実際には ELF 以外のライブラリ形式を扱うことができます。 特に、ほとんどの Linux ディストリビューションでは、旧式の a.out 形式を今なお使用することが可能です。 しかしながら、これらの形式はこの文書の対象外です。 共有ライブラリを指して dynamically linked libraries (DLL) という用語を使う人がいること、その DLL という用語を DL ライブラリとして使用される任意のライブラリを意味するために使う人がいること、 また、どちらかの条件を満たすライブラリを意味するために DLL という用語を使う人がいること、には注意したほうがよいです。 いずれの意味を取り上げるにしても、この HOWTO は Linux 上でのこれら全ての DLL についてカバーします。 多くのシステムに移植されるアプリケーションを作成しているならば、 ライブラリを構築しインストールするのに、Linux ツールを直接使用する代わりに GNU libtool を使用することを考慮したほうがよいかもしれません。GNU libtool は、 共有ライブラリ使用の複雑さ (例えば、それらを作成しインストールするなど) を一貫性のあるポータブルなインターフェースで隠す、 汎用的なライブラリサポートスクリプトです。Linux 上では、GNU libtool はこの HOWTO に記述されているツールと慣習の上に構築されています。 動的にロードされるライブラリへのポータブルなインターフェース用に、 様々なポータビリティラッパーを使用することができます。GNU libtool は、 ``libltdl'' と呼ばれるその種のラッパーを含んでいます。 他の選択肢としては、可搬性のある方法で動的ローディングをサポートする glib ライブラリ (glibc と混同しないでください) を使用することもできます。glib については、 http://developer.gnome.org/doc/API/glib/glib-dynamic-loading-of-modules.html でさらに知ることができます。再度述べますが、Linux 上では、この機能は、 この HOWTO 内に記述されている構成物を使用することによって実装されています。 もしもあなたが実際に Linux 上でコードを開発、もしくはデバッグしているならば、 おそらくなおさらのこと、この HOWTO 内の情報を欲されることでしょう。 この HOWTO の一次配布場所は http://www.dwheeler.com/program-library であり、Linux Documentation Project (http://www.linuxdoc.org) に寄贈されています。著作権は David A. Wheeler にあり (Copyright (C) 2000)、 General Public License (GPL) でライセンスされています。 さらなる情報については最後の章を読んでください。 静的ライブラリ 静的ライブラリは、通常のオブジェクトファイルの単なる集合体です。 慣習的に、静的ライブラリは ``.a'' という拡張子を持ちます。 この集合体は、ar (archiver) プログラムを使用して作成されます。 静的ライブラリは以前ほどには使われなくなっていますが、それは、 共有ライブラリのほうが優れていることによります (あとで述べます)。 それでもまだ、静的ライブラリは時々作成され ――はじめは歴史的な理由で存在していたのですが――、 説明するのもより簡単です。 ユーザは、コードを再コンパイルする必要もなく静的ライブラリを プログラムにリンクすることができ、再コンパイルにかかる時間を節約できます。 注――昨今のより高速なコンパイラのことを考えれば、 再コンパイル時間は重要ではなくなってきています ――そのためにこの理由付けは以前ほど有力ではありません。 静的ライブラリは、その開発者が、 ライブラリへリンクすることをプログラマに許可はしたいが ライブラリソースコードは渡したくはない、という場合にしばしば役に立ちます (これはライブラリベンダーにとっては好都合ですが、 そのライブラリを使おうとしているプログラマにとっては 明らかに好都合とは言えません)。論理的には、実行可能ファイルにリンクされる静的 ELF ライブラリ内のコードは若干速く (1-5%) 動作するはずですが、実際には、 他のごちゃごちゃした要因のため、その通りになることは稀のようです。 静的ライブラリを作成する、もしくは既に存在する静的ライブラリに さらにオブジェクトファイルを追加するには、次のようなコマンドを 使用してください―― ar rcs my_library.a file1.o file2.o このコマンド例では、静的ライブラリ my_library.a にオブジェクトファイル file1.o と file2.o を付け加えています。まだ my_library.a が存在していなければ、作成します。 静的ライブラリ作成に関してさらに情報を得るには、ar(1) を参照してください。 一度静的ライブラリを作成してしまうと、それを使いたくなることでしょう。 実行可能プログラムを作成するときにコンパイルとリンク処理の一部として 呼び込むことで、共有ライブラリを使用できます。実行可能ファイルを作成するのに gcc(1) を使っているならば、ライブラリを指定するのに -l オプションを使用できます。より詳しい情報については info:gcc を参照してください。 -l と -L オプションを使って、リンカ ld(1) を直接使用することもできます。 しかしながら、ld(1) のインターフェースは gcc(1) よりも変更されやすいので、 ほとんどの場合は gcc(1) を使うほうがよいです。 共有ライブラリ 共有ライブラリは、プログラムが起動するときにロードされるライブラリです。 共有ライブラリが適切にインストールされると、 その後に起動される全てのプログラムは、 自動的にその新しい共有ライブラリを使うことになります。 実際には、これ以上にはるかに柔軟で洗練されています。というのは、 Linux によるアプローチは次のことを可能にするからです―― ライブラリを更新しながらも、 そのライブラリの古くて後方互換性のないバージョンを使いたいというプログラムを、 依然としてサポートできる 特別なプログラムを実行するとき、特定のライブラリ、 もしくはライブラリ内の特定の関数でさえオーバーライドできる 既に存在しているライブラリを使用してプログラムが 動いている間にも、この全てをおこなうことができる 約束ごと これらの望ましい特性すべてを共有ライブラリがサポートするためには、 多くの慣習と指針に従わなければなりません。ライブラリの名前、 特に ``soname'' と ``real name'' の違いについて (及びそれらがどのように相互作用するかについて) 理解する必要があります。 また、それらがファイルシステム内のどの場所に置かれるべきであるかについても、 理解する必要があります。 共有ライブラリ名 全ての共有ライブラリは、``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 を参照するシンボリックリンクになることでしょう。 ファイルシステム配置 共有ライブラリはファイルシステムのどこかに配置されなければなりません。 ほとんどのオープンソースソフトウェアは、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 ライブラリ (これもあとで説明します) としてロードされる、ということに注意してください。 ライブラリはどのように使われるか 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 ファイルを使い、 必要とするライブラリをロードします。 環境変数 様々な環境変数がこの処理手順を制御できます。事実、 この処理手順をオーバーライドするのに使える環境変数が存在します。 例えば、この特殊な実行を、一時的に他のライブラリで代替することができます。 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 プログラムは環境変数群によって過度に影響を 受けるべきではないという理由――によります。 共有ライブラリの作成 共有ライブラリの作成は簡単です。 まずはじめに、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 注意すべきことが幾つかあります―― 生成されたライブラリを strip しないでください。また 本当に必要でない限りコンパイラオプション -fomit-frame-pointer を使わないでください。生成されたライブラリは機能するでしょうが、 これらの作業は、デバッガをほとんど使い物にならなくしてしまいます。 コードを生成するには、-fpic ではなく、-fPIC を使ってください (前者は機能しないこともあります。というのは、 分岐が大規模な配置転換を必要とする場合、-fpic は完全な位置独立コードを生成しないかもしれないからです)。 共有ライブラリのインストールと使用 一度共有ライブラリを作成してしまうと、それをインストールしたくなることでしょう。 簡単な方法は、標準的なディレクトリ (例えば /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 のリストが、 名前解決後のディレクトリ名と共に得られます。実際には全ての場合において、 少なくとも二つの依存要素を見ることになるでしょう―― /lib/ld-linux.so.N (N は、1 かそれ以上。たいていは少なくとも 2) 。 これは、他の全てのライブラリをロードするためのライブラリです。 libc.so.N (N は、6 かそれ以上) 。 これは C ライブラリです。他の言語でさえ、C ライブラリを使用する傾向があります (少なくともそれら自身のライブラリを実装するために) 。そのため、 ほとんどのプログラムは少なくともこのライブラリは含んでいます。 注意――信用できないプログラムに対して ldd を実行しては いけません。これについては、 ldd(1) のマニュアルで明確に述べられています。 ldd は、当該プログラムを直接呼び出すことで機能しています。 信用できないプログラムが予期していないコードを実行してしまうことがありえます。 非互換ライブラリ 新しいバージョンのライブラリが古いものとバイナリ非互換であるときは、 soname を変更する必要があります。C においては、 ライブラリがバイナリ互換ではなくなってしまう四つの基本的な理由があります。 元の仕様に適合しないように関数の動作が変更されてしまう エクスポートされるデータ項目が変更されてしまう (例外 ――構造体がライブラリ内でのみアロケートされる場合に限り、 構造体の末尾に任意の項目を追加するのは問題ない) エクスポートされている関数が削除されてしまう エクスポートされている関数のインターフェースが変更されてしまう これらの理由を回避できるならば、ライブラリをバイナリ互換に保つことができます。 別の言い方をすると、これらの変更を避ければ、Application Binary Interface (ABI) 互換を保つことができる、ということです。例えば、 新しい関数を追加したいけれども古い関数を削除したくはないかもしれません。 構造体の末尾にのみアイテムを追加し、 ライブラリにだけその構造体をアロケートすることを許可し (アプリケーションには許可しない)、その追加のアイテムをオプション扱いにする (もしくはライブラリがそれらを満たすようにする)、 などの操作をおこなって生じた変更が、 古いプログラムに対して影響を与えないことを確認できる場合にのみ、 アイテムを追加することができます。気を付けてください。 もしもユーザが構造体を配列で使っているならば、その構造体は拡張できません。 C++ (および、コンパイル済み組込みテンプレート且つ/又は コンパイル済みのディスパッチされるメソッドをサポートするその他の言語) では、状況はより複雑になります。 上記の問題点全てが適用される上、さらに多くの問題があります。 理由は、幾つかの情報がコンパイルされたコード内に ``隠された状態で'' 実装されているということにあるのですが、 このことが、 C++ が一般的にどのように実装されているかを知らない人には よく分からないような依存問題を引き起こしてしまうのです。 正確に言えば、それらは ``新しい'' 問題ではありません。 単に、コンパイル済みの C++ のコードが、あなたを驚かせる ことになるかもしれない方法でそれらの問題を引き起こすということなのです。 次のものは、バイナリ互換を維持するために C++ 内でやってはいけない項目のリスト (おそらく不完全ですが) であり、 Troll Tech テクニカル FAQ により報告されているものです。 メンバ関数を再実装したものを追加する (古いバイナリが元の実装を呼び出すのが安全ではない場合)。というのは、 コンパイラは SuperClass::virtualFunction() 呼出しをコンパイル時に評価するからです (リンク時ではありません)。 仮想メンバ関数を追加または削除する。というのは、この作業は 全サブクラスの仮想関数テーブルのサイズと配置を変更するだろうからです。 インラインメンバ関数経由でアクセスされうるデータメンバのタイプを 変更したり、またはそれらを移動させたりする。 クラス階層を変更する。ただし、リーフ (訳注:下位クラスを持たないクラス) の新規追加を除く。 private データメンバを追加または削除する。というのは、 この作業は全サブクラスのサイズと配置を変更するだろうからです。 public もしくは protected メンバ関数がインライン関数でない場合に、 それらを削除する。 public もしくは protected メンバ関数をインライン化する。 インライン関数がおこなっていたことを変更する。 ただし、古いバージョンが動作し続けている場合を除く。 ポータブルなプログラムのメンバ関数のアクセス権 (すなわち、 public, protected または private) を変更する。というのは、 アクセス権を関数名に押し込んでしまうコンパイラも存在するからです。 C++ ライブラリの開発者は、この長ったらしいリストを手にして、 バイナリ互換性をなくすことになってしまう時折の更新作業以上のことを 特に考慮しなければなりません。 幸いにして、Unix ライクなシステム (Linux を含みます) では一つのライブラリの複数のバージョンを同時にロードすることができるので、 ディスクスペースを失うことにはなりますが、 ユーザは古いライブラリを必要とする ``古い'' プログラムをその後も実行することが可能ではあります。 動的にロードされる (Dynamically Loaded; DL) ライブラリ 動的にロードされる (dynamically loaded; DL) ライブラリは、 プログラムの起動時以外のときにロードされるライブラリです。 これはプラグインやモジュールを実装するのに特に役に立ちます。 というのは、プラグインが必要になるまで、 それをロードするのを待つことができるからです。例えば、 Pluggable Authentication Modules (PAM) システムは、 管理者が認証の設定や再設定をおこなえるようにするため、 DL ライブラリを使用しています。また、 全体を止めることなく、効率を上げる目的で、 その時々でコードをマシンコードにコンパイルし、 そのコンパイル後のものを使用するというインタプリタを実装するのにも役に立ちます。 この方法は、ジャストインタイムコンパイラや、マルチユーザダンジョン (multi-user dungeon; MUD) の実装時にも役に立ちます。 Linux では、実際のところ、DL ライブラリは形式という点においては特別ではありません。それらは、 標準的なオブジェクトファイル、 もしくは今までに述べたような標準的な共有ライブラリとして構築されています。 主な違いは、ライブラリが、 プログラムのリンク時や起動時に自動的にはロードされない、という点です。 その代わり、ライブラリをオープンし、シンボルを検索し、エラーを処理し、 ライブラリを閉じる、という API は存在します。この API を使うためには、 C ユーザはヘッダファイル <dlfcn.h> をインクルードする必要があります。 Linux によって使用されるインターフェースは本質的に Solaris 上のものと同じで、私が ``dlopen()'' API と呼ぼうとしているものです。 しかしながら、この同じインターフェースは全てのプラットフォームで サポートされているわけではありません。HP-UX は shl_load() という異なる機構を用いますし、Windows プラットフォームは完全に異なるインターフェースの DLL を使用します。 あなたの最終目標が広範なポータビリティならば、おそらく、 プラットフォーム間の差違を隠すラッピングライブラリの使用 を考えたほうがよいでしょう。一つのアプローチは、 モジュールの動的ローディングをサポートする glib ライブラリです。 これは、プラットフォームで土台となっている動的ローディング用ルーチンを使い、 それらの機能へのポータブルなインターフェースを実装します。glib については、 http://developer.gnome.org/doc/API/glib/glib-dynamic-loading-of-modules.html を参照してください。 glib のインターフェースはそのドキュメントの中で十分に説明されているので、 ここではこれ以上は述べません。 もう一つのアプローチは、libltdl を使うことです。これは、 GNU libtool の一部です。もっと多くの機能を望むならば、 CORBA Object Request Broker (ORB) を調べてみるのもよいでしょう。 Linux と Solaris でサポートされるインターフェースを 直接使うことに依然として興味をお持ちならば、読み進んでください。 dlopen() dlopen(3) 関数は、ライブラリをオープンし、使用するための準備をします。 C では、そのプロトタイプは次のようになります―― void * dlopen(const char *filename, int flag); ファイル名が ``/'' ではじまるならば (つまり絶対パスならば) 、 dlopen() はライブラリを検索しません。そうでないならば、dlopen() は次の順序でライブラリを検索します―― ユーザの LD_LIBRARY_PATH 環境変数内のコロンで区切られたディレクトリリスト /etc/ld.so.cache に指定されたライブラリリスト /usr/lib, 次が /lib dlopen() では、flag の値は、RTLD_LAZY ――``動的ライブラリのコードが実行されるときに、未定義シンボルを解決せよ'' という意味です――、もしくは、RTLD_NOW ――``dlopen() がリターンする前に全ての未定義シンボルを解決せよ、 それができないようならば失敗せよ'' という意味です――、 のどちらかでなければいけません。RTLD_GLOBAL は、 flag のどちらかの値と任意に論理和結合されるもので、 続けてライブラリをロードすることによりライブラリ内で定義されている 外部シンボルを得られる、ということを意味しています。デバッグ中は、 おそらく RTLD_NOW を使いたくなるでしょう。RTLD_LAZY を使うと、 解決されない参照があったときに不可解なエラーが生成されます。 RTLD_NOW を使うと、ライブラリのオープンには若干時間が多くかかるようになります (しかし、のちのちの検索スピードは速くなります) 。 このことがユーザインターフェースの問題になるようでしたら、 あとで RTLD_LAZY にかえることができます。 ライブラリがお互いに依存しているようなら (例えば、X が Y に依存している) 、 依存されているほうを先にロードしてください (この例で言えば、Y を先にロードし、 それから X をロードします) 。 dlopen() の戻り値は、他の DL ライブラリルーチンで使用される ``ハンドル'' ――その実体は隠蔽されるぺきものと考えられている―― です。 ロードの試みが成功しない場合、dlopen() は NULL を返しますので、 この値をチェックする必要があります。 同じライブラリが dlopen() で二回以上ロードされると、 同じファイルハンドルが返されます。 もしもライブラリが _init という名前のルーチンをエクスポートしていれば、 そのコードは dlopen() が戻る前に実行されます。あなたのライブラリでも、 初期化ルーチンを実装するためにこれを使うことができます。 詳細は を参照してください。 dlerror() dlerror() を呼べば、エラーを報告できます。dlerror() は、 dlopen(), dlsym() もしくは dlclose() の最後の呼出しによるエラーについて記述してある文字列を返します。 一つ変わっているのは、dlerror() を呼び出すと、以降の dlerror() の呼出しは、 ほかのエラーが発生するまで NULL を返すという点です。 dlsym() DL ライブラリが使えなければ、それをロードしても意味がありません。 DL ライブラリを使うための主となるルーチンは、dlsym(3) です。これは、 与えられた (オープン済みの) ライブラリ内にあるシンボルの値を検索するものです。 この関数は次のように定義されます―― void * dlsym(void *handle, char *symbol); handle は dlopen で返された値で、symbol はヌル文字で終端された文字列です。 回避可能ならば、dlsym() の結果を void* ポインタに格納しないでください。 というのは、それを利用するたびにキャストしなければいけなくなるからです (プログラムをメンテナンスしようとしている人たちに、より少ない情報しか 与えないことにもなります) 。 dlsym() は、シンボルが見つからなければ NULL という結果を返します。 シンボルが NULL もしくはゼロという値をとることはありえないと分かっていれば、 それで構いません。しかし、そうでない場合は潜在的に曖昧さが残ります。 もしも NULL を受け取った場合、それは、 そんなシンボルは存在しないということを意味するのでしょうか、 もしくはそのシンボルの値が NULL であることを意味するのでしょうか? 標準的な解答は、dlerror() をはじめに呼び (存在しているかもしれない エラー条件をクリアするためです)、それから シンボルを要求するために dlsym() を呼び、エラーが発生しているかどうかを調べるために再度 dlerror() を呼び出すことです。 コードの断片は次のようになるでしょう―― dlerror(); /* エラーをクリアする */ s = (actual_type) dlsym(handle, symbol_being_searched_for); if ((err = dlerror()) != NULL) { /* ハンドルエラー。シンボルを見つけられなかった */ } else { /* シンボルが見つかった。その値は s に格納されている */ } dlclose() dlopen() の逆が dlclose() で、これは DL ライブラリをクローズします。 dl ライブラリは動的なファイルハンドルへのリンク数を管理しているので、 同一動的ライブラリに対して、dlopen が成功した回数と同じ数の dlclose が呼ばれない限り、当該ライブラリは実際にはメモリ上から削除されません。 そのため、同じプログラムが同じライブラリを何回ロードしても、 問題にはなりません。 ライブラリの割当てが解除される場合は、(もしも存在するならば) _fini 関数が呼ばれます。 詳細は を参照してください。 DL ライブラリの例 dlopen(3) の man ページからの例をここに載せます。 この例は、数学ライブラリをロードし、2.0 のコサインを出力し、また、 全てのステップでエラーをチェックしています (推奨されています) ―― #include int main(int argc, char **argv) { void *handle; double (*cosine)(double); char *error; handle = dlopen ("/lib/libm.so", RTLD_LAZY); if (!handle) { fputs (dlerror(), stderr); exit(1); } cosine = dlsym(handle, "cos"); if ((error = dlerror()) != NULL) { fputs(error, stderr); exit(1); } printf ("%f\n", (*cosine)(2.0)); dlclose(handle); } ]]> このプログラムが "foo.c" という名前のファイルだとすると、 次のコマンドでプログラムを作成することができます。 gcc -Wl,export-dynamic -o foo foo.c -ldl ``-Wl,export-dynamic'' オプションは実際には必要ありませんが、 時々役に立つことがあります。ld(1) で次のように明記されています ――``ELF ファイルを作成しているとき、このオプションが、 全てのシンボルを動的シンボルテーブルに加えます。通常、動的シンボルテーブルは 動的オブジェクトによって使われるシンボルだけを含んでいます。 このオプションは dlopen の使用のために必要となります'' Linux システムだけで作業をしているならば、``-Wl,export-dynamic'' のかわりに ``-rdynamic'' を使えるけれども、ELF ドキュメントによれば、 非 Linux システム上の gcc では ``-rdynamic'' フラグは必ずしも機能しない、 ということには注意しておいてください。 雑録 nm コマンド nm(1) コマンドは、与えられたライブラリ内のシンボルのリストを報告します。 静的ライブラリ、共有ライブラリのどちらに対しても機能します。 nm(1) は与えられたライブラリで定義されているシンボル名、シンボルの値、 シンボルのタイプを表示します。また、そのライブラリ内に情報が存在するならば (-l オプションを見てください) 、シンボルがソースコード内のどこで (ファイル名と行番号) 定義されているかということも特定できます。 シンボルタイプについてはもう少し説明が必要です。 タイプは一文字で表示されます。小文字はそのシンボルがローカルであることを意味し、 大文字はそのシンボルがグローバル (外部) であることを意味します。 典型的なシンボルのタイプは次のものを含みます―― T (コードセクション内の普通の定義) D (初期化されたデータセクション) B (初期化されていないデータセクション) U (未定義。シンボルはライブラリによって使われているが、 ライブラリ内では定義されていない) W (weak. もしも他のライブラリもこのシンボルを定義していた場合、 その定義がオーバーライドする) 関数の名前は覚えているけれども、 それがどのライブラリで定義されているか正確には思い出せない場合、 ライブラリ名を見つけるため、nm の ``-o'' オプション (各ラインのファイル名の前に置きます) に grep を続けて使うことができます。 Bourne シェルであれば、/lib, /usr/lib, /usr/lib の直下のサブディレクトリ、 および /usr/local/lib 内の全ライブラリを対象にして ``cos'' を検索するには、 次のようにします―― nm -o /lib/* /usr/lib/* /usr/lib/*/* \ /usr/local/lib/* 2> /dev/null | grep 'cos$' nm に関するもっと多くの情報は、 info:binutils#nm にローカルにインストールされている nm の ``info'' ドキュメントで得られます。 特別な関数 _init と _fini 二つの特別な関数 _init と _fini は、モジュールの初期処理と終了処理を支援します。 もしもライブラリ内で関数 ``_init'' がエクスポートされていると、 そのライブラリのオープン時に dlopen() が呼ばれるたび、 その関数が呼び出されます。 C のプログラムでは、_init という名前の関数を定義することを意味します。 これに対応する _fini と呼ばれる関数も存在し、 これは、クライアントがライブラリの解放時に dlclose() を呼ぶたびに、呼び出されます (そして解放されます) 。 これらの関数の C プロトタイプは次のようになっています。 void _init(void); void _fini(void); Gcc でファイルを ``.o'' ファイルへとコンパイルするときは、 忘れずに ``-nostartfiles'' オプションを付けてください。 このオプションは、C コンパイラが .so ファイルに対してシステムスタートアップライブラリをリンクしないようにします。 このオプションを付けないと、``multiple-definition'' (重複定義) エラーになってしまいます。 _init と _fini に関する議論を加えることを提案してくれたこと、 およびその作成を手伝ってくれたことに対して、 Jim Mischel と Tim Gentry に感謝します。 共有ライブラリはスクリプト化できる 通常のライブラリ形式の代わりに、 特殊なスクリプト言語を使っているテキストファイルを共有ライブラリとして GNU ローダが認めることは、注目に値します。これは、 他のライブラリを間接的に結合させるのに役立ちます。例えば、 私の持つある一つのシステム上の /usr/lib/libc.so をリスティングしたものは次のようになります。 /* GNU ld スクリプト 共有ライブラリを使うが、幾つかの関数は静的ライブラリ内にしか 存在しない。そのため、二番目に試みる。 */ GROUP ( /lib/libc.so.6 /usr/lib/libc_nonshared.a ) これに関するより詳しい情報は、ld リンカスクリプト (ld コマンド言語) についての texinfo ドキュメントを参照してください。 一般的な情報は、info:ld#Options と info:ld#Commands にあり、よく使うコマンドは info:ld#Option Commands で説明されています。 GNU libtool 多くのシステムに移植されるアプリケーションを作成しているならば、 ライブラリを構築しインストールするのに、 GNU libtool を使用することを考慮したほうがよいかもしれません。 GNU libtool は、汎用的なライブラリサポートスクリプトです。 Libtool は、共有ライブラリ使用の複雑さを一貫性のあるポータブルな インターフェースで隠します。Libtool は、 オブジェクト作成、ライブラリのリンク (静的および共有) 、 実行可能ファイルのリンク、実行可能ファイルのデバッグ、 ライブラリのインストール、実行可能ファイルのインストール、 についてポータブルなインターフェースを提供します。 また、プログラムを動的にロードするためのポータビリティラッパー である libltdl も含んでいます。より詳細な情報は、 http://www.gnu.org/software/libtool/manual.html を参照してください。 極端に小さな実行可能ファイル 「本当に小さな Linux 用 ELF 実行可能ファイル作成に関する慌ただしいチュートリアル」 という文書が、よい参考となるでしょう。 これは、本当に小さな実行可能プログラムを作成する方法について論じています。 率直に言えば、一般的な環境では、これらのトリックのほとんどは使うべきではありませんが、 それらは、ELF が実際にどのように機能するかを示しているという点において、 極めて有益です。 さらに多くの例 下記に、全三つのアプローチ (静的、共有、および動的にロードされるライブラリ) の例をさらに挙げます。ファイル libhello.c は平凡なライブラリで、 ヘッダファイルとして libhello.h を持ちます。ファイル demo_use.c は、 そのライブラリの平凡な呼出しです。 静的ライブラリや動的ライブラリとして当該ライブラリを使う方法を示すため、 コメント付きのスクリプト (script_static と script_dynamic) があとに続きます。 さらに demo_dynamic.c と script_dynamic があとに続きます。 これらは共有ライブラリを動的にロードされるライブラリとして使う方法を示します。 ファイル libhello.c void hello(void) { printf("Hello, library world.\n"); } ]]> ファイル libhello.h ファイル demo_use.c ファイル script_static ファイル script_shared ファイル demo_dynamic.c #include /* "libhello.h" をインクルードする必要がないことに 注意してください。しかしながら、関連するものを 指定する必要はあります。dlsym() から得ようとして いる値を保持するタイプを指定する必要があります。*/ /* "simple_demo_function" タイプは、引数をとらず、 何も値を返さない関数を示しています。 */ typedef void (*simple_demo_function)(void); int main(void) { const char *error; void *module; simple_demo_function demo_function; /* 動的にロードされるライブラリをロードする */ module = dlopen("libhello.so", RTLD_LAZY); if (!module) { fprintf(stderr, "Couldn't open libhello.so: %s\n", dlerror()); exit(1); } /* シンボルを得る */ dlerror(); demo_function = dlsym(module, "hello"); if ((error = dlerror())) { fprintf(stderr, "Couldn't find hello: %s\n", error); exit(1); } /* DL ライブラリ内の関数を呼び出す */ (*demo_function)(); /* 全てが終了したので、物事をきれいに片付ける */ dlclose(module); return 0; } ]]> ファイル script_dynamic その他の情報源 下記のものは、ライブラリに関して特に役に立つ情報を含んでいます。 Daniel Barlow による ``The GCC HOWTO''. 特にこの HOWTO は、ライブラリ作成用のオプションと、 どのようにライブラリに問い合わせをするかについて論じています。 ここでは取り上げていない情報を扱っていますが、逆のことも言えます。 この HOWTO は、 http://www.linuxdoc.org の Linux Documentation Project から入手できます (訳注:``The GCC HOWTO'' の日本語訳は http://www.linux.or.jp/JF/JFdocs/GCC-HOWTO.html です)。 The Tool Interface Standards (TIS) 委員会による ``Executable and Linkable Format (ELF)'' (実際にはこれは、同委員会による the Portable Formats Specification Version 1.1 内の一つの章です). これは、ELF 形式 (Linux や GNU gcc に特化したものではありません) 及び、その形式に関する大量の詳細情報を提供するものです。 ftp://tsx-11.mit.edu/pub/linux/packages/GCC/ELF.doc.tar.gz を見てください。 MIT からファイルを取得するなら、 そのフォーマットが一般的ではないことに注意してください。 gunzip と untar を実行すると、``hps'' ファイルができます。 ファイルの最初と最後の行を削除し、``ps'' ファイルに名前を変更すれば、 普通のファイル名を持つ印字可能な Postscript ファイルを得られます。 Hongjui Lu による ``ELF: From the Programmer's Perspective''. これは、ELF に関する Linux と GNU gcc に特化した情報を提供します。 ftp://tsx-11.mit.edu/pub/linux/packages/GCC/elf.ps.gz で取得できます。 (訳者による追加) 佐野武俊さんによる ``Linux C Library (libc) について''. Linux C Library (libc) の概要について、その役割と歴史などを簡単にまとめたものです。 http://www.linux.or.jp/JF/JFdocs/libc-intro.html で参照できます。 著作権とライセンス この文書の著作権は David A. Wheeler にあり (Copyright (C) 2000)、 GNU 一般公有使用許諾 (GPL) により保護されます。 代価なしで再配布しても構いません。 文書の原文を ``プログラム'' と解釈し、次の条件も守ってください。
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. 本プログラムはフリー・ソフトウェアです。あなたは、Free Software Foundation が公表した GNU 一般公有使用許諾の「バージョン 2」或いは それ以降の各バージョンの中からいずれかを選択し、そのバージョンが 定める条項に従って本プログラムを再頒布または変更することができます。 This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 本プログラムは有用とは思いますが、頒布にあたっては、市場性及び 特定目的適合性についての暗黙の保証を含めて、いかなる保証も行ない ません。詳細については GNU 一般公有使用許諾書をお読みください。 You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA あなたは、本プログラムと一緒に GNU 一般公有使用許諾の写しを 受け取っているはずです。そうでない場合は、Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 0211-1307, USA へ手紙を書いてください。
これらの条項は、 他のウェブサイトがミラーリングをすることを許可するものですが、 あなたのミラーサイトがマスターサイトから 最新情報を自動的に取得するようにし、 マスターサイトへのハイパーリンクと共に マスターサイトのロケーション http://www.dwheeler.com/program-library を明示し、そして 著者として、私 (David A. Wheeler) に謝辞をお願いします。 はじめの二つは、 第一に、私が過去のバグについて繰り返し話を聞かされることを防ぎます。 単にあなたが文書を適切にミラーリングしていないという原因のために、 私は一年前に直したバグに関する話を聞きたくはありません。 マスターサイトへリンクを張ることにより、 ユーザはあなたのサイトが最新のものであるかどうかを確認できます。 非常に厳しいセキュリティ要求があり、そのためにインターネットへ 通常に接続するリスクを取ることができないサイトの問題に対して、 私は敏感です。このことがあなたの状況にあてはまるならば、 少なくとも、他のポイントへの接続を試みたり、 時折あなたの環境へのスニーカーネット・アップデート (訳注:スニーカーネット (sneakernet) ―― FD 等を持ち運びすることにより情報を共有するネットワーク) を試みるなどしてください。 このライセンスによれば、あなたはドキュメントを変更しても構いませんが、 あなたが書いたものではないものをあなたのものであると主張したり (つまり盗用です) 、 変更されたバージョンが原作であるかのようなふりをすることはできません。 著作物の変更は、著作物全体の著作権をあなたに譲渡するものではありません。 これは、著作権法の用語でいうところの ``public domain'' の著作物ではありません。ライセンスを詳細に見てください。特に、 ``You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.'' ――``ファイルを変更した旨とその変更日とを、変更したファイル上に 明確に表示すること'' ということには注意してください。 ライセンスがどのようなことを許可しているかについて質問がある場合は、 私に連絡を取ってください。たいていの場合には、 あなたの変更が他のみなさんの変更と共にマスターコピーへ統合されるよう、 あなたの変更を統合者 (現在は David A. Wheeler) へ送るのがよいでしょう。