totofugaのブログ

ネットワークとかc言語、perlの話。

LinuxとFreeBSDの共有ライブラリについて

共有ライブラリについて

Linuxでは共有ライブラリとして.soファイルが使用される。 .soは実行時にリンクする以外でも、実行中にdlopenを使用して読み込むことができる。

実行中の共有ライブラリ読み込みの仕組み

共有ライブラリは.textセクションを共有する為、 静的ライブラリのように外部ライブラリのアドレスを relセクションを作ってリンク時に書き換えということができない。

.textセクションの関数呼び出し先を共有しない書き込み可能な.got(global offset table)という領域に分けて、 そのポインタを参照することにより対処している。

共有ライブラリの遅延ロード

dlopenにはRTLD_LEZYというフラグがある。 RTLD_LEZYは関数の実行時まで参照先のシンボルを解決しないというフラグだが、 それを実現するために.pltという領域を使用している。

.pltには

   0x00007fffeb62f568 <+0>:     jmpq   *0x2022ea(%rip)        # 0x7fffeb831858
   0x00007fffeb62f56e <+6>:     pushq  $0x22
   0x00007fffeb62f573 <+11>:    jmpq   0x7fffeb62f338

のような関数ごとにjmpを2回するようなコードがある。 0x7fffeb831858はGOTのアドレスを指していて、この場合一度も実行していないと0x00007fffeb62f56e(pushdのところ)のアドレスが入っている。

したがって1回目の実行では最初のjmpは何もしないで次の行に進みシンボルをを解決したい関数のID(0x22)を入れて、シンボル解決するコードにジャンプする。 シンボル解決コードでは0x7fffeb831858の中を本来jmpしたいアドレスに書き換えてから、本来jmpしたいアドレスにjmpする。 2回目の呼び出しでは、0x7fffeb831858のGOTが書き換えられているので本来呼びたい関数に直接ジャンプできるという仕組みになっている

f:id:totofuga:20170622105542p:plain

f:id:totofuga:20170622105623p:plain

LinuxFreeBSDの挙動の違い

一度解決したリンク先がdlcloseによって参照先がなくなった場合の挙動にlinuxBSDでは微妙に違うところがある。

first.so

#include <stdio.h>
void do_action() {  printf("action for first_test\n"); }

second.so

#include <stdio.h>
void do_action() { printf("action for second_test\n"); }

test.so

void do_test() { do_action(); }

main.c

void main(){
    void (*do_test)();
    void *first  = dlopen("./libfirst.so", RTLD_LAZY | RTLD_GLOBAL);
    void *h = dlopen("./libtest.so", RTLD_LAZY | RTLD_LOCAL);
    *(void **)(&do_test) = dlsym(h, "do_test");
    (*do_test)();
    dlclose(first);
    void *second = dlopen("./libsecond.so", RTLD_LAZY | RTLD_GLOBAL);
    (*do_test)();
}

main => do_test => do_actionの順番に呼ばれているプログラムである。

上記実行するとFreeBSDではセグフォで落ちる どうなっているのかというとdo_testを読んだ時点で、do_actionの.gotがfirst.soのシンボルとして解決され、 その後, first.soをdlcloseにてプロセスから削除している。

次に呼ばれたdo_testでは削除されたアドレスにシンボル解決されたdo_actionを呼ぶので落ちるのである。

Linuxの出力は

action for first_test
action for first_test

とfirstが二回出力される。

Linuxではシンボル解決された段階で参照先(first.so)の参照カウントが上がるようで、 dl_closeしても、プロセス空間から削除されないようだ。

まとめ

dl_openで読み込む対象を動的に切り替えるのはうまくいかない。 Linuxではエラーにはならないが最初に読み込まれたもののみが実行される。 FreeBSDではセグフォで落ちる