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が書き換えられているので本来呼びたい関数に直接ジャンプできるという仕組みになっている
LinuxとFreeBSDの挙動の違い
一度解決したリンク先がdlcloseによって参照先がなくなった場合の挙動にlinuxとBSDでは微妙に違うところがある。
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ではセグフォで落ちる