同名のシンボルを持つ同名のライブラリーをリンクしたライブラリーを 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 で同名の、しかし別の実装を持つシンボルを参照できる
  • 同名のシンボルを使わないにこしたことはないし、実装が違うならライブラリーの名前は変えるべき