同名のシンボルを持つ複数のライブラリーをリンクしたときの名前解決

GOT やリンカー・ローダーの仕組みを考えれば、そうなるかな、と納得はできるんだけれど、たいそうショックを受けたことがら。

次のような三つのソースを用意する。

/* a.c */
int puts(const char *str);
void foo() { puts("hello"); }
void bar() { foo(); }

/* A.c */
int puts(const char *str);
void foo() { puts("HELLO"); }

/* main.c */
void bar();
int main() { bar(); return 0; }

そして a.c と A.c から共有ライブラリーを作成する。

$ gcc -fpic -shared -o liba.so a.c
$ gcc -fpic -shared -o libA.so A.c

main.c をコンパイルして A と a にリンクして実行してみる。

$ gcc -o A_a main.c -L. -lA -la -Wl,-rpath=.
$ ./A_a
HELLO

main で呼び出す bar を定義したのは a.c。 bar は foo を呼び出していて、同じソース内に foo を定義してあるのに、呼び出されたのは A.c の foo になっている。

ライブラリーのリンク順序を a、それから A の順にすると a.c の foo が呼ばれる。

$ gcc -o a_A main.c -L. -la -lA -Wl,-rpath=.
$ ./a_A
hello

そういうものだったんだ…。 ライブラリーとしてリンクしたときに、できる限りの名前解決はされるものだとおもいこんでいた。 外部にエクスポートされるシンボルの場合、ライブラリー内の定義の有無にかかわらず GOT 経由での呼び出しになるのね。

念のため -fpic -O3 -fomit-frame-pointer での a.c アセンブルリスト

アセンブルリストを出力してみる。

$ gcc -fpic -O3 -fomit-frame-pointer -S a.c

得られた結果の a.s を眺めてみると、 bar の中の foo 呼び出しは jmp foo@PLT、つまり PLT (Procedure Linkage Table) 経由でのジャンプになっている。 同じコンパイル単位に関数が定義されていても、それは無視するんだな…。

        .file   "a.c"
        .section        .rodata.str1.1,"aMS",@progbits,1
.LC0:
        .string "hello"
        .text
        .p2align 4,,15
.globl foo
        .type   foo, @function
foo:
.LFB0:
        .cfi_startproc
        leaq    .LC0(%rip), %rdi
        jmp     puts@PLT
        .cfi_endproc
.LFE0:
        .size   foo, .-foo
        .p2align 4,,15
.globl bar
        .type   bar, @function
bar:
.LFB1:
        .cfi_startproc
        xorl    %eax, %eax
        jmp     foo@PLT
        .cfi_endproc
.LFE1:
        .size   bar, .-bar
        .ident  "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
        .section        .note.GNU-stack,"",@progbits