1 組込みエンジニアのための Linux 入門 ダイナミックリンク編 (2) 株式会社アプリックス 小林哲之
2 このスライドの対象とする方 今までずっと組込み機器のプロジェ クトに携わってきて最近は OS に Linux を使っている方々
3 このスライドの目的 Linux で使用されているダイナミック リンクの仕組みを理解し現在のプロ ジェクトに役立てる。 – 仕組みを知らなくてもプログラムは動 くが、トラブルに対処したり性能を引 き出すためには仕組みの理解が重要。
4 今日のお題 ダイナミックリンクライブラリの関数呼 び出しの実際 – 裏でリンカ、ローダはどんなことをしてくれ ているのか? ダイナミックリンクライブラリを作って 動かしてみる –PIC, シンボルの visibility 残念ながら prelink は次回以降で。
5 ダイナミックリンクライブラ リの 関数呼び出しの実際
6 ダイナミックリンクライブラリ main() program libraries リンク時に配置されるアドレスが決まっている - 1 プロセスにひとつしかないので固定アドレスでよい。 リンク時に配置されるアドレスが決まっていない。 - 1 プロセスに複数のライブラリが使われる。 - ライブラリ同士が重ならないように配置しなければ ならない。 ロード時に配置されるアドレスが決まる。
7 実際のコードを見てみる main hello2 puts programlibc hello1 #include void hello2(char *msg) { puts(msg);} void hello1(char *msg) { hello2(msg);} int main() { hello1("Hello, world"); return 0;}
8 ダイナミックリンクライブラリの 関数呼び出し hello1hello2 puts programlibc ? リンク時にはアドレス が決まっていない。 どうやって呼び出す? main
9 ダイナミックリンクライブラリの 関数呼び出し hello1hello2 puts programlibc GOT リンカが生成 ジャンプテーブル経由でライブラリの関数を呼び出す。 ローダが 値を入れる PLT: Procedure Linkage Table GOT: Global Offset Table main
10 hello1 から hello2 の呼び出し (i386) : :55 push %ebp :89 e5 mov %esp,%ebp :83 ec 08 sub $0x8,%esp a:8b mov 0x8(%ebp),%eax d: mov %eax,(%esp) :e8 2f ff ff ff call :c9 leave :c3 ret : :55 push %ebp :89 e5 mov %esp,%ebp a:83 ec 08 sub $0x8,%esp d:8b mov 0x8(%ebp),%eax : mov %eax,(%esp) :e8 dc ff ff ff call :c9 leave :c3 ret
: :ff jmp *0x a: push $0x f:e9 c0 ff ff ff jmp hello2 から puts の呼び出し (i386) : :e8 2f ff ff ff call : libc の puts.got 間接ジャンプ
12 hello1 から hello2 の呼び出し (arm) c : 837c:e52de004 strlr, [sp, #-4]! 8380:e24dd004 subsp, sp, #4; 0x4 8384:ebffffc4 bl829c 8388:e28dd004 addsp, sp, #4; 0x4 838c:e8bd8000 ldmiasp!, {pc} : 8390:e52de004 strlr, [sp, #-4]! 8394:e24dd004 subsp, sp, #4; 0x4 8398:ebfffff7 bl837c 839c:e28dd004 addsp, sp, #4; 0x4 83a0:e8bd8000 ldmiasp!, {pc}
c : :ebffffc4 bl829c 829c:e28fc600 addip, pc, #0; 0x0 82a0:e28cca08 addip, ip, #32768; 0x a4:e5bcf2bc ldrpc, [ip, #700]! hello2 から puts の呼び出し (arm) 10560: libc の puts.got ip = (0x829c + 8) + 0x = 0x10560 pc = *(ip)
14 Lazy binding ( 遅延バインディング ) ローダが GOT の値の設定するが、その方法に2 つの選択肢 – 起動時に全て設定 ( 環境変数 LD_BIND_NOW=1 としたときの動作 ) – 実行時に最初に使用された時に設定 (= lazy binding) ( デフォルトの動作 ) 関数の参照の初期値にはローダ内にある参照を解決するため の 関数 ( = __dl_runtime_resolve) がセットされている。 最初に使用したときにその関数が呼ばれて、 GOT に 解決されたアドレスが書き込まれる。 変数の参照は起動時に全て解決される。
: :ff jmp *0x a: push $0x f:e9 c0 ff ff ff jmp Lazy binding の実際 (i386) : :e8 2f ff ff ff call :.got 初期値として 0x a( = + 6) が入っている plt を経由してローダの __dl_runtime_resolve へ puts の参照を解決して ここを書き換え puts にジャンプ を表す識別子
: 8288:e52de004 strlr, [sp, #-4]! 828c:e59fe004 ldrlr, [pc, #4]; :e08fe00e addlr, pc, lr 8294:e5bef008 ldrpc, [lr, #8]! 8298:000082bc.word0x000082bc c : :ebffffc4 bl829c 829c:e28fc600 addip, pc, #0; 0x0 82a0:e28cca08 addip, ip, #32768; 0x a4:e5bcf2bc ldrpc, [ip, #700]! Lazy binding の実際 (arm) 1055c: 10560:.got ip = (0x829c + 8) + 0x = 0x10560 pc = *(ip) lr = 0x000082bc + (0x ) + 8 = 0x1055c pc = *(lr) ローダの __dl_runtime_resolve
17 ダイナミックリンクのためのサイ ズの増加 関数のエントリひとつごとに (arm) –plt スタブ 3 命令 12 バイト –GOT 1 エントリ 4 バイト – シンボルの文字列 平均 10 バイトくらい? – その他... ? ダイナミックリンクライブラリの利点に比べれば 細かいことだが、把握しておいたほうがいいかも
18 PLT, GOT をどうしてもどうしても 節約したい場合 ライブラリ関数へのポインタを取得して、 それ経由で呼び出せばよい。 #include int (*puts_addr)(const char*); void hello1(char* msg) { puts_addr(msg); } int main() { puts_addr = puts; hello1("hello, world"); return 0; } ただしこう書いても puts_addr に得られるアドレスは libc の puts でなくて なので意味が無い。
19 dlsym を利用してアドレスを得 る $ cat p2.c #include int (*puts_addr)(const char*); void hello1(char* msg) { puts_addr(msg); } int main() { puts_addr = dlsym(RTLD_DEFAULT, "puts"); hello1("hello, world"); return 0; } $ cc -D_GNU_SOURCE p2.c -ldl $ libc のように必ずロードされているとわかっているライブラリでは dlopen しなくても、 RTLD_DEFAULT という定義済みのハンドラが 使用できる。 (dlclose を心配しなくても済む。 )
20 自作ライブラリの場合なら 関数ポインタ ( のストラクチャ ) へのポイン タを返す関数を用意する。 int func_a() int func_b() int func_c() static int func_a() static int func_b() static int func_c() FUNCS* get_funcs() typedef struct { int (*func_a)(); int (*func_b)(); int (*func_c)(); } FUNCS; APIs single API
21 まとめ ダイナミックリンクライブラリの関数呼 び出しと プログラム内の関数呼び出しでは、 C の ソース上ではほとんど変わらないが、実 際の動作ではダイナミックリンクライブ ラリの呼び出しは複雑。 複雑な仕組みはリンカとローダが隠蔽し てくれている。
22 ダイナミックリンクライブラ リの 作って動かしてみる
23 ダイナミックリンクライブラリの 作り方 リンク時に –shared をつける。 [ 必須 ] コンパイル時に –fpic をつけて、 Position Independent Code にする。 [ 推奨 ] ( 後述 ) –-fpic と -fPIC の違いはドキュメント参照 ライブラリの名前は lib ~.so とする。 gcc4.0 以降の場合は 適切な visibility を指定する。 ( 後述 )
24 簡単な実例 $ make cc -fpic -c -o hello1.o hello1.c cc -fpic -c -o hello2.o hello2.c cc -shared -o libhello.so hello1.o hello2.o -lc $ file libhello.so libhello.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped $
25 ダイナミックリンクライブラリの インストールの方法 暫定的に使う場合 – 環境変数 LD_LIBRARY_PATH に設定する。 – コロン ( : ) で区切って複数のライブラリを指定できる。 恒久的なインストール –/lib または /usr/lib に置くか、 /etc/ld.so.conf に 登録されているディレクトリに置くか、ライブラリ の あるディレクトリを /etc/ld.so.conf に登録する。 – その後、ルート権限で /sbin/ldconfig コマンドを実行 する。 – ライブラリの情報が /etc/ld.so.cache にキャッシュさ れる ので、 LD_LIBRARY_PATH を使うより起動時間が短 縮できる。
26 PIC (Position Independent Code) 絶対アドレスによる参照の代わりに、プログラ ムカウンタからの相対アドレスを使用する。 ロードするアドレスが変わってもコードを書き 換える必要が無い。 –i386 ではプログラムカウンタ相対のアドレッシング モードがないので汚いコードになる。 PC を取得するために call 命令を使ってスタックに PC を書き 出させるなど。 – 組込み系で使用するプロセッサでも数命令多く必要 な場合がある。 コードサイズが少し大きくなり、その分だけ実 行時間も増える。
27 PIC/ 非 PIC のコードの比較 extern int gvar; static int svar; void setGvar(int var) { gvar = var; } void setSvar(int var) { svar = var; } グローバル変数、スタティック変数への書き込み
28 グローバル変数への書き込み (arm) setGvar: ldr r3,.L3.LPIC0: add r3, pc, r3 ldr r2,.L3+4 ldr r3, [r3, r2] str r0, [r3, #0] bx lr.L4:.align 2.L3:.word _GLOBAL_OFFSET_TABLE_ -(.LPIC0+8).word gvar(GOT) PIC setGvar: ldr r3,.L3 str r0, [r3, #0] bx lr.L4:.align 2.L3:.word gvar Non PIC 3 命令 + 1 ワード増加 ( 計 16 バイト ) 変数 gvar の絶対アドレス GLOBAL_OFFSET_TABLE gvar(GOT) gvar
29 static 変数への書き込み (arm) setSvar: ldr r3,.L7.LPIC1: add r3, pc, r3 ldr r2,.L7+4 str r0, [r3, r2] bx lr.L8:.align 2.L7:.word _GLOBAL_OFFSET_TABLE_ -(.LPIC1+8).word.LANCHOR0(GOTOFF) setSvar: ldr r3,.L7 str r0, [r3, #0] bx lr.L8:.align 2.L7:.word.LANCHOR0 Non PICPIC 2 命令 + 1 ワード増加 ( 計 12 バイト ) グローバル変数のときより 1 命令少ないのは GOT にオフセットでなく変数そのものが格納されるため static 変数 svar の絶対アドレス GLOBAL_OFFSET_TABLE gvar(GOT) gvar
30 グローバル関数の呼び出し (arm) call_gfunc: str lr, [sp, #-4]! sub sp, sp, #4 bl gfunc add sp, sp, #4 ldmfd sp!, {pc} call_gfunc: str lr, [sp, #-4]! sub sp, sp, #4 bl gfunc(PLT) add sp, sp, #4 ldmfd sp!, {pc} extern int gfunc(); void call_gfunc() { gfunc(); } Non PIC PIC gfunc の直接呼出し ( ローダが gfunc のアドレスが 確定した後にこの bl 命令のオフセットを 書き換える。 ) PLT スタブ経由
31 Non PIC のコードの問題点 setGvar: ldr r3,.L3 str r0, [r3, #0] bx lr.L4:.align 2.L3:.word gvar Non PIC 変数 gvar の絶対アドレス ダイナミックリンクライブラリの場合は ロードした後に絶対アドレスが決まる。 ローダが解決した値をここに書き込む ( ローダの負荷が大きく起動時間増大 ) コード領域に書き込みが発生する コード領域のそのページは 共有できない (dirty で private なページになる。) 物理メモリ使用量増大 PIC ならば実行され ないコード領域は 物理メモリにロード されない。
32 シンボルの visibility デフォルトでは全ての static でない関数、変数は ライブラリの外部からの参照が可能になってい る。 → そのため、 PLT スタブ経由でのアクセスに なっている → オーバーヘッド多め デフォルトの設定を「ライブラリ外から参照不 可」にしてインタフェース関数のみを明示的に 外部から参照を許可するほうがよい。 シンボルの visibility の設定は gcc 4.0 以降で可能。
33 libhello hellohello1 puts libhellolibc hello2 $ cat hello1.c void hello1() { hello2();} void hello() { hello1();} $ cat hello2.c #include void hello2(){ puts("Hello, world\n");} $ make cc -fpic -c -o hello1.o hello1.c cc -fpic -c -o hello2.o hello2.c cc -shared -o libhello.so hello1.o hello2.o -lc $
34 実際には hellohello1 puts libhellolibc hello2 hello1, hello2 の呼び出しはライブラリ内に閉じているのにも かかわらず PLT スタブ経由の呼び出しになってしまう。 (static 関数にすれば直接コールされるが同じソースファイルにある必要がある... )
35 余談 hellohello1 puts libhello1 libc hello2 hello1 同一の関数名 hello1 を持つライブラリ libhello1 を作って 環境変数 LD_PRELOAD を使って検索パスの前に置くと この図のように、 libhello を変更せずに関数 hello1 を置き換える ことができる。 “BINARY HACKS” hack #60 LD_PRELOAD で共有ライブラリを差し替える libhello
36 visibility を設定すると hellohello1 puts libhellolibc hello2 hello だけ公開し、それ以外を非公開にする $cat hello1.c #define EXPORT __attribute__((visibility ("default"))) void hello1() { hello2();} EXPORT void hello(){ hello1();} $ make cc -fpic -fvisibility=hidden -c -o hello1.o hello1.c cc -fpic -fvisibility=hidden -c -o hello2.o hello2.c cc -shared -o libhello.so hello1.o hello2.o -lc $ 余分な PLT が節約できた。
37 まとめ ダイナミックリンクのためには -fpic をつけ てコンパイルするのが定跡。 -fpic をつけないほうがコードはすっきりし ているが、起動時にローダに負担がかか り、コード領域が他のプロセスと共有で きなくなる。 gcc4.0 以降ならばシンボルの visibility を適 切にコントロールすることで実行時シン ボル解決のオーバーヘッドを低減できる。
38 参考文献 “How To Write Shared Libraries” “Linkers & Loaders” オーム社 “BINARY HACKS” オライリージャパン “GNU Development Tools” Wataru Nishida GNU C ライブラリのソース その他たくさんの WEB 検索結果