C/C++ミックスの実行体を使うようにしたら、実は __la_symbol_ptr セクションは存在しつつ、 __got セクションが出現しました。__la_symbol_ptrセクションが示す先が PLT.GOT相当の領域っぽいなぁと思っていただけに、そのものズバリなセクションが出現して驚きでした。この__gotセクションも他の同種セクションのように扱うだけで参照先を特定することができます。
今回は、こういった外部関数の呼び出しにおいてどのように解決処理がなされるのかを追いかけてみたいと思います。
用意するもの
#includeint main() { printf( "Hello, " ); printf( "world.\n" ); return 0; }
上記のような簡単な 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.out`main: -> 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.out`symbol 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.out`main + 41, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1 frame #0: 0x0000000100000f29 a.out`main + 41 a.out`main + 41: -> 0x100000f29: callq 0x100000f3e ; symbol stub for: printf 0x100000f2e: movl $0x0, %ecx (lldb) si Process 1698 stopped * thread #1: tid = 0x25c01, 0x0000000100000f3e a.out`printf, queue = 'com.apple.main-thread', stop reason = instruction step into frame #0: 0x0000000100000f3e a.out`printf a.out`symbol 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っぽいなーと思います。