Linux共有ライブラリの簡単なまとめ

Linuxで共有ライブラリ(*.so)を作るようになったのでちょっと勉強してみた。今までは使うだけだったので、以下のようなことは知っていた。

  • 作るときはgccの-sharedオプションを使う。
  • 使うときはgccの"-lライブラリ名"でリンクするライブラリを指定する。
  • リンク時のライブラリ探索パスは-Lオプションで指定する。
  • 実行時のライブラリ探索パスは/etc/ld.so.confに書いてあるディレクトリ。環境変数LD_LIBRARY_PATHでも指定可能。
  • ライブラリを作るときは、.cから.oを作るときに-fPICをつけるといいらしい。
  • 新しくライブラリを入れたときはldconfigするといいらしい。

逆に今まであまり知らなかったこと。

  • ほとんどのライブラリはlibhoge.so, libhoge.so.1, libhoge.so.1.1のように3つくらいのファイルがあり、libhoge.soやlibhoge.so.1はシンボリックリンクになっている。バージョン管理のためということは聞いたことあるがどういう仕組みなのか?
  • 共有ライブラリを作るとき、-Wl,-soname=libhoge.so.1 -o libhoge.so.1.0みたいにするらしいが、sonameとは何か?
  • ldconfigは何をしてるのか?

以下調べてたり試したりしたことのまとめ。間違ってたらすいません。

ライブラリ名に関する用語

呼び方 意味
soname lib + ライブラリ名 + .so + バージョン libhoge.so.1
完全記述の(fully-qualified)soname sonameにパスを付けたもの /usr/lib/libhoge.so.1
real name sonameにマイナー番号、リリース番号を付けたもの libhoge.so.1.0.0
linker name コンパイラが使用する名前 libhoge.so

sonameに付くバージョンは、インターフェース非互換になったときに上げる。つまり、既存の関数の引数を変えた場合や、公開している構造体のメンバ変数を変えた場合などである(構造体の末尾に追加するだけなら互換らしい)。

real nameに付くマイナー番号やリリース番号は互換性のある変更を行ったときに上げる。公開インターフェースは変えず、内部のバグフィックスのみなどのときである。また、関数の追加だけの場合も互換性は保たれる。

soname

ここで注意しなければならないのは、sonameが使われる場所が2ヶ所あるということだ。一つはファイル名であり、これはreal nameへのシンボリックリンクである。もう一つは共有ライブラリの中に埋め込まれたsonameである。これはobjdump -pなどで見ることができる。例えばzlibを見てみると、

> objdump -p /usr/lib/libz.so.1.2.3
中略
Dynamic Section:
  NEEDED               libc.so.6
  SONAME               libz.so.1
後略

となり、sonameがlibz.so.1であることが分かる。

このダイナミックセクションに埋め込まれるsonameを指定するのがライブラリ作成時の-Wl,-soname=libhoge.so.1というオプションである。従って、ファイル名をlibxxx.so.1とし、ファイル埋め込まれるsonameをlibyyy.so.1などと全く別の名前にすることもできる。が、そのようなことをしてもリンクや実行時に困るだけなのでやらない。

なお、sonameやreal nameの付け方はライブラリによって多少流儀が異なるようである。例えば手元の環境ではlibGLはlibGL.so.1とlibGL.so.1.2となっており、real nameのリリース番号がない。また、librubyはlibruby-1.8とlibruby-1.8.7になっており、objdumpで見るとlibruby-1.8がsonameで7がマイナー番号になっているようだ。

位置独立コード

ライブラリを作るときは.c -> .o -> .soの順に作るが、.c -> .oをのときに-fPICを付ける。この理由は参考文献2を参照。位置独立コードの方が以下の点で優れているらしい。

  • soファイルのサイズが小さい。
  • 動的リンク時の再配置の時間がかからない。-> 起動にかかる時間が短くなるということだと思う。
  • 他のプロセスとテキストが共有できる。-> libhoge.soを使うプロセスが2つ立ち上がったときに位置独立コードでないと実メモリ上にlibhoge.soが2つ載ってしまうということ?

以下実際にライブラリを作り、バージョンアップする例。

ライブラリの作成とldconfig

hello.c、util.cというファイルからlibhelloを作ってみる。最初のバージョンなのでsonameはlibhello.so.1、real nameはlibhello.so.1.0.0だ。

> gcc -fPIC -c hello.c
> gcc -fPIC -c util.c
> gcc -shared -Wl,-soname=libhello.so.1 -o libhello.so.1.0.0 hello.o util.o

出来上がったlibhello.so.1.0.0を例えば/usr/local/libに置く(Red Hat系のディストリビューションではデフォルトでは/usr/local/libはld.so.confに含まれないため、追加しておく)。そしてldconfigを実行する。

> sudo cp libhello.so.1.0.0 /usr/local/lib
> sudo ldconfig

ldconfigはld.so.confに含まれるディレクトリを探索し、自動的にsonameをファイル名とするシンボリックリンクを作成する。この例では/usr/local/lib/libhello.so.1が作られ、libhello.so1.0.0へのリンクである。ここで使われるsonameはライブラリに埋め込まれた名前が使われる。

他にも/etc/ld.so.cacheの更新も行うようだが、何をしているのかよくわからなかった。

ldconfigはsonameを作ってはくれるがlinker nameは作ってくれないので手動で作る必要がある(この理由については参考文献1を参照)。

> sudo ln -s libhello.so.1.0.0 libhello.so

リンク

libhelloを使うソースファイル(test1.c)があり、実行ファイル(test1)を作ってみる。コマンドは以下のようになる。

> gcc -o test1 test1.c -lhello

