共有ライブラリーを解剖する

共有ライブラリーを知る

共有ライブラリーはバージョン番号を使うことによって、古いアプリケーションとの互換性を保ちつつ、アプリケーションが使うライブラリーをアップグレードできるようにしています。この記事では、通常のLinux™システムの /usr/libに、なぜあれほど多くのシンボリック・リンクがあるのか、裏に隠れた真相を探ります。

Peter Seebach (developerworks@seebs.plethora.net), Author, Freelance

Peter SeebachPeter Seebachは健全とは思えないほどエミュレーターでOSを走らせていますが、それを楽しいことだと思っています。エミュレーターのエミュレーターのエミュレーターは走らせたことはありませんが、そのうちにするかも知れません。



2005年 1月 11日

共有ライブラリーは、最近のUNIX®システムで空間とリソースを効率良く使用するための、基本的なコンポーネントです。SUSE 9.1システムのCライブラリーは約1.3MBでできていますが、このライブラリーのコピーを /usr/binにある全てのプログラム(私は2,569も持っています!)に対してコピーすると、2、3ギガバイトの空間を食うことになります。

もちろん、この数字は水増しされています。つまり静的にリンクされたプログラムは、ライブラリーの内、そのプログラムが使う部分のみを取り込みます。それにしても、こうしたprintf()などの重複コピーによって、システムが肥満体のようになってしまいます。

共有ライブラリーは、ディスク・スペースだけではなく、メモリーも節約します。カーネルは共有ライブラリーのコピーを一つだけメモリーに持ち、それを複数のアプリケーションが共有します。こうすることで、printf()のコピーはディスク上に一つだけであればよいだけではなく、メモリー上にも一つだけで済むのです。これによって、パフォーマンスに歴然とした差が現れます。

この記事では、共有ライブラリーが使用している、裏に隠れた技術について学びます。そして、共有ライブラリーのバージョン管理手法によって、お粗末な共有ライブラリー実装が過去に起こしたような互換性問題の悪夢を防げることも学びます。まず、共有ライブラリーがどのように動作するのかを見てみましょう。

共有ライブラリーはどのように動作するか

考え方はごく簡単です。ライブラリーがあり、そのライブラリーを共有するのです。しかし、プログラムがprintf() を呼ぼうとする時に実際に起こること、つまり共有ライブラリーの実際の動作は、もう少し複雑です。

静的にリンクされたシステムでは、動的にリンクされたシステムよりも簡単です。静的にリンクされたシステムでは、生成されたコードが、ファンクションへの参照を処理します。リンカーはその参照を、ファンクションがロードされた実際のアドレスで置き換え、その結果できあがるバイナリー・コードが正しいアドレスを持つようにします。そうすると、このコードは実行されると単純に、対象となるアドレスにジャンプするのです。プログラム中のある点で、実際に参照されるオブジェクトにしかリンクされないので、この作業の管理は単純です。

ところが大部分の共有ライブラリーは動的にリンクされています。これは静的リンクの場合よりも幾つか、深い意味を含んでいます。一つは、ファンクションが呼ばれた時に、そのファンクションが実際にどのアドレスで実行するかを事前に予測できないことです!(BSD/OSにあるような、静的にリンクされた共有ライブラリー・スキーマもありますが、この記事で扱うような範囲ではありません。)

動的リンカーは、リンクされた各ファンクションに対して様々な仕事をする必要があるので、大部分のリンカーは怠け者です。つまりファンクションが呼ばれた時に、初めて作業を完了させるのです。Cライブラリーには外から見えるシンボルが1,000以上もあり、さらにローカルのシンボルが3,000近くあることを考えると、この考え方で大きな時間短縮ができることになります。

これが動作する秘密は、PLT(Procedure Linkage Table)と呼ばれるデータの塊、つまりプログラムが呼ぶ全てのファンクションをリストアップした、プログラム中のテーブルです。プログラムが開始する時に、ファンクションがロードされたアドレスをランタイム・リンカーに対して問い合わせるコードを、PLTは各ファンクションに対して持っています。そうするとプログラムは、テーブル中のそのエントリーを取り込み、そこにジャンプします。それぞれのファンクションが呼ばれるにつれ、それぞれのファンクションに対するPLTでのエントリーは、ロードされたファンクションへの直接ジャンプに単純化されます。

ただしこれでもやはり、余分な間接参照のレイヤーがあることは重要ですので、よく注意してください。つまり各ファンクション・コールは、テーブルへのジャンプで解決されるのです。


