動的ライブラリの観察その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 キャッシュ ??? やっぱり今回はこのあたりは謎のまま置いておこう。

まとめ

  • ジーシンボルポインタが動的ライブラリによってレジーバインドされていた
  • dyld_stub_binder 以降の処理はまだブラックボックスのまま。ImageLoader, レジーバインド情報とは一体・・・