共有ライブラリのバージョニング

2004.08.29新規作成。(2001.05.19の日記に加筆・修正)

共有ライブラリ(共有オブジェクト)でライブラリのインターフェイスが変わってもメジャー番号を上げないで済ます方法。

最初のライブラリ

まず簡単な共有ライブラリを作ってみる。このライブラリはsotest.h、libsotest1.ccの二つのファイルからなり、関数add()を公開する。

sotest.h
  2| extern "C" {
  3| int add(int x, int y);
  4| }
libsotest1.cc
  1| #include "sotest.h"
  2| 
  3| int add(int x, int y) {
  4|   return x + y;
  5| }

このライブラリを使うプログラムを書く。

main.cc
  1| #include <stdio.h>
  2| #include "sotest.h"
  3| 
  4| int main() {
  5|   printf("add 1, 2 => %d\n", add(1, 2));
  6|   return 0;
  7| }
  8| 

これらをコンパイルするMakefileは次のようになる(抜粋)。

main: main.o libsotest.so
	$(CC) main.o $(LDFLAGS) -o $@ -L. -lsotest -lstdc++

libsotest.so: libsotest.so.1.0
	ln -sf $< libsotest.so.1
	ln -sf libsotest.so.1 libsotest.so

libsotest.so.1.0: libsotest.o
	$(CC) $^ $(LDFLAGS) -shared -Wl,-soname,libsotest.so.1 -o $@

libsotest.o: libsotest1.cc
	$(CC) $(CFLAGS) -fPIC -c $< -o $@

生成される順に(Makefileでは下のほうから)説明すると、まずlibsotest1.ccをコンパイルするときに、-fPICオプションを指定する。これによりgccは位置独立コードを生成するようになる。

次に、libsotest.oからlibsotest.so.1.0を生成する。-sharedオプションは、共有オブジェクトを生成するために指定する。-Wl,...はリンカオプションであり、-sonameオプションはELF共有オブジェクトのDT_SONAMEエントリを設定する。

さらに、シンボリックリンクlibsotest.soおよびlibsotest.so.1を生成する。

libsotest.soは、実行プログラムのコンパイル時にシンボルの解決に用いられる。libsotest.so.1 (DT_SONAMEエントリの値を名前とするファイル) がプログラムの実行時に動的にリンクされる。ここではいずれもlibsotest.so.1.0へのシンボリックリンクなので、実際には〜.so.1.0が使われる。

そのため、〜.so.1へのシンボリックリンクを張れば、共有ライブラリの実体のファイル名は全く別のものでも実行時にリンクできる。

実行結果は次のとおり。

$ LD_LIBRARY_PATH=. ./main
add 1, 2 => 3

ライブラリの変更

ライブラリにバイナリインターフェイスを変更しないような修正を加えたときは,ファイル名をlibsotest.so.1.1とし,DT_SONAMEエントリはlibsotest.so.1とすればよい。さらに、libsotest.so.1 -> libsotest.so.1.1, libsotest.so -> libsotest.so.1というシンボリックリンクを張れば,このライブラリを使う実行プログラムを再コンパイルする必要はない。

インターフェイスを変更したり削除したりするような修正があったとき、あるいは同じインターフェイスであっても動作が変わったときには、通常は、libsotest.so.2.0とする。このようにso-nameのメジャー番号を進めなければ、旧いインターフェイスを想定している利用プログラムがクラッシュしたりすることになる。

しかし,libcのように,メジャー番号を進めると他のプログラムへの影響が非常に大きい場合,メジャー番号を維持したまま動作を変更したい場合がある。

このような状況に対処するには、ライブラリにリンクしていた既存の実行ファイルでは古い内容の関数を呼び出すようにし、新しいプログラムから呼ばれたときには動作を変えるようにする。このために、.symverというGNUアセンブラの命令が使える。では、先ほどのadd()関数の意味を変えてみる。

libsotest2.cc
  1| #include "sotest.h"
  2| 
  3| extern "C" {
  4| int add_old(int x, int y);
  5| int add_new(int x, int y);
  6| }
  7| 
  8| int add_old(int x, int y) {
  9|   return x + y;
 10| }
 11| 
 12| int add_new(int x, int y) {
 13|   return x - y;
 14| }
 15| 
 16| __asm__(".symver add_old,add@SOTEST_1.0");
 17| __asm__(".symver add_new,add@@SOTEST_2.0");

libsotest2.defファイルを作成する。

SOTEST_1.0 {
  global:
    add;
  local:
    *;
};

SOTEST_2.0 { } SOTEST_1.0;

共有ライブラリを生成するときに,リンカにこのdefファイルを与えるよう、Makefileを書き換える。

libsotest.so.1.0: libsotest.o
	$(CC) $^ $(LDFLAGS) -shared -Wl,-soname,libsotest.so.1 -o $@ \
	     -Wl,--version-script,libsotest2.def

これで,古いプログラムでは以前と同じ動作,新しくコンパイルするプログラムでは新しい動作になる。

$ LD_LIBRARY_PATH=. ./main1
add 1, 2 => 3

$ LD_LIBRARY_PATH=. ./main
add 1, 2 => -1

外部リンク

.symver
.symverの使い方について。
Library Interface Versioning in Solaris and Linux
より詳しい解説。
ELF - osdev-j
ELFフォーマットに関する文献などへのリンク