互換性は健全な関係のためだけではない

これはつまり、リンク先となるライブラリーは、そのライブラリーを呼ぶコードと互換性がある方が良い、ということ意味します。静的にリンクされた実行可能プログラムでは、何も変わらないであろうという保証が多少ありますが、動的リンクでは、その保証はありません。

では新しいバージョンのライブラリーが登場したら何が起きるのでしょう? 特に、対象のファンクションを呼ぶシーケンスが、新バージョンでは変わっていたとしたらどうでしょう?

ここでバージョン番号が救いに登場します。共有ライブラリーがバージョン番号を持つのです。プログラムがライブラリーに対してリンクされると、プログラムは、そのプログラムの中に含むべきライブラリーのバージョン番号を持ちます。動的リンカーはバージョン番号の一致をチェックします。ライブラリーが変更されているとバージョン番号が一致しないので、プログラムは新しいバージョンのライブラリーにはリンクされません。

また動的リンクは潜在的な利点として、バグを修正できます。ライブラリーの中でバグを修正し、何千ものプログラムを再コンパイルすることなく、その修正が利用できれば素晴らしいことです。ですから時々、新しいバージョンにリンクしたくなります。

残念ながら、そうすると、ある場合には新しいバージョンにリンクしたい、またある場合には古いバージョンの方が良い、ということが起こり得ます。ただしその解決方法もあるのです。つまり2種類のバージョン番号を持ち、

  • メジャー番号は、異なるライブラリー・バージョン間で互換性が無い可能性を示し、
  • マイナー番号はバグ修正のみを表すのです。

ですから大部分の場合では、メジャー番号が同じでマイナー番号が高いライブラリーをロードすれば安全であり、メジャー番号が今よりも高いライブラリーをロードするのは安全な習慣ではない、と考えるのです。

ユーザーが(そしてプログラマーが)ライブラリーの番号や更新を追跡しなくてもすむように、システムには大量のシンボリック・リンクがあります。一般的なパターンとしては、

libexample.so

libexample.so.N

へのリンクであり、ここでNは、システム上で見つかる最高のメジャー・バージョン番号です。

サポートされるメジャー・バージョン番号それぞれに対して

libexample.so.N

は逆に

libexample.so.N.M

へのリンクであり、ここでMは、最大のマイナー・バージョン番号です。

ですから、リンカーに対して-lexampleを規定すると、リンカーは一番最近のバージョンへのシンボリック・リンクへのシンボリック・リンクである、libexample.soを探します。一方、既存のプログラムがロードされると、リンカーはlibexample.so.Nをロードしようとします。ここでNは、元々リンクされていたバージョンの番号です。全員ハッピーというわけです!


デバッグするには、まずコンパイル方法を知る必要がある

共有ライブラリーで問題をデバッグするには、共有ライブラリーがどのようにコンパイルされるのかを、もう少し知っておくと便利です。

伝統的な静的ライブラリーでは、生成されるコードは普通、.aで終わる名前でライブラリー・ファイルと一緒にされ、そしてリンカーに渡されます。動的ライブラリーでは、ライブラリーのファイル名は通常、.soで終わります。ファイル構造も少し異なります。

通常の静的ライブラリーは、arユーティリティーで作られるフォーマットです。arユーティリティーは基本的に、非常に単純なアーカイブ・プログラムであり、tarと似ていますが、tarよりも単純です。これとは対照的に、共有ライブラリーは普通、もっと複雑なファイル・フォーマットで保存されます。

最近のLinuxシステムでは、このフォーマットというのは普通、ELF(Executable and Linkable Format)バイナリー・フォーマットを指します。ELFでは、各ファイルにはELFヘッダーがあり、その後にゼロまたは何らかのセグメント、そしてゼロまたは何らかのセクションが続きます。セグメントはファイルのランタイム実行に必要な情報を含み、セクションはリンクと再配置のために重要なデータを含みます。全ファイルの各バイトは、一度に一セクションを超えない範囲の単位で扱われますが、セクションにカバーされない「みなしご」のバイトがあり得ます。UNIXの実行可能プログラムでは通常、一つのセグメントの中に一つ以上のセクションが含まれます。

ELFフォーマットには、アプリケーションやライブラリーに対する仕様があります。ただしライブラリー・フォーマットは、オブジェクト・モジュールの単純なアーカイブよりも、ずっと複雑です。

