mach-oの外部関数解決の仕組みを追う(__stubs, __nl_symbol_ptr, __la_symbol_ptrセクション)

C/C++ミックスの実行体を使うようにしたら、実は __la_symbol_ptr セクションは存在しつつ、 __got セクションが出現しました。__la_symbol_ptrセクションが示す先が PLT.GOT相当の領域っぽいなぁと思っていただけに、そのものズバリなセクションが出現して驚きでした。この__gotセクションも他の同種セクションのように扱うだけで参照先を特定することができます。

今回は、こういった外部関数の呼び出しにおいてどのように解決処理がなされるのかを追いかけてみたいと思います。

用意するもの

上記のような簡単な Cプログラムを用意して確認してみます。
このプログラムで、1回目の printf で関数初回呼び出し、2回目で解決済み呼び出しとなるのか、を調査しようという計画です。

調査

まずは実行体の調査をしておきます。このプログラムでは __stubs の内容が以下のようになっていました(以前のプログラムを実行させて必要そうなデータだけ抜粋します)。

section[1] : __TEXT [__stubs]
           : addr=0x100000f3e, offset=3902, size=6
           : align=2, reloff=0, nreloc=0
           : resv0=0, resv1=6, resv2=0
           : flags=80000408 > S_SYMBOL_STUBS S_ATTR_PURE_INSTRUCTIONS S_ATTR_SOME_INSTRUCTIONS
           : Indirect Sym Index = 0, Size of Stubs = 6 (stub count=1)
section[2] : __TEXT [__stub_helper]
           : addr=0x100000f44, offset=3908, size=26
           : align=4, reloff=0, nreloc=0
           : resv0=0, resv1=0, resv2=0
           : flags=80000400 > S_REGULAR S_ATTR_PURE_INSTRUCTIONS S_ATTR_SOME_INSTRUCTIONS

section[0] : __DATA [__nl_symbol_ptr]
           : addr=0x100001000, offset=4096, size=16
           : align=8, reloff=0, nreloc=0
           : resv0=1, resv1=0, resv2=0
           : flags=00000006 > S_NON_LAZY_SYMBOL_POINTERS
           : Indirect Sym Index = 1
section[1] : __DATA [__la_symbol_ptr]
           : addr=0x100001010, offset=4112, size=8
           : align=8, reloff=0, nreloc=0
           : resv0=3, resv1=0, resv2=0
           : flags=00000007 > S_LAZY_SYMBOL_POINTERS
           : Indirect Sym Index = 3

nl_symbol_ptr
  offset:00001000  data: 0000000000000000  (dyld_stub_binder)
  offset:00001008  data: 0000000000000000  (ABSOLUTE)
la_symbol_ptr
  offset:00001010  data: 0000000100000F54  (_printf)
*** import ***
0 : _printf (100000f3e)

main関数直後でbreakさせて、コードを確認してみます。

(lldb) disassemble
a.outmain:
-> 0x100000f00:  pushq  %rbp
   0x100000f01:  movq   %rsp, %rbp
   0x100000f04:  subq   $0x10, %rsp
   0x100000f08:  leaq   0x4f(%rip), %rdi          ; "Hello,"
   0x100000f0f:  movl   $0x0, -0x4(%rbp)
   0x100000f16:  movb   $0x0, %al
   0x100000f18:  callq  0x100000f3e               ; symbol stub for: printf
   0x100000f1d:  leaq   0x41(%rip), %rdi          ; "world.\n"
   0x100000f24:  movl   %eax, -0x8(%rbp)
   0x100000f27:  movb   $0x0, %al
   0x100000f29:  callq  0x100000f3e               ; symbol stub for: printf
   0x100000f2e:  movl   $0x0, %ecx
   0x100000f33:  movl   %eax, -0xc(%rbp)
   0x100000f36:  movl   %ecx, %eax
   0x100000f38:  addq   $0x10, %rsp
   0x100000f3c:  popq   %rbp
   0x100000f3d:  ret    

