動的ライブラリの観察その3
前回は動的ライブラリの関数が実行される様子の概要を観た。今回は詳細を観察してみる。
事前にhoge1のセクション一覧を取得しておく。(出力結果抜粋)
$otool -l hoge1 Section sectname __text segname __TEXT addr 0x0000000100000ec8 size 0x0000000000000066 Section sectname __symbol_stub1 segname __TEXT addr 0x0000000100000f2e size 0x000000000000000c Section sectname __stub_helper segname __TEXT addr 0x0000000100000f3a size 0x0000000000000024 Section sectname __unwind_info segname __TEXT addr 0x0000000100000f60 size 0x0000000000000054 Section sectname __eh_frame segname __TEXT addr 0x0000000100000fb8 size 0x0000000000000048 Section sectname __nl_symbol_ptr segname __DATA addr 0x0000000100001000 size 0x0000000000000010 Section sectname __la_symbol_ptr segname __DATA addr 0x0000000100001010 size 0x0000000000000010 Section sectname __program_vars segname __DATA addr 0x0000000100001020 size 0x0000000000000028 Section sectname __data segname __DATA addr 0x0000000100001048 size 0x0000000000000020
gdbで動的ライブラリの関数 do_foo(int) が呼び出される様子を観察。
まずは main(int, char **)
$gdb hoge1 (gdb) disas main Dump of assembler code for function main: 0x0000000100000f04 <main+0>: push %rbp 0x0000000100000f05 <main+1>: mov %rsp,%rbp 0x0000000100000f08 <main+4>: sub $0x10,%rsp 0x0000000100000f0c <main+8>: mov %edi,-0x4(%rbp) 0x0000000100000f0f <main+11>: mov %rsi,-0x10(%rbp) 0x0000000100000f13 <main+15>: mov $0x5,%edi 0x0000000100000f18 <main+20>: callq 0x100000f2e <dyld_stub_do_foo> 0x0000000100000f1d <main+25>: mov $0x6,%edi 0x0000000100000f22 <main+30>: callq 0x100000f2e <dyld_stub_do_foo> 0x0000000100000f27 <main+35>: mov $0x0,%eax 0x0000000100000f2c <main+40>: leaveq 0x0000000100000f2d <main+41>: retq End of assembler dump.
do_foo(int) の呼び出し部分は dyld_stub_do_foo というスタブ関数への呼出となっていることがわかる。引数は第一引数(ediレジスタ)に5を渡している。
スタブ関数は __symbol_stub1 セクションに有る。
(gdb) disas 0x100000f2e 0x100000f3a Dump of assembler code from 0x100000f2e to 0x100000f3a: 0x0000000100000f2e <dyld_stub_do_foo+0>: jmpq *0xdc(%rip) # 0x100001010 0x0000000100000f34 <dyld_stub_exit+0>: jmpq *0xde(%rip) # 0x100001018 End of assembler dump.
__symbol_stub1 セクションには exit のスタブ関数も有ることがわかる。とりあえず今は do_foo(int) のスタブ関数に注目。0xdc(%rip) つまり 0x100001010 に有る値を読み込みそのアドレスにjmpしている。0x100001010 は __la_symbol_ptr セクションに含まれるアドレスなのでこのコードは レジーシンボルポインタを経由して jmp していると言うことになる。
__la_symbol_ptr セクションは今の時点ではこう。
(gdb) x /2xg 0x0000000100001010 0x100001010: 0x0000000100000f4a 0x0000000100000f54
1個目のポインタが do_foo 用のレジーシンボルポインタで、2個目が exit 用だな。
ついでに __nl_symbol_ptr セクションは今の時点ではこう。
(gdb) x /2xg 0x0000000100001000 0x100001000: 0x0000000000000000 0x0000000000000000
何も入ってない・・・。
この辺りでプログラムを開始ししてみる。とりあえず C 言語の初期化関数である start にブレイクポイントを設定してみる。
(gdb) b start Breakpoint 1 at 0x100000ecd
もう一度__nl_symbol_ptrセクションを見てみる。
(gdb) x /2xg 0x0000000100001000 0x100001000: 0x00007fff82b84fa8 0x0000000000000000
なにか入っている。これは
(gdb) x /5i 0x00007fff82b84fa8 0x7fff82b84fa8: push %rbp 0x7fff82b84fa9 : mov %rsp,%rbp ・・・
からわかるように dyld_stub_binder のアドレス。もうひとつはまだ 0 のまま。
__la_symbol_ptr に目を戻そう。これらのポインタが指しているのは __stub_helper セクションだ。__stub_helper セクションはこう。
(gdb) disas 0x0000000100000f3a 0x0000000100000f5e Dump of assembler code from 0x100000f3a to 0x100000f5e: 0x0000000100000f3a < stub helpers+0>: lea 0xc7(%rip),%r11 # 0x100001008 0x0000000100000f41 < stub helpers+7>: push %r11 0x0000000100000f43 < stub helpers+9>: jmpq *0xb7(%rip) # 0x100001000 0x0000000100000f49 < stub helpers+15>: nop 0x0000000100000f4a < stub helpers+16>: pushq $0x0 0x0000000100000f4f < stub helpers+21>: jmpq 0x100000f3a < stub helpers> 0x0000000100000f54 < stub helpers+26>: pushq $0xe 0x0000000100000f59 < stub helpers+31>: jmpq 0x100000f3a < stub helpers> End of assembler dump.
スタブ関数からレジーポインタを通してここにjmpしてくる。do_foo の場合は 0x0 を, exit の場合は 0xe をスタックへ積んだ後、二つ目のノンレジーポインタのアドレス(0x100001008)をr11に設定し、さらにスタックにもプッシュして、一つ目のノンレジーポインタを通して dyld_stub_binder へ jmp している。
とりあえずこの jmp 直前まで実行してみる。
(gdb) b *0x0000000100000f43 Breakpoint 2 at 0x100000f43 (gdb) c Continuing. Breakpoint 2, 0x0000000100000f43 in stub helpers ()
この時、スタックの内容を確認してみると以下の様にstub helpersでプッシュされた 0x0 と 二つ目のノンレジーポインタのアドレスである 0x100001008 が格納されていることがわかる。
(gdb) x /1xg $rsp 0x7fff5fbff798: 0x0000000100001008 (gdb) x /1xg $rsp+8 0x7fff5fbff7a0: 0x0000000000000000
jmp 先である dyld_stub_binder をダンプ。(抜粋)
0x00007fff82b84fa8 <dyld_stub_binder+0>: push %rbp 0x00007fff82b84fa9 <dyld_stub_binder+1>: mov %rsp,%rbp 0x00007fff82b84fac <dyld_stub_binder+4>: sub $0xc0,%rsp (レジスタの保存) 0x00007fff82b85011 <dyld_stub_binder+105>: mov 0x8(%rbp),%rdi 0x00007fff82b85015 <dyld_stub_binder+109>: mov 0x10(%rbp),%rsi 0x00007fff82b85019 <dyld_stub_binder+113>: callq 0x7fff82a88c4f <_Z21_dyld_fast_stub_entryPvl> 0x00007fff82b8501e <dyld_stub_binder+118>: mov %rax,%r11 (レジスタの復元) 0x00007fff82b8507f <dyld_stub_binder+215>: add $0xc0,%rsp 0x00007fff82b85086 <dyld_stub_binder+222>: pop %rbp 0x00007fff82b85087 <dyld_stub_binder+223>: add $0x10,%rsp 0x00007fff82b8508b <dyld_stub_binder+227>: jmpq *%r11
dyld_stub_binder は /usr/lib/libSystem.B.dylib で定義されているコードであり、ソースは dyld_stub_binder.s というアセンブリコード。x86_64用のdyld_stub_binderラベルのコメントにはこのようにある。
/* * sp+4 lazy binding info offset * sp+0 address of ImageLoader cache */
・・・いやいや、sp+0 と sp+8 やろ。ま、それはいいとして、スタブヘルパでプッシュされていた 0x0 は レジーバインド情報のオフセット、 ノンレジーポインタの二つ目のアドレスは ImageLoader キャッシュのアドレス、ということらしい。・・・なんだそれ。とりあえず今回はは気にしないでブラックボックスだと思っておこう。
dyld_stub_binder の最後の方のコード、 dyld_stub_binder+215〜223 は スタックの状態を dyld_stub_do_foo 呼び出しの時と同じ状態に復元している。
dyld_stub_binder の最後の jmp でブレイクする。
(gdb) b *0x00007fff82b8508b Breakpoint 2 at 0x7fff82b8508b (gdb) c Continuing. Breakpoint 2, 0x00007fff82b8508b in dyld_stub_binder ()
スタック確認。
(gdb) x /1xg $rsp 0x7fff5fbff7a8: 0x0000000100000f1d
確かに戻りアドレスとして、最初の dyld_stub_do_foo を call した次の命令のアドレスが入っている。
(gdb) x /1i $r11 0x100003f54: push %rbp
ここで jmp しようとしている先は動的ライブラリの do_foo(int) で有り、dyld_stub_binder の中で解決されたことがわかる。
(gdb) x /1xg 0x100001010 0x100001010: 0x0000000100003f54
do_foo(int) 用のレジーポインタの値もスタブヘルパのアドレスから do_foo(int) のアドレスへと書き換えられている。これにより以降の dyld_stub_do_foo 呼び出しでは スタブヘルパや dyld_stub_binder を経由せずに直接 do_foo(int) へ処理が移ることになる。
(gdb) x /1xg 0x100001008 0x100001008: 0x00007fff5fc43c18
二つ目のノンレジーポインタ(ImageLoader キャッシュのアドレス) にも値が入っている。ノンレジーと言いながらもレジーっぽいタイミングで値が入るんだなー。一応ダンプしてみる。
(gdb) x /4xg 0x00007fff5fc43c18 0x7fff5fc43c18 <__dyld__ZL18initialPoolContent+2104>: 0x00007fff5fc3c8f0 0x00007fff5fbff8d8 0x7fff5fc43c28 <__dyld__ZL18initialPoolContent+2120>: 0x0000000000000000 0x0000000000000000
initialPoolContent というのは dyldNew.cpp で定義されているが、これが ImageLoader キャッシュ ??? やっぱり今回はこのあたりは謎のまま置いておこう。