リンカーはシンボルへの参照を並べ替え、そのシンボルがどのライブラリーで見つかったのか、という注記を作ります。静的ライブラリーにあるシンボルは、最終的な実行可能プログラムに追加されます。共有ライブラリーにあるシンボルはPLTに置かれ、PLTへの参照が作られます。こうした作業が終わると、できあがった実行可能プログラムはシンボルのリストを持っていることになります。このシンボルを、そのプログラムの実行時にロードするライブラリーから参照するのです。

実行時には、アプリケーションが動的リンカーをロードします。実際、動的リンカー自体が、共有ライブラリーと同種のバージョン規則を使います。例えばSUSE Linux 9.1では、ファイル/lib/ld-linux.so.2は、/lib/ld-linux.so.2.3.3へのシンボリック・リンクです。一方、/lib/ld-linux.so.1を探しているプログラムは、新しいバージョンを使おうとはしません。

そうすると動的リンカーは、楽しい仕事を次々にこなします。プログラムは元々どのライブラリーに(そしてどのバージョンに)リンクされていたのかを調べ、それらをロードします。ライブラリーのロードは次のような手順で行われます。

  • ライブラリーを見つける(システム上の、どこのディレクトリーにもある可能性があります)
  • ライブラリーをプログラムのアドレス空間にマップする
  • ライブラリーが必要とする、ゼロで満たした何ブロックかのメモリーを割り振る
  • ライブラリーのシンボル・テーブルを添付する

このプロセスをデバッグするのは簡単ではありません。出くわす可能性のある問題としては、何種類かがあります。例えば、動的リンカーが指定のライブラリーを見つけられないと、プログラムのロードを中止してしまいます。求めるライブラリーはすべて見つかったものの、シンボルが見つからない場合にも、やはり中止してしまいます(ただしそのシンボルを参照しようとする試みが実際に起こるまでは、中止はしません)。ただしシンボルがそこに無ければ、普通は最初のリンクの時に気が付くはずなので、これは実際には稀な場合です。


動的リンカーのサーチ・パスを修正する

プログラムをリンクする時には、実行時にサーチすべきパスを追加して指定することができます。gccでは、その構文は-Wl,-R/pathです。プログラムが既にリンクされている場合には、環境変数LD_LIBRARY_PATHを設定することによって、その振る舞いを変更することができます。通常これが必要なのは、システム全体に渡るデフォルトとは異なるパスをアプリケーションが探そうとする場合であり、大部分のUNIXシステムでは稀です。理論的に言えばMozillaを作った人達は、パスを設定してコンパイルしたバイナリーを配布することができたはずですが、彼らは実行可能プログラムを起動する前にライブラリー・パスを適切に設定する、ラッパー・スクリプトを配布する方を好んだようです。

ライブラリー・パスを設定するということは、2つのアプリケーションが互換性のないバージョンのライブラリーを要求するという稀な場合に、回避策があることを意味します。つまり、ラッパー・スクリプトを利用すれば、一方のアプリケーションが必要とする特別バージョンのライブラリーを使って、そのアプリケーションにディレクトリーをサーチさせることができるのです。とてもすっきりしている解決策とは言えませんが、場合によっては、これが最善なこともあり得るのです。

何らかの理由から、多くのプログラムに対してパスを追加したいという場合にも、システムのデフォルト・サーチ・パスを変更することができます。動的リンカーは、デフォルトでサーチすべきディレクトリーのリストを含む、/etc/ld.so.confで制御されます。LD_LIBRARY_PATHで規定されたパスはどれも、ld.so.confにリストアップされたパスよりも前にサーチされます。ですからユーザーは、こうした設定をオーバーライドすることができるのです。

大部分のユーザーにとって、システム・デフォルトのライブラリー・サーチ・パスを変更する理由はありません。ツールキットにあるライブラリーとリンクしたいとか、新しいバージョンのライブラリーに対してプログラムをテストしたい、などといった、サーチ・パス変更の理由となりそうなものに対しては、一般的に環境変数の方が適切なのです。

lddを使う

共有ライブラリーの問題をデバッグするのに便利なツールの一つがlddです。lddという名前はlist dynamic dependencies(動的依存性をリストアップする)に由来しています。このプログラムは、与えられた実行可能プログラムまたは共有ライブラリーを調べ、どの共有ライブラリーをロードする必要があるのか、またどのバージョンを使用するのかを判断します。その出力は次のようなものになります。

