The Man Who Fell From The Wrong Side Of The Sky:2007年1月23日分

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

2007/1/23(Tue)

どのようにしてlibcは後方互換を保つのか(その2)

その1では、C90とC90:AMD1、C99のそれぞれのバージョン間で後方互換を保つということは

ということを説明しました。
今回はバイナリレベルではlibcはどのようにして名前衝突を避けるか、を説明します。

libcはld(1)によって暗黙的にリンクされます、この場合リンク順は一番最後になります。
(libcをリンクしたくない場合、例えばkernelのビルドなどの場合は-ffreestandingスイッチが必要です)

libcのリンク順が一番最後である場合、名前衝突は発生しません。
前に説明した通り、シンボルが重複する場合は実行ファイルに静的リンクされてる同名シンボル、
あるいは動的リンクされた共有ライブラリの内で、リンク順が最も先のものが使われるからです。

復習がてらサンプルを作って確認してみましょう。
C90:AMD1のmbrtowc(3)と重複するシンボルを持つ共有ライブラリをC90でコンパイルします。

user_mbrtowc.c
----------------------------------------
#include <stdio.h>
void mbrtowc()
{
	puts("hello, world.");
}
----------------------------------------
$ gcc -std=c89 -shared -Wl,-soname,libmbrtowc.so.0 -o libmbrtowc.so.0 user_mbrtowc.c
$ nm libmbrtowc.so.0 | grep mbrtowc
000004ec T mbrtowc

次はこのユーザ定義mbrtowcを呼び出すアプリケーションを用意します。

test_mbrtowc.c
----------------------------------------
extern void mbrtowc(void);
int main(void)
{
	mbrtowc();
	return 0;
}
----------------------------------------
$ ln -sf libmbrtowc.so.0 libmbrtowc.so
$ gcc -std=c89 -Wl,-rpath=. -L. -lmbrtowc -o test_mbrtowc test_mbrtowc.c
$ nm test_mbrtowc | grep mbrtowc
	U mbrtowc

それでは実行してみましょう。

$ ./test_mbrtowc
hello, world.

libmbrtowc.so.0のmbrtowcが呼ばれています。
つまりlibcの後方互換性はちゃんと守られているということです。

ではlibcのリンク順を明示的に指定した場合はどうでしょうか?

$ gcc -g -std=c89 -Wl,-rpath=. -L/usr/lib -lc -L. -lmbrtowc -o test_mbrtowc test_mbrtowc.c
$ ldd test_mbrtowc
test_mbrtowc:
	-lc.12 => /usr/lib/libc.so.12
	-lmbrtowc.0 => ./libmbrtowc.so.0
$ gdb ./test_mbrtowc
GNU gdb 6.5
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386--netbsdelf"...
(gdb)
Starting program: /home/tnoazki/tmp/test_mbrtowc

Program received signal SIGSEGV, Segmentation fault.
0xbbbbc65a in mbrtowc () from /usr/lib/libc.so.12
(gdb)

libcのmbrtowc(3)が呼ばれ、Segmentation faultが発生しました。
つまりlibcのバイナリレベルでの後方互換を保つ為には
libcのリンク順を明示的に指定してはならないということです。
普通のアプリケーション開発者はそれさえ守っていればまず問題はおきないでしょう。

しかしlibcの開発者はまだまだ注意しなければならないことがあります。
C90の関数からC90:AMD1やC99の関数を呼び出すなんていうことは良くあることなのですが
この場合、ユーザ定義のシンボルにより上書きされる可能性を考慮しなければなりません。

例としてNetBSDとOpenBSDの場合を説明します。
C90の関数であるprintf(実装の本体はvfprintf)は、フォーマット文字列の解析に内部的に
C90:AMD1の関数であるmbrtowc(3)を呼び出しています。
vfprint.c

197 int
198 __vfprintf_unlocked(fp, fmt0, ap)
...

