同名のシンボルを持つ同名のライブラリーをリンクしたライブラリーを dlopen したときの名前解決
昨日のエントリーのあとで、では dlopen/dlclose で明示的にリンクしたらどうなるか? という疑問がわいたので引き続いて調査。 (リンカー・ローダーのソースを読まずに動作から確認している…というあたり、弱いなあ)
main というアプリケーションが one/libone.so と two/libtow.so をロードし、それぞれからシンボル hello_one と hello_two を検索して呼び出す。 また one/libone.so は one/libhello.so に暗黙リンクしており、この中の hello() を、 two/libtwo.so は two/libhello.so に暗黙リンクしており、この中の hello() を呼び出す。
one/libhello.so の hello() は "HELLO WORLD" を出力し、 two/libhello.so の hello() は "hello world" を出力する。
というような状況を実現するためにこんなソースコードを起こす。
/* main.c */ #include <dlfcn.h> int main() { void *const dlone = dlopen("one/libone.so", RTLD_LAZY); ((void (*)())dlsym(dlone, "hello_one")) (); dlclose(dlone); void *const dltwo = dlopen("two/libtwo.so", RTLD_LAZY); ((void (*)())dlsym(dltwo, "hello_two")) (); dlclose(dltwo); return 0; }
/* one/one.c */ void hello(); void hello_one() { hello(); }
/* one/hello.c */ int puts(const char *str); void hello() { puts("HELLO WORLD"); }
/* two/two.c */ void hello(); void hello_two() { hello(); }
/* two/hello.c */ int puts(const char *str); void hello() { puts("hello world"); }
これらをビルドする Makefile はこんな感じで準備した。
# Makefile CFLAGS=-W -Wall -Werror -fpic -fomit-frame-pointer -fstrict-aliasing LDFLAGS=-ldl all: one/libone.so two/libtwo.so main clean: $(RM) *~ *.o *.so main $(MAKE) clean -C one $(MAKE) clean -C two one/libone.so: $(MAKE) -C one two/libtwo.so: $(MAKE) -C two
# one/Makefile CFLAGS=-W -Wall -Werror -fpic -fomit-frame-pointer -fstrict-aliasing LDFLAGS=-L. -Wl,-rpath=one all: libhello.so libone.so clean: $(RM) *~ *.o *.so libhello.so: hello.o $(CC) -shared -o $@ $< $(LDFLAGS) libone.so: one.o libhello.so $(CC) -shared -o $@ $< $(LDFLAGS) -lhello
# two/Makefile FLAGS=-W -Wall -Werror -fpic -fomit-frame-pointer -fstrict-aliasing LDFLAGS=-L. -Wl,-rpath=two all: libhello.so libtwo.so clean: $(RM) *~ *.o *.so libhello.so: hello.o $(CC) -shared -o $@ $< $(LDFLAGS) libtwo.so: two.o libhello.so $(CC) -shared -o $@ $< $(LDFLAGS) -lhello
この設定で make して ./main と実行すると…
$ make $ ./main HELLO WORLD hello world
となる。 libone.so と libtwo.so はそれぞれ同名のライブラリー libhello.so にリンクしているけれど、 ライブラリー検索パスを one と two に設定しているため、混じることなく期待通りのライブラリーがロードされる。
実用性は低い
けれどこれはやはり紙一重である。
main では libtwo.so を dlopen する前に libone.so を dlclose していたけれど、この dlclose の位置をこんなふうに変えると、…
int main() { void *const dlone = dlopen("one/libone.so", RTLD_LAZY); void *const dltwo = dlopen("two/libtwo.so", RTLD_LAZY); ((void (*)())dlsym(dlone, "hello_one")) (); ((void (*)())dlsym(dltwo, "hello_two")) (); dlclose(dlone); dlclose(dltwo); return 0; }
hello_one と hello_two のいずれも one/libhello.so の hello() を呼び出すようになってしまう。
$ make clean all && ./main HELLO WORLD HELLO WORLD
つまり libhello.so のライブラリーの名前が同じだったため libtwo.so を dlopen したときに、すでにロードされている one/libhello.so が使われてしまったということである。
異なる機能を実装しているにもかかわらず同じ名前をつけると、罰があたるという話だ。
では libhello に別名をつけたなら?
two/Makefile 内の libhello.so をすべて libhello2.so に書き換え、またリンクするライブラリーを -lhello2 に変更する。
# two/Makefile FLAGS=-W -Wall -Werror -fpic -fomit-frame-pointer -fstrict-aliasing LDFLAGS=-L. -Wl,-rpath=two all: libhello2.so libtwo.so clean: $(RM) *~ *.o *.so libhello2.so: hello.o $(CC) -shared -o $@ $< $(LDFLAGS) libtwo.so: two.o libhello2.so $(CC) -shared -o $@ $< $(LDFLAGS) -lhello2
すると dlopen/dlclose の位置によらず
$ make clean all && ./main HELLO WORLD hello world
の結果が得られるようになる。
まとめると
- .so が dlopen/dlclose で分離されていれば、異なる .so で同名の、しかし別の実装を持つシンボルを参照できる
- 同名のシンボルを使わないにこしたことはないし、実装が違うならライブラリーの名前は変えるべき