リスト1. /bin/shの依存性
$ ldd /bin/sh
        linux-gate.so.1 =>  (0xffffe000)
        libreadline.so.4 => /lib/libreadline.so.4 (0x40036000)
        libhistory.so.4 => /lib/libhistory.so.4 (0x40062000)
        libncurses.so.5 => /lib/libncurses.so.5 (0x40069000)
        libdl.so.2 => /lib/libdl.so.2 (0x400af000)
        libc.so.6 => /lib/tls/libc.so.6 (0x400b2000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

「単純な」プログラムがどれほど多くの共有ライブラリーを使うものか、知ってみると驚くものです。libhistorylibncursesを呼んでいるという場合が正にそうでしょう。それを調べるために、再度lddコマンドを実行します。

リスト2. libhistoryの依存性
$ ldd /lib/libhistory.so.4
        linux-gate.so.1 =>  (0xffffe000)
        libncurses.so.5 => /lib/libncurses.so.5 (0x40026000)
        libc.so.6 => /lib/tls/libc.so.6 (0x4006b000)
        /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)

ある場合には、アプリケーションは追加のライブラリー・パスを規定する必要があります。例えばMozillaバイナリーでlddを実行しようとする場合の最初の数行は、次のようになります。

リスト3. サーチ・パスに無いアイテムに対してlddを実行した結果
$ ldd /opt/mozilla/lib/mozilla-bin
		linux-gate.so.1 =>  (0xffffe000)
		libmozjs.so => not found
		libplds4.so => not found
		libplc4.so => not found
		libnspr4.so => not found
		libpthread.so.0 => /lib/tls/libpthread.so.0 (0x40037000)

なぜこれらのライブラリーは見つからないのでしょう? その理由は、これらのライブラリーが、ライブラリーに対する通常のサーチ・パス上にないからです。実際これらのライブラリーは/opt/mozilla/libにあります。ですから一つの答えは、このディレクトリーをLD_LIBRARY_PATHに加えることです。

もう一つの選択肢としては、パスを. に設定し、そのディレクトリーからlddを実行することです。ただしこの方法は、より大きな危険を伴います。カレント・ディレクトリーをライブラリー・パスに置くということは、カレント・ディレクトリーを実行パスに置くのと同じくらい、潜在的な危険を伴うものです。

この場合では、これらが置かれているディレクトリーを、システム全体に渡るサーチ・パスに置くのが良い考えではないことは明らかです。Mozilla以外は、これらのライブラリーを必要としないからです。

Mozillaをリンクする

Mozillaに関する追加説明として、数行のライブラリーしか見たことがないという読者のために、もう少し典型的で大きなアプリケーションの例を下記に挙げました。Mozillaの起動がなぜ、あれほど長くかかるのか、これで理由が分かるでしょう。