オプション-lhelloによってlibhello.soというファイルが探索される。libhello.soがない場合はエラーとなる。libhello.so.1やlibhello.so.1.0.0があっても自動的には読んでくれたりはしない。ここではlibhello.soはシンボリックリンクであり、結果としてそのリンク先であるlibhello.so.1.0.0が使われる。こうして実行ファイルtest1ができあがる。このtest1に対してlddを実行すると以下のようになる。

> ldd test
        linux-gate.so.1 =>  (0xffffe000)
        libhello.so.1 => /usr/local/lib/libhello.so.1 (0xb7f9a000)
        libc.so.6 => /lib/libc.so.6 (0xb7e3d000)
        /lib/ld-linux.so.2 (0xb7faa000)

このように、リンクするライブラリはリンクのときに使われたlibhello.so.1.0.0ではなく、そこに埋め込まれたsonameになることに注意。

実行

test1を実行するとlddの出力結果にあるライブラリが自動的に読み込まれる。ここではlibhello.so.1だが、このときはlibhello.so.1という名前のファイルを使う。先ほどのldconfigで作られたlibhello.so.1はここで役に立つ。

互換性のある変更

ここでhello.cにバグが見つかったため修正を加える必要が生じたとする。幸いインターフェースの変更はなく互換性のある変更である。ただしその変更は既存の実効ファイルであるtest1にも反映されなければならない。

互換性のある変更の場合はマイナー番号やリリース番号のみを上げる。ここではマイナー番号を上げlibhello.so.1.1.0にしてライブラリを再ビルドする。

> gcc -fPIC -c hello.c
> gcc -shared -Wl,-soname=libhello.so.1 -o libhello.so.1.1.0 hello.o util.o

でき上がったlibhello.so.1.1.0を/usr/local/libに置き、ldconfigを実行する。

> sudo cp libhello.so.1.1.0 /usr/local/lib
> sudo ldconfig

これでシンボリックリンクlibhello.so.1が作り直され、libhello.so.1.1.0にリンクされる。

/usr/local/libには同じsonameを持つ2つのファイル(libhello.so.1.0.0とlibhello.so.1.1.0)がある状態だが、ldconfigはどのような規則でリンクを作るのかはわからなかった。マイナーバージョン・リリース番号の数値を比較しているのか、更新時刻が新しい方なのか、あるいは単にたまたま先(または後)に見つかったものになるのかは不明だ。もしかしたらldconfigを行っても古いバージョンへのリンクになってしまうこともあるかもしれない。

linker nameであるlibhello.soも忘れずに更新し、libhello.so.1.1.0へのリンクにしておく。

sudo ln -sf libhello.so.1.1.0 libhello.so

これでライブラリの更新は完了であり、実行ファイルtest1も新しいライブラリを使うようになる。実行時に読むファイルはlddの結果にあるとおりlibhello.so.1であり、これはlibhello.so.1.1.0へのリンクになっているからである。

互換性のない変更

今度はhello.cに互換性のない変更を加える必要が生じたとする。関数の引数が変わった場合などである。さらにその新しいバージョンを使う実行ファイル(test2: ソースファイルはtest2.c)も作る。

互換性のない変更のため、今度はバージョン番号を上げてlibhello.so.2.0.0とする。sonameはlibhello.so.2だ。

> gcc -fPIC -c hello2.c
> gcc -shared -Wl,-soname=libhello.so.2 -o libhello.so.2.0.0 hello.o util.o

これを/usr/local/libに置き、ldconfigを実行する。libhello.soも作るのも忘れないように。

> sudo cp libhello.so.2.0.0 /usr/local/lib
> sudo ldconfig
> sudo cp ln -sf libhello.so.2.0.0 libhello.so

この結果libhello.so.2.0とlibhello.soが作られる。今/usr/local/libは以下のような状態だ。

> ls -l /usr/local/lib/libhello.so*
 lrwxrwxrwx 1 root root   15 10月24日 09:44 libhello.so -> libhello.so.2.0.0*
 lrwxrwxrwx 1 root root   15 10月24日 09:40 libhello.so.1 -> libhello.so.1.1.0*
 -rwxr-xr-x 1 root root 4168 10月24日 09:41 libhello.so.1.0.0*
 -rwxr-xr-x 1 root root 4168 10月24日 09:40 libhello.so.1.1.0*
 lrwxrwxrwx 1 root root   15 10月24日 09:43 libhello.so.2 -> libhello.so.2.0.0*
 -rwxr-xr-x 1 root root 4162 10月24日 09:43 libhello.so.2.0.0*

ここでtest2.cをビルドする。

> gcc -o test2 test2.c -lhello

オプション-lhelloによってlibhello.so、つまりそのリンク先であるlibhello.so.2.0.0とリンクされる。このためlinker nameは開発に使いたいバージョンへのリンクにしておく必要がある。

リンクされたlibhello.so.2.0.0のsonameはlibhello.so.2であるため、test2の実行時はlibhello.so.2が使われる。これはlibhello.so.2.0.0へのリンクであるため、実行時もlibhello.so.2.0.0が使われる。

一方古いバージョンを使うtest1の方は再ビルドをしていないため実行時にlibhello.so.1が読み込まれる。これはlibhello.so.1.1.0へのリンクであり、問題なく動かすことができる。もしtest1を再ビルドをしてしまうとlibhello.so.2へリンクされてしまい正常動作しなくなるだろう。

参考文献

  1. Program Library HOWTO
  2. Linux の共有ライブラリを作るとき PIC でコンパイルするのはなぜか

結局のところこの文書は参考文献1をまとめただけのようなものだ。

残った疑問

  • 同じsonameを持つライブラリがあったときにldconfigはどちらを優先するのか。
  • ld.so.cacheは何をキャッシュしているのか。

追記 (2015/11/22)

はてなダイアリーからはてなブログに移行したら一部表示がおかしくなっていたのを修正。