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

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

2007/1/21(Sun)

ウィークシンボルって何ですか?

@ 2008/10/09 追記

Linux及び多くのSysV実装(Solaris, AIX etc)では動的リンクの場合、weakをstrongとして扱います。
ですのでサンプルはうまく動かないでしょう。
こちらをご一読ください。

@

さて今回はウィークシンボルのお話。

前回の最後で共有ライブラリlibconflict1.so.0とlibconflict2.so.0で
barとbuzというシンボルが重複する場合

使いたいような場合は、リンク順では解決できないという話をしました。
この場合ウィークシンボルを使えば解決できます。

では実際に、前回のconflict1.cのbuzをウィークシンボルにしてみましょう。
一度gas(1)形式のアセンブラソースに落とす必要があります。
これにはgcc(1)の-Sスイッチを使います。

$ gcc -S -o conflict1.s conflict1.c

では出力された conflict1.sの中身を見てみましょう。

	.file	"conflict1.c"
	.section	.rodata
.LC0:
	.string	"i'm conflict1::bar!."
	.text
.globl bar
	.type	bar, @function
bar:
	pushl	%ebp
(中略)
	.section	.rodata
.LC1:
	.string	"i'm conflict1::buz!."
	.text
.globl buz
	.type	buz, @function
buz:
	pushl	%ebp
(以下略)

このうち

.globl bar
.globl buz

としている部分がbarおよびbuzをグローバルシンボルにするという指令です。

ではbuzをウィークシンボルに書き換えます、やることは

です。

--- conflict1.s.orig	2007-01-21 03:26:03.000000000 +0900
+++ conflict1.s	2007-01-21 03:34:19.000000000 +0900
@@ -20,9 +20,10 @@
 .LC1:
 	.string	"i'm conflict1::buz!."
 	.text
-.globl buz
+.weak buz
+	buz = _buz
 	.type	buz, @function
-buz:
+_buz:
 	pushl	%ebp
 	movl	%esp, %ebp
 	subl	$8, %esp

次に書換後の conflict1.sをコンパイルしてみましょう。

$ gcc -shared -Wl,-soname,libconflict1.so.0 -o libconflict1.so.0 conflict1.s
$ nm libconflict1.so.0 | grep b[au][rz]
00000534 t _buz
0000051c T bar
00000534 W buz

buzの型が"T"から"W"に変わっています。
名前を_buzに変更したラベルはローカルシンボルとなっています。

$ ln -sf libconflict1.so.0 libconflict1.so
$ gcc -Wl,-rpath=. -L . -lconflict1 -lconflict2 -o test_conflict test_confict.c
$ nm test_conflict | grep b[au][rz]
         U bar
         U buz
$ ./test_conflict
i'm conflict1::bar!
i'm conflict2::buz!

barは変わらずlibconflict1.so.0が呼ばれますが、ウィークシンボルにした
libconflict1.so.0のbuzは呼ばれずlibconfict2.so.0が呼ばれました、成功です。

ではlibconflict2からbuzを消した場合どうなるでしょうか。

conflict2.c
--------------------
#include <stdio.h>
void bar(void) {
	puts("i'm conflict2::bar!.");
}
--------------------
$ gcc -shared -Wl,-soname,libconflict2.so.0 -o libconflict2.so.0 conflict2.c
$ nm libconflict2.so.0  | grep b[au][rz]                                       
000004ec T bar
$ ./test_conflict
i'm conflict1::bar!
i'm conflict1::buz!

ローカルシンボルに変更したlibconflict1.so.0の_buzが呼ばれています。

以上でお判りの通り、リンクローダはリンク順が上位であっても
ウィークシンボルであればシンボル解決を一旦留保します。
下位のライブラリにあればそちらを使いますが、解決できなければ
ウィークシンボルは参照先を呼びます。
いいかげんに書いた フローチャート

ウィークシンボルは何につかうのですか?

実際にこのウィークシンボルはどのように使われているのでしょうか?
それはlibcの内部でも呼び出すけど実装そのものは別ライブラリに追い出したいという場合です。

NetBSDやOpenBSDの場合、pthread(3)の関数をlibcからlibpthreadに移す為に使われてたりします。
NetBSDの pthread.hには

#define pthread_mutex_lock	__libc_mutex_lock
...

の定義がありますのでlibcをnm(1)した結果を__libc_mutex_でgrep(1)してみると

$ nm /lib/libc.so.12.150 | grep __libc_mutex_
000220a4 T __libc_mutex_catchall_stub
...
000220a4 W __libc_mutex_lock

ウィークシンボル化され、参照先はスタブ(__libc_mutext_catchall_stub)になっています。