345 		while ((n = mbrtowc(&wc, fmt, MB_CUR_MAX, &ps)) > 0) {
346 			fmt += n;
347 			if (wc == '%') {
...

何も対策をしないままだと、これはユーザ定義のmbrtowcに置き換えられてしまいます。
先ほどのユーザ定義mbrtowcを修正して、puts(3)でなくprintf(3)に変更してみましょう。

----------------------------------------
--- user_mbrtowc.c.orig 2007-01-22 01:08:26.000000000 +0900
+++ user_mbrtowc.c      2007-01-22 01:46:42.000000000 +0900
@@ -1,6 +1,6 @@
 #include <stdio.h>
 void mbrtowc()
 {
-       puts("hello, world.");
+       printf("hello, world.");
 }
----------------------------------------
$ gcc -std=c89 -shared -Wl,-soname,libmbrtowc.so.0 -o libmbrtowc.so.0 user_mbrtowc.c

ではリンク順を修正して、test_mbrtowcを実行します。

$ rm -f test_mbrtowc
$ gcc -g -std=c89 -Wl,-rpath=. -L. -lmbrtowc -o test_mbrtowc test_mbrtowc.c
$ gdb ./test_mbrtowc
GNU gdb 6.5
Copyright (C) 2006 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386--netbsdelf"...
(gdb) run
Starting program: /home/tnozaki/tmp/test_mbrtowc

Program received signal SIGSEGV, Segmentation fault.
0xbbbb8314 in __vfprintf_unlocked () from /usr/lib/libc.so.12
(gdb)

Segmentation faultが発生しました。
これは__vfprintf_unlocked()内で、libcのmbrtowc(3)ではなくユーザ定義シンボルが呼び出されたのが原因です。
明らかにバグです(← つかさっさと直せ、俺)。

それでは正しい対策がされている例を見てみましょう。
C90の関数であるscanf(実装はvfscanf)は、入力された文字列を数値に変換する為に
内部的にC99の関数であるstrtoimax(3)を呼び出しています。
vfscanf.c

136 int
137 __svfscanf_unlocked(fp, fmt0, ap)
...

152 	uintmax_t (*ccfn) __P((const char *, char **, int));
...

251 		case 'd':
252 			c = CT_INT;
253 			ccfn = (uintmax_t (*) __P((const char *, char **, int)))strtoimax;
...

しかしこちらの場合はユーザ定義のstrtoimaxで置き換えても正しい動作をします。
この違いは何故でしょうか?

NetBSDとOpenBSDの場合 namespace.hというものを使って名前空間の保護を行っています。

65 #define strtoimax	_strtoimax
66 #define strtold	_strtold
67 #define strtoll	_strtoll
...

プリプロセッサでstrtoimaxを_strtoimaxに置換するだけの非常にシンプルなものです。

慣例上アンダースコアで始まるシンボル名はlibcが内部実装用に使うものとして予約され、
アプリケーション開発者は使ってはならないことになっています。
#あくまで慣例上なのでexternすれば簡単に呼べてしまいますが...
#つか呼ばないで下さい、おながいしまふ。

vfscanf.cは44行目でnamespace.hをインクルードしています

42 #endif /* LIBC_SCCS and not lint */
43
44 #include "namespace.h"
45
...

$ cd /usr/src/lib/libc/stdio
$ gcc -I../include -E vfscanf.c | grep strtoimax
intmax_t _strtoimax(const char * __restrict,
   ccfn = (uintmax_t (*) (const char *, char **, int))_strtoimax;
   ccfn = (uintmax_t (*) (const char *, char **, int))_strtoimax;
...

よってnamespace.hをインクルードすることで、直接strtoimax(3)を呼ぶのではなく
libcの内部実装用の_strtoimax呼ぶように自動的に書き換えられているわけです。
これならばユーザ定義のstrtoimaxによってシンボルが上書きされたとしても影響は受けません。

内部実装用の_strtoimaxは strtoimax.c

59 intmax_t
60 _strtoimax(nptr, endptr, base)
61 	const char *nptr;
62 	char **endptr;
63 	int base;
64 {
...

に定義されています。

そしてもうひとつの工夫です、strtoimax(外部向け)はこの_strtoimax(内部向け)を
_strtoimax.cにあるように

41 #include <inttypes.h>
42 intmax_t	_strtoimax(const char *, char **, int);
43
44 intmax_t
45 strtoimax(const char *nptr, char **endptr, int base)
46 {
47
48	return _strtoimax(nptr, endptr, base);
...

と呼び出すだけの関数として実装すると、関数呼び出しのコスト(分岐命令、スタックの積み替え etc)が無駄です。
このオーバーヘッドを回避するいい方法はないのでしょうか。

ここで 前回解説したウィークシンボルの応用です。
先ほどの strtoimax.cをもう一度見てみましょう。

BR
49 #ifdef __weak_alias
50 __weak_alias(strtoimax, _strtoimax)
51 #endif
...

strtoimaxをウィークシンボル化として定義し、参照先は_strtoimaxにしてあります。
nm(1)の出力を見ると、strtoimaxは_strtoimaxと同じアドレスになっています。

$ nm /usr/lib/libc.so.12.150 | grep strtoimax
000717d4 T _strtoimax
000717d4 W strtoimax

これならばリンクローダがウィークシンボルを解決する時のオーバヘッドだけで済み、
strtoimaxが繰り返し呼ばれる場合も無駄なコストはかからない、という訳です。

glibcも同じアイデアです、差は内部実装の名前が__strtol_internalとなっているくらいだと思います。
stdlib.h

289 #ifndef __strtol_internal_defined
290 extern long int __strtol_internal (__const char *__restrict __nptr,
291 				    char **__restrict __endptr,
292 				    int __base, int __group)
293      __THROW __nonnull ((1)) __wur;
294 # define __strtol_internal_defined	1
295 #endif
...

vfscanf.c

1544               if (number_signed)
1545                 num.l = __strtol_internal (wp, &tw, base, flags & GROUP);
1546               else
...

次回(最終回)はlibcのsoname変更問題、NetBSDの__RENAMEマクロや
SolarisやglibcのELF symbol versioningについてを予定しています。


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