printfの関数として 0x100000f3e アドレスを呼び出しています。この部分は事前情報によれば、__stubsの内容の先頭アドレスと一致しています。まさにprintfのスタブですね。
この 0x100000f3e アドレスでは以下のようになっていました。この周辺をちょっと調べてみます。

(lldb) disassemble 
a.outsymbol stub for: printf:
-> 0x100000f3e:  jmpq   *0xcc(%rip)               ; (void *)0x0000000100000f54

(lldb) memory read 0x100000f44+0xcc   ; 上記のrip+相対0xccで求まる番地
0x100001010: 54 0f 00 00 01 00 00 00 00 00 00 00 00 00 00 00  T...............
0x100001020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

(lldb) si
* thread #1: tid = 0x25c01, 0x0000000100000f54 a.out, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x0000000100000f54 a.out
-> 0x100000f54:  pushq  $0x0
   0x100000f59:  jmpq   0x100000f44

(lldb) b 0x100000f29
(lldb) c

Process 1698 stopped
* thread #1: tid = 0x25c01, 0x0000000100000f29 a.outmain + 41, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x0000000100000f29 a.outmain + 41
a.outmain + 41:
-> 0x100000f29:  callq  0x100000f3e               ; symbol stub for: printf
   0x100000f2e:  movl   $0x0, %ecx

(lldb) si
Process 1698 stopped
* thread #1: tid = 0x25c01, 0x0000000100000f3e a.outprintf, queue = 'com.apple.main-thread', stop reason = instruction step into
    frame #0: 0x0000000100000f3e a.outprintf
a.outsymbol stub for: printf:
-> 0x100000f3e:  jmpq   *0xcc(%rip)               ; (void *)0x00007fff966e3784: printf
   0x100000f44:  leaq   0xbd(%rip), %r11          ; (void *)0x00007fff5fc3b7b8

(lldb) memory read 0x100001010
0x100001010: 84 37 6e 96 ff 7f 00 00 00 00 00 00 00 00 00 00  .7n.?...........
0x100001020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

(lldb) disassemble --start-address 0x7fff93021878
libdyld.dylib`dyld_stub_binder:
   0x7fff93021878:  pushq  %rbp
   0x7fff93021879:  movq   %rsp, %rbp
   0x7fff9302187c:  subq   $0xc0, %rsp
   0x7fff93021883:  movq   %rdi, (%rsp)
   0x7fff93021887:  movq   %rsi, 0x8(%rsp)

わかる人には不要な話ですが、とりあえず解説をしてみます。
jmpq 命令では指定されたアドレスの内容へ飛んでいます。またこのアセンブラを見るとRIPとオフセット値(0xCC)でアドレスを求めています。そのアドレスの中身を見てみると、元々のコメントで指摘あるように 0x100000f54 が書かれていました。
このアドレスが 0x100001010 となります。このアドレスというのが la_symbol_ptrセクションの内容(先頭)となっています。
0x100000f54番地付近では 0x10000f44へジャンプするコードが見えます。この 0x10000f44 というアドレスは、__stub_helper の内容です。ここで __stub_helperの実行が行われることがわかります。
__stub_helperのやっていることはちょっと難しいので飛ばして、2回目のprintfの実行まで飛ばします。同じように 0x100000f3eのコードを見てみるとコメント部分が変わっていることに気付きます。先ほどと同じように 0x100001010 のアドレスの中身を見てみます。前回見たときの値と違う値が格納されていることがわかります。動作が同じなので今回はこの値を使って関数を呼び出していることになります。このアドレスは何かというとこれが printf関数実体です。

__stub_helperの実行後は何らかの関数アドレスが更新され、アドレス解決されることがわかります。この元々何かのアドレス格納されているが実行後更新されて格納される場所がまさにPLT.GOTっぽいなーと思います。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする