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