The Man Who Fell From The Wrong Side Of The Sky:2008年3月27日分

[最新版] [一覧] [前月] [今月] [翌月]

2008/3/27(Thu)

[NetBSD] tech-userlevel

なんかmajor crunkしたライブラリをmark as obsoleteしてしまったみたいだね。

共有ライブラリの前方互換性が壊れる時

以前のエントリで、共有ライブラリの「後方」互換について説明しましたが、今度は「前方」互換について。

共有ライブラリの前方互換とは

ことを保障するということです。

よく似た言葉である後方互換についておさらいすると

ことを保障するものでした、前方互換とは視点が逆になってます。
この両者を混同しないようによーく注意してください。

どのような変更を行うとこの前方互換が損なわれるのでしょうか?
それは共有ライブラリに新しい関数や変数(=グローバルシンボル)が追加された場合などです *1

foo.h

extern void foo(void);

foo.c

#include <stdio.h>
#include "foo.h"

void
foo()
{
	printf("foo\n");
}

これをlibfoo.so.0.0としてコンパイルします。

$ gcc -shared -Wl,-soname,libfoo.so.0 -o libfoo.so.0.0 foo.c
$ ln -sf libfoo.so.0.0 libfoo.so.0
$ ln -sf libfoo.so.0 libfoo.so

次にlibfoo.so.0をリンクするアプリケーション、test_fooを作成します。
test_foo.c

#include "foo.h"
int
main(void)
{
	foo();
}

コンパイルして実行します。

$ gcc -Wl,-rpath,. -L. -lfoo -o test_foo test_foo.c
$ ./test_foo
foo

次にこのlibfooに新しい関数を追加します。
foo.h

--- foo.h.orig	2008-03-27 19:13:43.000000000 +0900
+++ foo.h	2008-03-27 19:14:00.000000000 +0900
@@ -1 +1,2 @@
 extern void foo(void);
+extern void bar(void);

foo.c

--- foo.c.orig	2008-03-27 19:13:50.000000000 +0900
+++ foo.c	2008-03-27 19:14:35.000000000 +0900
@@ -6,3 +6,9 @@
 {
 	printf("foo\n");
 }
+
+void
+bar()
+{
+	printtf("bar\n");
+}

これをマイナーバージョンを1つ上げ、libfoo.so.0.1としてコンパイルします。

$ gcc -shared -Wl,-soname,libfoo.so.0 -o libfoo.so.0.1 foo.c
$ ln -sf libfoo.so.0.1 libfoo.so.0
$ ln -sf libfoo.so.0 libfoo.so

先ほど作成したtest_fooを再コンパイルなしで実行してみましょう

$ ./test_foo
foo

後方互換は壊れていませんので、何のトラブルも無く実行可能です。

では、先ほどマイナーバージョンを変更した時に追加した
関数bar()を呼び出すようにアプリケーションを変更します。

--- foobar.c.orig       2008-03-27 19:26:47.000000000 +0900
+++ foobar.c    2008-03-27 19:26:59.000000000 +0900
@@ -3,4 +3,5 @@
 main(void)
 {
        foo();
+       bar();
 }

コンパイルして実行してみましょう。

$ gcc -Wl,-rpath,. -L. -lfoo -o test_foo test_foo.c
$ ./test_foo
foo
bar

こちらも当然ですが、何のトラブルも無く実行可能です。

ではシンボリックリンクであるlibfoo.so.0の実体を
新しいlibfoo.so.0.1から古いlibfoo.so.0.0に巻き戻すとどうなるでしょうか?

$ ls -l libfoo.so.0
lrwxr-xr-x  1 tnozaki  tnozaki  13 Mar 27 19:31 libfoo.so.0 -> libfoo.so.0.1
$ rm -f libfoo.so.0
$ ln -sf libfoo.so.0.0 libfoo.so.0
$ ls -l libfoo.so.0
lrwxr-xr-x  1 tnozaki  tnozaki  13 Mar 27 19:37 libfoo.so.0 -> libfoo.so.0.0

先ほど作成したtest_fooを再コンパイルなしで実行してみましょう

$ ./test_foo
foo
./test_foo: Undefined PLT symbol "bar" (symnum = 15)

barシンボルを解決できず、実行時エラーになってしまいました。
これが「前方互換性がない」状態という訳です。

他にも、構造体の最後に新規にメンバが追加された場合なんかも
後方互換はOKでも前方互換は失われます、例えば このエントリ
struct lconvの変更がこのケースに該当します *2

通常UNIX系OSではライブラリのバージョン管理は

として扱いますが、メジャーとマイナーの使い分けは

です、ライブラリ開発者の皆様におかれましては
その日の機嫌や語呂合わせなんかで適当に変えたり変えなかったり
しないように切にお願いする次第です。

後方互換性が壊れた場合には、新旧のメジャーを持つライブラリは残しておく必要があります *3

しかし前方互換性に関しては最新のマイナーを持つものがありさえすればノートラブルです
古いものは消しちゃっても構いません *4、しかし

ということだけは記憶の片隅に置いといてください。
#でないと前方互換性の問題でトラブる可能性が。

ってもFreeBSDなんかだとobjformatをELF化した時、SysV系の真似?して
マイナーを廃止してしまい新旧の区別が判らんようですが *6

ここまで書いてsource-changesに tsutsuiさんの解説発見、わーいダブった。


*1:外部公開が目的ではなく実装内部でのみ使う関数(libcなどでは_をprefixに持つ)
を追加した場合はminor crunkは不要です。

*2:こちらも外部公開せず内部でのみ使う構造体の場合は不要でしょう、私も
NetBSDのlibcの struct _RuneLocaleにメンバ追加した時にはminor crunkはしてません。

*3:ですのでNetBSDの場合、古いメジャーの共有ライブラリをsrc/distrib/sets/lists/base/mi他で obsoleteにマークしてはいけません。
*4:NetBSDでは古いマイナーはmark as obsoletedとしてpostinstall(8) fix obsolete時に消しちゃいます。
*5:pkgsrcのbinary distributionは@pkgdep、@blddepと2つの依存関係をチェックしてるので
問題なし、厳密にはlibc他のバージョンも見る必要があるけど
そこはuname -rのバージョンで代用してるはずです。

*6:NetBSDもPAM(8)すなわち/usr/lib/security/*はminor持ってないですが
あれはpluginだから必ずmajor crunkする必要があるから。
ま、同じpluginでもCitrusすなわち/usr/lib/i18n/*はminorまで持ってますが
あれはABI_VERSIONとかfallback functionで、major crunkしなくて済むように工夫してるからね。



[ホームへ] [ページトップへ]