リスト4. mozilla-binの依存性
linux-gate.so.1 =>  (0xffffe000)
libmozjs.so => ./libmozjs.so (0x40018000)
libplds4.so => ./libplds4.so (0x40099000)
libplc4.so => ./libplc4.so (0x4009d000)
libnspr4.so => ./libnspr4.so (0x400a2000)
libpthread.so.0 => /lib/tls/libpthread.so.0 (0x400f5000)
libdl.so.2 => /lib/libdl.so.2 (0x40105000)
libgtk-x11-2.0.so.0 => /opt/gnome/lib/libgtk-x11-2.0.so.0 (0x40108000)
libgdk-x11-2.0.so.0 => /opt/gnome/lib/libgdk-x11-2.0.so.0 (0x40358000)
libatk-1.0.so.0 => /opt/gnome/lib/libatk-1.0.so.0 (0x403c5000)
libgdk_pixbuf-2.0.so.0 => /opt/gnome/lib/libgdk_pixbuf-2.0.so.0 (0x403df000)
libpangoxft-1.0.so.0 => /opt/gnome/lib/libpangoxft-1.0.so.0 (0x403f1000)
libpangox-1.0.so.0 => /opt/gnome/lib/libpangox-1.0.so.0 (0x40412000)
libpango-1.0.so.0 => /opt/gnome/lib/libpango-1.0.so.0 (0x4041f000)
libgobject-2.0.so.0 => /opt/gnome/lib/libgobject-2.0.so.0 (0x40451000)
libgmodule-2.0.so.0 => /opt/gnome/lib/libgmodule-2.0.so.0 (0x40487000)
libglib-2.0.so.0 => /opt/gnome/lib/libglib-2.0.so.0 (0x4048b000)
libm.so.6 => /lib/tls/libm.so.6 (0x404f7000)
libstdc++.so.5 => /usr/lib/libstdc++.so.5 (0x40519000)
libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x405d5000)
libc.so.6 => /lib/tls/libc.so.6 (0x405dd000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
libX11.so.6 => /usr/X11R6/lib/libX11.so.6 (0x406f3000)
libXrandr.so.2 => /usr/X11R6/lib/libXrandr.so.2 (0x407ef000)
libXi.so.6 => /usr/X11R6/lib/libXi.so.6 (0x407f3000)
libXext.so.6 => /usr/X11R6/lib/libXext.so.6 (0x407fb000)
libXft.so.2 => /usr/X11R6/lib/libXft.so.2 (0x4080a000)
libXrender.so.1 => /usr/X11R6/lib/libXrender.so.1 (0x4081e000)
libfontconfig.so.1 => /usr/lib/libfontconfig.so.1 (0x40826000)
libfreetype.so.6 => /usr/lib/libfreetype.so.6 (0x40850000)
libexpat.so.0 => /usr/lib/libexpat.so.0 (0x408b9000)

共有ライブラリーについて、さらに学ぶ

Linuxでの動的リンクに関してさらに学びたい人には、様々な選択肢があります。GNUコンパイラーとリンカー・ツール・チェーンのドキュメンテーションは素晴らしい資料です。ただし実質的な内容はinfoフォーマットで保存されており、標準のmanページには書かれていません。

ld.soのマニュアル・ページは、動的リンカーの振る舞いを修正するための変数を幅広く網羅しています。また、過去に使われてきた様々なバージョンの動的リンカーに関する説明もされています。

大部分のLinuxドキュメンテーションでは、すべての共有ライブラリーは動的にリンクされていると想定しています。これはLinuxシステムでは、一般的にその通りだからです。静的にリンクされた共有ライブラリーを作るのは大仕事であり、ほとんどのユーザーには特にメリットはありません。ただし静的にリンクされた共有ライブラリーのサポート機能を持つシステムでは、そのパフォーマンス差は歴然としたものになります。

もし店頭で市販されている、パッケージ化されたシステムを使っているユーザーであれば、出くわす共有ライブラリーのバージョンの種類はそれほど多くはないでしょう。恐らくシステムは単に、そのシステムがリンクされている共有ライブラリーを一緒にパッケージしているだけです。ところが頻繁なアップデートやソースのビルドを行うような人であれば、古いバージョンの共有ライブラリーも「万が一のために」捨てずにおきがちなので、無数のバージョンの共有ライブラリーを相手にせざるを得なくなります。

毎度のことですが、もっと知ろうと思うのであれば、実際に試してみることです。システム上のほとんどすべてのものが、幾つもない、同じ共有ライブラリーを参照していることを忘れないでください。ですからシステムで核となる共有ライブラリーを壊してしまうと、何らかのシステム回復ツールをいじらざるを得ない羽目になります。

参考文献

  • John Levine著による Linkers and Loaders(1999年10月刊)は、コンパイル時間とランタイム・プロセスに焦点を絞った、権威ある資料です(一部はオンラインでも読むことができます。)
  • 共有ライブラリーに対するバージョン規約がなぜ重要なのか不思議に思う人は、この文を読んでください。
  • GNU Cライブラリーのオーバーライド -- やすやすと行う方法(developerWorks, 2002年4月)は、ルート権限無しに、また全ライブラリーを再ビルドせずに個々のライブラリー・ファンクションをオーバーライドするには、動的リンクをどのように使うべきかを説明しています。
  • Writing DLLs for Linux apps(developerWorks, 2001年10月)は、新しいLinuxアプリケーションを書くことなく機能を追加するために、動的リンクされたライブラリーがいかに有効かを強調しています。
  • オブジェクト不指向の共用オブジェクト」(developerWorks, 2001年4月)では、動的ロード可能なライブラリーの書き方と、その課程で使うツールに関して解説しています。
  • Linuxで使用する共有オブジェクト」(developerWorks, 2004年5月)は、共有メモリー・プロセスを動作させるための方法を解説しています。
  • developerWorksのLinuxゾーンには、Linux開発者のための資料が豊富に取り揃えられています。
  • developerWorks blogsに参加してdeveloperWorksコミュニティーに加わってください。
  • Developer BookstoreのLinuxセクションでは、Linux関係の書籍が割引して購入できますので、ぜひご利用ください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=231753
ArticleTitle=共有ライブラリーを解剖する
publish-date=01112005