golangのバイナリをちょっとまじめに読んでみる
golangのバイナリをちょっとまじめに読んだのでメモ。runtimeのコードと合わせて、バイナリがどういう構造になっているのか正面から読んだ。
環境
$ uname -a Linux ubuntu 3.19.0-58-generic #64~14.04.1-Ubuntu SMP Fri Mar 18 19:05:43 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 14.04.4 LTS Release: 14.04 Codename: trusty $ go version go version go1.6.2 linux/amd64
準備
バイナリを生成
"hello, world"をprintするだけのバイナリを生成する。
コード
package main import "fmt" func main() { fmt.Println("hello, world") }
バイナリを読んでいく
エントリポイント
まずは、gdbでエントリポイントを確認する。
gdb-peda$ i file Symbols from "/mnt/hgfs/code/go_study/print_test". Local exec file: `/mnt/hgfs/code/go_study/print_test', file type elf64-x86-64. Entry point: 0x4562a0 0x0000000000401000 - 0x00000000004aa630 is .text 0x00000000004ab000 - 0x000000000052d7df is .rodata 0x000000000052d7e0 - 0x000000000052f898 is .typelink 0x000000000052f898 - 0x000000000052f898 is .gosymtab 0x000000000052f8a0 - 0x000000000057fb0b is .gopclntab 0x0000000000580000 - 0x0000000000581ce8 is .noptrdata 0x0000000000581d00 - 0x0000000000584250 is .data 0x0000000000584260 - 0x000000000059eab8 is .bss 0x000000000059eac0 - 0x00000000005a38c0 is .noptrbss 0x0000000000400fc8 - 0x0000000000401000 is .note.go.buildid gdb-peda$ i symbol 0x4562a0 _rt0_amd64_linux in section .text
エントリポイントは0x4562a0の_rt0_amd64_linux だと分かる。
つづいて_rt0_amd64_linuxの中身を見る。
gdb-peda$ disas _rt0_amd64_linux Dump of assembler code for function _rt0_amd64_linux: 0x00000000004562a0 <+0>: lea rsi,[rsp+0x8] 0x00000000004562a5 <+5>: mov rdi,QWORD PTR [rsp] 0x00000000004562a9 <+9>: lea rax,[rip+0x10] # 0x4562c0 <main> 0x00000000004562b0 <+16>: jmp rax 0x00000000004562b2 <+18>: int3 0x00000000004562b3 <+19>: int3 0x00000000004562b4 <+20>: int3 0x00000000004562b5 <+21>: int3 0x00000000004562b6 <+22>: int3 0x00000000004562b7 <+23>: int3 0x00000000004562b8 <+24>: int3 0x00000000004562b9 <+25>: int3 0x00000000004562ba <+26>: int3 0x00000000004562bb <+27>: int3 0x00000000004562bc <+28>: int3 0x00000000004562bd <+29>: int3 0x00000000004562be <+30>: int3 0x00000000004562bf <+31>: int3 End of assembler dump.
main関数に飛ばすだけだった。
lib.go でアーキテクチャからエントリポイントに置く関数を決定したあと、この場合だと rt0_linux_amd64.s に書かれた関数の処理内容を、elf.go でELFのエントリポイントに書き込むようだ。
main関数
つづいてmain関数を読んでいく。
gdb-peda$ disas main Dump of assembler code for function main: 0x00000000004562c0 <+0>: lea rax,[rip+0xffffffffffffcb19] # 0x452de0 <runtime.rt0_go> 0x00000000004562c7 <+7>: jmp rax 0x00000000004562c9 <+9>: int3 0x00000000004562ca <+10>: int3 0x00000000004562cb <+11>: int3 0x00000000004562cc <+12>: int3 0x00000000004562cd <+13>: int3 0x00000000004562ce <+14>: int3 0x00000000004562cf <+15>: int3 End of assembler dump.
main関数はruntime.rt0_goに飛ばすだけで、ここで文字列の出力等のメインの処理を行っているわけではないようだ。
これは rt0_linux_amd64.s で定義されていた。
runtime.rt0_go
runtime.rt0_goを読む。長いので4つに分けて読んでいく。
なお、この関数は asm_amd64.s で定義されていた。
gdb-peda$ disas 0x452de0 Dump of assembler code for function runtime.rt0_go: 0x0000000000452de0 <+0>: mov rax,rdi 0x0000000000452de3 <+3>: mov rbx,rsi 0x0000000000452de6 <+6>: sub rsp,0x27 0x0000000000452dea <+10>: and rsp,0xfffffffffffffff0 0x0000000000452dee <+14>: mov QWORD PTR [rsp+0x10],rax 0x0000000000452df3 <+19>: mov QWORD PTR [rsp+0x18],rbx 0x0000000000452df8 <+24>: lea rdi,[rip+0x131d61] # 0x584b60 <runtime.g0> 0x0000000000452dff <+31>: lea rbx,[rsp-0xff98] 0x0000000000452e07 <+39>: mov QWORD PTR [rdi+0x10],rbx 0x0000000000452e0b <+43>: mov QWORD PTR [rdi+0x18],rbx 0x0000000000452e0f <+47>: mov QWORD PTR [rdi],rbx 0x0000000000452e12 <+50>: mov QWORD PTR [rdi+0x8],rsp 0x0000000000452e16 <+54>: xor eax,eax 0x0000000000452e18 <+56>: cpuid 0x0000000000452e1a <+58>: cmp rax,0x0 0x0000000000452e1e <+62>: je 0x452e9a <runtime.rt0_go+186> 0x0000000000452e20 <+64>: cmp ebx,0x756e6547 0x0000000000452e26 <+70>: jne 0x452e3f <runtime.rt0_go+95> 0x0000000000452e28 <+72>: cmp edx,0x49656e69 0x0000000000452e2e <+78>: jne 0x452e3f <runtime.rt0_go+95> 0x0000000000452e30 <+80>: cmp ecx,0x6c65746e 0x0000000000452e36 <+86>: jne 0x452e3f <runtime.rt0_go+95> 0x0000000000452e38 <+88>: mov BYTE PTR [rip+0x14bc95],0x1 # 0x59ead4 <runtime.lfenceBeforeRdtsc> 0x0000000000452e3f <+95>: mov rax,0x1 0x0000000000452e46 <+102>: cpuid 0x0000000000452e48 <+104>: mov DWORD PTR [rip+0x14bc9a],ecx # 0x59eae8 <runtime.cpuid_ecx> 0x0000000000452e4e <+110>: mov DWORD PTR [rip+0x14bc98],edx # 0x59eaec <runtime.cpuid_edx> 0x0000000000452e54 <+116>: and ecx,0x18000000 0x0000000000452e5a <+122>: cmp ecx,0x18000000 0x0000000000452e60 <+128>: jne 0x452f78 <runtime.rt0_go+408> 0x0000000000452e66 <+134>: xor ecx,ecx 0x0000000000452e68 <+136>: xgetbv 0x0000000000452e6b <+139>: and eax,0x6 0x0000000000452e6e <+142>: cmp eax,0x6 0x0000000000452e71 <+145>: jne 0x452f78 <runtime.rt0_go+408> 0x0000000000452e77 <+151>: mov BYTE PTR [rip+0x14bc58],0x1 # 0x59ead6 <runtime.support_avx> 0x0000000000452e7e <+158>: mov eax,0x7 0x0000000000452e83 <+163>: xor ecx,ecx 0x0000000000452e85 <+165>: cpuid 0x0000000000452e87 <+167>: and ebx,0x20 0x0000000000452e8a <+170>: cmp ebx,0x20 0x0000000000452e8d <+173>: jne 0x452f6c <runtime.rt0_go+396> 0x0000000000452e93 <+179>: mov BYTE PTR [rip+0x14bc3d],0x1 # 0x59ead7 <runtime.support_avx2>
0x452e18から0x452e93まででは、Intel CPUを使用しているか確認し、それがAVX、AVX2拡張命令セットをサポートしているCPUであればそれに対応するための処理を実行しているようだ。
0x0000000000452e9a <+186>: mov rax,QWORD PTR [rip+0x1313c7] # 0x584268 <_cgo_init> 0x0000000000452ea1 <+193>: test rax,rax 0x0000000000452ea4 <+196>: je 0x452f34 <runtime.rt0_go+340> 0x0000000000452eaa <+202>: mov rcx,rdi 0x0000000000452ead <+205>: lea rsi,[rip+0x1dbc] # 0x454c70 <setg_gcc> 0x0000000000452eb4 <+212>: call rax 0x0000000000452eb6 <+214>: lea rcx,[rip+0x131ca3] # 0x584b60 <runtime.g0> 0x0000000000452ebd <+221>: mov rax,QWORD PTR [rcx] 0x0000000000452ec0 <+224>: add rax,0x2d0 0x0000000000452ec6 <+230>: mov QWORD PTR [rcx+0x10],rax 0x0000000000452eca <+234>: mov QWORD PTR [rcx+0x18],rax
0x452e9aから0x452ea4までで、_cgo_initとシンボルが付いているアドレスの値がゼロか確認している。goには、cgoというc言語のコードを呼べるようにする機能があり、それが有効であった場合のみ、cgoを有効にするための処理をするようだ。今回、cgoは使っていないので 0x452f34にジャンプする。0x452eb6から0x452ecaまでではStackGuardの更新をしている。
0x0000000000452f34 <+340>: lea rdi,[rip+0x131fc5] # 0x584f00 <runtime.m0+96> 0x0000000000452f3b <+347>: call 0x456790 <runtime.settls> 0x0000000000452f40 <+352>: mov QWORD PTR fs:0xfffffffffffffff8,0x123 0x0000000000452f4d <+365>: mov rax,QWORD PTR [rip+0x131fac] # 0x584f00 <runtime.m0+96> 0x0000000000452f54 <+372>: cmp rax,0x123 0x0000000000452f5a <+378>: je 0x452ece <runtime.rt0_go+238> 0x0000000000452f60 <+384>: mov DWORD PTR ds:0x0,eax 0x0000000000452f67 <+391>: jmp 0x452ece <runtime.rt0_go+238> 0x0000000000452f6c <+396>: mov BYTE PTR [rip+0x14bb64],0x0 # 0x59ead7 <runtime.support_avx2> 0x0000000000452f73 <+403>: jmp 0x452e9a <runtime.rt0_go+186> 0x0000000000452f78 <+408>: mov BYTE PTR [rip+0x14bb57],0x0 # 0x59ead6 <runtime.support_avx> 0x0000000000452f7f <+415>: jmp 0x452f6c <runtime.rt0_go+396> 0x0000000000452f81 <+417>: int3 0x0000000000452f82 <+418>: int3 0x0000000000452f83 <+419>: int3 0x0000000000452f84 <+420>: int3 0x0000000000452f85 <+421>: int3 0x0000000000452f86 <+422>: int3 0x0000000000452f87 <+423>: int3 0x0000000000452f88 <+424>: int3 0x0000000000452f89 <+425>: int3 0x0000000000452f8a <+426>: int3 0x0000000000452f8b <+427>: int3 0x0000000000452f8c <+428>: int3 0x0000000000452f8d <+429>: int3 0x0000000000452f8e <+430>: int3 0x0000000000452f8f <+431>: int3 End of assembler dump.
0x452f3bでruntime.settlsという関数を呼び、TLSのセットアップをしている。TLS (Thread Local Storage)は他のスレッドからは参照されない、スレッド固有のデータを保持できるようにするものである。この関数は sys_linux_amd64.s で定義されていた。
TLSのセットアップが終われば0x452eceにジャンプする。
0x0000000000452ece <+238>: lea rcx,[rip+0x131c8b] # 0x584b60 <runtime.g0> 0x0000000000452ed5 <+245>: mov QWORD PTR fs:0xfffffffffffffff8,rcx 0x0000000000452ede <+254>: lea rax,[rip+0x131fbb] # 0x584ea0 <runtime.m0> 0x0000000000452ee5 <+261>: mov QWORD PTR [rax],rcx 0x0000000000452ee8 <+264>: mov QWORD PTR [rcx+0x30],rax 0x0000000000452eec <+268>: cld 0x0000000000452eed <+269>: call 0x4364c0 <runtime.check> 0x0000000000452ef2 <+274>: mov eax,DWORD PTR [rsp+0x10] 0x0000000000452ef6 <+278>: mov DWORD PTR [rsp],eax 0x0000000000452ef9 <+281>: mov rax,QWORD PTR [rsp+0x18] 0x0000000000452efe <+286>: mov QWORD PTR [rsp+0x8],rax 0x0000000000452f03 <+291>: call 0x435e20 <runtime.args> 0x0000000000452f08 <+296>: call 0x425770 <runtime.osinit> 0x0000000000452f0d <+301>: call 0x42ab00 <runtime.schedinit> 0x0000000000452f12 <+306>: lea rax,[rip+0xd80ff] # 0x52b018 <runtime.mainPC> 0x0000000000452f19 <+313>: push rax 0x0000000000452f1a <+314>: push 0x0 0x0000000000452f1c <+316>: call 0x430b30 <runtime.newproc> 0x0000000000452f21 <+321>: pop rax 0x0000000000452f22 <+322>: pop rax 0x0000000000452f23 <+323>: call 0x42c7b0 <runtime.mstart> 0x0000000000452f28 <+328>: mov DWORD PTR ds:0xf1,0xf1 0x0000000000452f33 <+339>: ret
0x452eedでruntime.checkという関数を呼んでいる。これは型のサイズ等を確認する。この関数は runtime1.go で定義されていた。
0x452f03でruntime.argsという関数を呼んでいる。ここではコマンドライン引数とその個数を保存している。この関数も runtime1.go で定義されていた。
0x452f08から0x452f23ではメインの処理を行うgoroutineをスタートする処理を行っている。
0x452f08でruntime.osinitという関数を呼んでいる。ncpuという変数にCPUのコア数を入れている。
os1_linux.go で定義されていた。
0x452f0dでruntime.schedinitという関数を呼んでいる。proc.go で定義されていた。
これはスケジューリングを行い、race conditionを防ぐ。
0x452f1cでruntime.newprocを呼ぶことで、新しいgoroutineを作り、0x452f23でruntime.mstartを呼ぶことで、goroutineを開始している。どんなgoroutineを開始したのか。
0x452f12でruntime.mainPCというシンボルのついたメモリアドレスが指す値をlea命令によってraxに入れているのが気になる。
runtime.mainPCをgdbで確認するとruntime.mainという関数へのポインタになっていた。
gdb-peda$ x/x 0x52b018 0x52b018 <runtime.mainPC>: 0x0000000000429800 gdb-peda$ i symbol 0x429800 runtime.main in section .text of /mnt/hgfs/code/go_study/print_test
つづく0x452f19でこのポインタをpushしている。runtime.newproc、runtime.mstartで使うためのようだ。runtime.main関数がgoroutineとしてスタートされるようだ。
runtime.newprocは proc.go で、runtime.mstartは proc.go で定義されていた。
runtime.rt0_goがCPUの確認やcgoを使う準備、runtime.mainの起動を行う関数だと分かった。
runtime.main
この関数は長いのでmain.initとmain.mainという関数を呼んでいることに着目し、これらを読んでいく。
gdb-peda$ disas 0x429800 Dump of assembler code for function runtime.main: ... 0x0000000000429a7a <+634>: call 0x401100 <main.init> ... 0x0000000000429aab <+683>: call 0x401000 <main.main>
main.init
gdb-peda$ disas 0x401100 Dump of assembler code for function main.init: 0x0000000000401100 <+0>: mov rcx,QWORD PTR fs:0xfffffffffffffff8 0x0000000000401109 <+9>: cmp rsp,QWORD PTR [rcx+0x10] 0x000000000040110d <+13>: jbe 0x401143 <main.init+67> 0x000000000040110f <+15>: movzx ebx,BYTE PTR [rip+0x19d9ac] # 0x59eac2 <main.initdone.> 0x0000000000401116 <+22>: cmp bl,0x0 0x0000000000401119 <+25>: je 0x40112f <main.init+47> 0x000000000040111b <+27>: movzx ebx,BYTE PTR [rip+0x19d9a0] # 0x59eac2 <main.initdone.> 0x0000000000401122 <+34>: cmp bl,0x2 0x0000000000401125 <+37>: jne 0x401128 <main.init+40> 0x0000000000401127 <+39>: ret 0x0000000000401128 <+40>: call 0x426660 <runtime.throwinit> 0x000000000040112d <+45>: ud2 0x000000000040112f <+47>: mov BYTE PTR [rip+0x19d98c],0x1 # 0x59eac2 <main.initdone.> 0x0000000000401136 <+54>: call 0x466f70 <fmt.init> 0x000000000040113b <+59>: mov BYTE PTR [rip+0x19d980],0x2 # 0x59eac2 <main.initdone.> 0x0000000000401142 <+66>: ret 0x0000000000401143 <+67>: call 0x4531f0 <runtime.morestack_noctxt> 0x0000000000401148 <+72>: jmp 0x401100 <main.init> 0x000000000040114a <+74>: int3 0x000000000040114b <+75>: int3 0x000000000040114c <+76>: int3 0x000000000040114d <+77>: int3 0x000000000040114e <+78>: int3 0x000000000040114f <+79>: int3 End of assembler dump.
0x401136でfmt.init関数を呼び、importしたfmtパッケージの初期化を行っている。main.initはパッケージの初期化を行う関数のようだ。
main.main
$ disas 0x401000 Dump of assembler code for function main.main: 0x0000000000401000 <+0>: mov rcx,QWORD PTR fs:0xfffffffffffffff8 0x0000000000401009 <+9>: cmp rsp,QWORD PTR [rcx+0x10] 0x000000000040100d <+13>: jbe 0x4010ec <main.main+236> 0x0000000000401013 <+19>: sub rsp,0x78 0x0000000000401017 <+23>: lea rbx,[rip+0x100252] # 0x501270 0x000000000040101e <+30>: mov QWORD PTR [rsp+0x50],rbx 0x0000000000401023 <+35>: mov QWORD PTR [rsp+0x58],0xc 0x000000000040102c <+44>: xor ebx,ebx 0x000000000040102e <+46>: mov QWORD PTR [rsp+0x40],rbx 0x0000000000401033 <+51>: mov QWORD PTR [rsp+0x48],rbx 0x0000000000401038 <+56>: lea rbx,[rsp+0x40] 0x000000000040103d <+61>: cmp rbx,0x0 0x0000000000401041 <+65>: je 0x4010e5 <main.main+229> 0x0000000000401047 <+71>: mov QWORD PTR [rsp+0x68],0x1 0x0000000000401050 <+80>: mov QWORD PTR [rsp+0x70],0x1 0x0000000000401059 <+89>: mov QWORD PTR [rsp+0x60],rbx 0x000000000040105e <+94>: lea rbx,[rip+0xb7d9b] # 0x4b8e00 0x0000000000401065 <+101>: mov QWORD PTR [rsp],rbx 0x0000000000401069 <+105>: lea rbx,[rsp+0x50] 0x000000000040106e <+110>: mov QWORD PTR [rsp+0x8],rbx 0x0000000000401073 <+115>: mov QWORD PTR [rsp+0x10],0x0 0x000000000040107c <+124>: call 0x40b9f0 <runtime.convT2E> 0x0000000000401081 <+129>: mov rcx,QWORD PTR [rsp+0x18] 0x0000000000401086 <+134>: mov rax,QWORD PTR [rsp+0x20] 0x000000000040108b <+139>: mov rbx,QWORD PTR [rsp+0x60] 0x0000000000401090 <+144>: mov QWORD PTR [rsp+0x30],rcx 0x0000000000401095 <+149>: mov QWORD PTR [rbx],rcx 0x0000000000401098 <+152>: mov QWORD PTR [rsp+0x38],rax 0x000000000040109d <+157>: cmp BYTE PTR [rip+0x19da3c],0x0 # 0x59eae0 <runtime.writeBarrier> 0x00000000004010a4 <+164>: jne 0x4010d1 <main.main+209> 0x00000000004010a6 <+166>: mov QWORD PTR [rbx+0x8],rax 0x00000000004010aa <+170>: mov rbx,QWORD PTR [rsp+0x60] 0x00000000004010af <+175>: mov QWORD PTR [rsp],rbx 0x00000000004010b3 <+179>: mov rbx,QWORD PTR [rsp+0x68] 0x00000000004010b8 <+184>: mov QWORD PTR [rsp+0x8],rbx 0x00000000004010bd <+189>: mov rbx,QWORD PTR [rsp+0x70] 0x00000000004010c2 <+194>: mov QWORD PTR [rsp+0x10],rbx 0x00000000004010c7 <+199>: call 0x45a680 <fmt.Println> 0x00000000004010cc <+204>: add rsp,0x78 0x00000000004010d0 <+208>: ret 0x00000000004010d1 <+209>: lea r8,[rbx+0x8] 0x00000000004010d5 <+213>: mov QWORD PTR [rsp],r8 0x00000000004010d9 <+217>: mov QWORD PTR [rsp+0x8],rax 0x00000000004010de <+222>: call 0x40eef0 <runtime.writebarrierptr> 0x00000000004010e3 <+227>: jmp 0x4010aa <main.main+170> 0x00000000004010e5 <+229>: mov DWORD PTR [rbx],eax 0x00000000004010e7 <+231>: jmp 0x401047 <main.main+71> 0x00000000004010ec <+236>: call 0x4531f0 <runtime.morestack_noctxt> 0x00000000004010f1 <+241>: jmp 0x401000 <main.main> 0x00000000004010f6 <+246>: int3 0x00000000004010f7 <+247>: int3 0x00000000004010f8 <+248>: int3 0x00000000004010f9 <+249>: int3 0x00000000004010fa <+250>: int3 0x00000000004010fb <+251>: int3 0x00000000004010fc <+252>: int3 0x00000000004010fd <+253>: int3 0x00000000004010fe <+254>: int3 0x00000000004010ff <+255>: int3 End of assembler dump.
0x4010c7でfmt.Println関数を呼び出し、"hello, world"を出力している。main.mainはメインとなる処理を行う、c言語のmain関数のようなものだと分かった。
まとめ
goのバイナリにおいてメインの処理を行うのはmain.mainであり、これは_rt0_amd64_linux、main、runtime.rt0_go、runtime.mainを経て呼び出されると分かった。
感想
処理系のコードと照らし合わせながら、バイナリを読んでいくことで、新たな楽しさを見つけられた。この記事がgoの開発やリバース・エンジニアリングの助けとなればうれしい。
参考資料
http://blog.matttproud.com/2015/02/exploring-gos-runtime-how-process.html
Golang Internals, Part 5: the Runtime Bootstrap Process | Altoros
Golang Internals, Part 6: Bootstrapping and Memory Allocator Initialization | Altoros
A Quick Guide to Go's Assembler - The Go Programming Language
http://www.binwang.me/2014-09-01-notes-on-go-scheduler.html