スタブの中身を見てみましょう、 thread-stub.cです。

 61 extern int __isthreaded;
 62
 63 #define DIE()   (void)kill(getpid(), SIGABRT)
 64
 65 #define CHECK_NOT_THREADED_ALWAYS()     \
 66 do {                                    \
 67         if (__isthreaded)               \
 68                 DIE();                  \
 69 } while (/*CONSTCOND*/0)
 70
 71 #if 1
 72 #define CHECK_NOT_THREADED()    CHECK_NOT_THREADED_ALWAYS()
 73 #else
 74 #define CHECK_NOT_THREADED()    /* nothing */
 75 #endif
...

 82 __weak_alias(__libc_mutex_init,__libc_mutex_init_stub)
...

 95 int
 96 __libc_mutex_init_stub(mutex_t *m, const mutexattr_t *a)
 97 {
 98 	/* LINTED deliberate lack of effect */
 99 	(void)m;
100 	/* LINTED deliberate lack of effect */
101 	(void)a;
102
103 	CHECK_NOT_THREADED();
104
105 	return (0);
106 }
...


中身は__isthreaded変数をチェックして真なら自らをkill(2)するだけです。

82行目の__weak_aliasマクロは上の演習でアセンブラに落として行った
.globlから.weak指令への書き換えをCのインラインアセンブラでやるマクロです。
sys/cdefs_elf.hに定義されています。

#define __weak_alias(alias,sym)                                         \
    __asm(".weak " _C_LABEL_STRING(#alias) "\n"                 \
            _C_LABEL_STRING(#alias) " = " _C_LABEL_STRING(#sym));

OpenBSDも同名のマクロが sys/cdefs.hに定義されてます、glibcの場合は libc-symbol.hあたりにいろいろあるようです。

世の中のほとんどのアプリケーションはマルチスレッド対応は必要のないものです。
使用頻度の低いpthread(3)の関数は別ライブラリにしてしまえばlibcのサイズは小さくなります。
しかしここでひとつ問題があります、libcの一部の関数はpthread(3)に依存しているのです。

例えばfgetc(3)などのファイル操作関数は、内部でflockfile(3)を呼び出します。
fgetc.c

50 int
51 fgetc(fp)
52 	FILE *fp;
53 {
54 	int r;
55
56 	_DIAGASSERT(fp != NULL);
57
58 	FLOCKFILE(fp);
59 	r = __sgetc(fp);
...

これはマルチスレッドの場合、他のスレッドとの競合状態(レースコンディション)を防ぐ為です。
競合状態って? という人は 読んでください。
flockfile(3)の実装はpthread_mutex_lock(=__libc_mutex_lock)を使って行われています。
flockfile.c

 67 void
 68 flockfile(FILE *fp)
 69 {
 70
 71 	__flockfile_internal(fp, 0);
...

105 void
106 __flockfile_internal(FILE *fp, int internal)
107 {
108  
109 	if (__isthreaded == 0)
110 		return;
111         
112 	mutex_lock(&_LOCK(fp));
...

__isthreadedが偽の場合もせず終了しますが、mutex_lock(=__libc_mutex_lock)を
呼んでることに変わりありません、単純にlibcからpthread(3)を追い出すと
libcのコンパイル時にリンカは__libc_mutex_lockのシンボルを解決できずリンクに失敗します(*1)。
ですのでリンカをだます為、__libc_mutex_lockをウィークシンボルにしてlibc自身に定義しておく必要があるのです。

ここまでの話をもっと詳しく知りたい(あるいはお前の話じゃ判んねーYO!)という方は

あたりの本を読んでください。

次回は本題に戻り、どのようにしてlibcは後方互換性を保つのか?(その2)の予定。


(*1)
実は最近のGNU ld(1)だとエラーにならず、"U"(未定義グローバルシンボル)になるみたい。
これ困ったことに、例えば_DIAGASSERTというマクロを打ち間違えて
_DIAGASSSERTとかミススペルをしてもリンク時に発覚せず、実行時に

./test: Undefined PLT symbol "_DIAGASSSERT" (symnum = 10)

とエラーになるので、時々libcが壊れてるのに気付かずインストールしてハマります...


(補足)
シンボルの競合の解決にはもっと複雑なケースが考えられます。
場合によっては同一アプリケーションの中でlibconflict1.so.0のbarと
libconflict2.so.0のbarを使い分けたいケースもあるでしょう。
これは当然ウィークシンボルでは不可能です。

そんな場合には、SunOS由来のdlopen(3), dlsym(3)が使えるかもしれません。
Cygwinの場合ならWin32APIのLoadLibrary, GetProcAddressでしょうか。
これらについては本題とはあまり関係がないので別の機会に譲ります。


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