Goのオブジェクトファイルの中身を見てみる(リロケーション編)
前回のシンボル編に引き続き、Goのオブジェクトファイルについてのメモ。今回はリロケーションについて。前回と同じく、Linux環境、Go 1.10が前提。CPUアーキテクチャはx86_64。
リロケーション情報のフォーマット
Goのオブジェクトファイルの場合、リロケーション情報はシンボル情報の一部として定義されている。シンボルは以下のような構造になっている(objabi/doc.goより)。
Each symbol is laid out as the following fields: - byte 0xfe (sanity check for synchronization) - type [byte] - name & version [symref index] - flags [int] 1<<0 dupok 1<<1 local 1<<2 add to typelink table - size [int] - gotype [symref index] - p [data block] - nr [int] - r [nr relocations, sorted by off]
前回はこの内、リロケーション以外の部分を確認した。今回は末尾にあるリロケーション情報を確認していく。
サンプルコード
今回は以下のようなサンプルコードを使う。
package main import "fmt" type I interface { M() } type S struct{} func (s S) M() { fmt.Println("") } func callM(i I) { i.M() } func main() { var i I = S{} callM(i) }
リロケーション情報の確認方法
Goのアセンブリ言語を確認するのが一番わかりやすかった。-S
オプションを付けてコンパイルすると確認できる。
$ go tool compile -S relocations.go "".S.M STEXT size=110 args=0x0 locals=0x48 0x0000 00000 (relocations.go:11) TEXT "".S.M(SB), $72-0 0x0000 00000 (relocations.go:11) MOVQ (TLS), CX 0x0009 00009 (relocations.go:11) CMPQ SP, 16(CX) 0x000d 00013 (relocations.go:11) JLS 103 0x000f 00015 (relocations.go:11) SUBQ $72, SP 0x0013 00019 (relocations.go:11) MOVQ BP, 64(SP) 0x0018 00024 (relocations.go:11) LEAQ 64(SP), BP 0x001d 00029 (relocations.go:11) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x001d 00029 (relocations.go:11) FUNCDATA $1, gclocals·e226d4ae4a7cad8835311c6a4683c14f(SB) 0x001d 00029 (relocations.go:12) XORPS X0, X0 0x0020 00032 (relocations.go:12) MOVUPS X0, ""..autotmp_1+48(SP) 0x0025 00037 (relocations.go:12) LEAQ type.string(SB), AX 0x002c 00044 (relocations.go:12) MOVQ AX, ""..autotmp_1+48(SP) 0x0031 00049 (relocations.go:12) LEAQ "".statictmp_0(SB), AX 0x0038 00056 (relocations.go:12) MOVQ AX, ""..autotmp_1+56(SP) 0x003d 00061 (relocations.go:12) LEAQ ""..autotmp_1+48(SP), AX 0x0042 00066 (relocations.go:12) MOVQ AX, (SP) 0x0046 00070 (relocations.go:12) MOVQ $1, 8(SP) 0x004f 00079 (relocations.go:12) MOVQ $1, 16(SP) 0x0058 00088 (relocations.go:12) PCDATA $0, $1 0x0058 00088 (relocations.go:12) CALL fmt.Println(SB) 0x005d 00093 (relocations.go:13) MOVQ 64(SP), BP 0x0062 00098 (relocations.go:13) ADDQ $72, SP 0x0066 00102 (relocations.go:13) RET 0x0067 00103 (relocations.go:13) NOP 0x0067 00103 (relocations.go:11) PCDATA $0, $-1 0x0067 00103 (relocations.go:11) CALL runtime.morestack_noctxt(SB) 0x006c 00108 (relocations.go:11) JMP 0 0x0000 64 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 58 48 dH..%....H;a.vXH 0x0010 83 ec 48 48 89 6c 24 40 48 8d 6c 24 40 0f 57 c0 ..HH.l$@H.l$@.W. 0x0020 0f 11 44 24 30 48 8d 05 00 00 00 00 48 89 44 24 ..D$0H......H.D$ 0x0030 30 48 8d 05 00 00 00 00 48 89 44 24 38 48 8d 44 0H......H.D$8H.D 0x0040 24 30 48 89 04 24 48 c7 44 24 08 01 00 00 00 48 $0H..$H.D$.....H 0x0050 c7 44 24 10 01 00 00 00 e8 00 00 00 00 48 8b 6c .D$..........H.l 0x0060 24 40 48 83 c4 48 c3 e8 00 00 00 00 eb 92 $@H..H........ rel 5+4 t=16 TLS+0 rel 40+4 t=15 type.string+0 rel 52+4 t=15 "".statictmp_0+0 rel 89+4 t=8 fmt.Println+0 rel 104+4 t=8 runtime.morestack_noctxt+0 // 他のシンボルについても同様の情報が出力される。ここでは省略。
これはS構造体のMメソッドの例。前半にGoアセンブリ言語が出力される。下の方のrel
から始まる行が、リロケーション情報を示す。
各フィールドの意味
リロケーション情報は以下のような構造になっている(objabi/doc.goより)。
Each relocation has the encoding: - off [int] - siz [int] - type [int] - add [int] - sym [symref index]
off
は、relocate処理によって書き換えるアドレスを意味する。値はシンボルデータの開始アドレスからのオフセットになっている。siz
は書き換えるデータのサイズを示す。
type
はrelocateの方法を示している。Relocate処理なので基本的には特定のアドレスを指定された領域に書き込むのだけれど、そのときに絶対アドレスを書くのか相対アドレスを書くのか、といった具体的な方法が示されている。また、リンク時に特殊な役割を果たすtypeもある。詳細は下の方で見ていく。
add
はいわゆるaddendを意味する。Relocate処理時、add
の値を足した値を書き込む。例えば、sym
に関数のシンボルを指定して、add
に関数のサイズを指定すると、関数の終わりのアドレスを書き込める。
sym
はどのシンボルのアドレスを書き込むのかを示す。
上記のGoアセンブリ言語の出力には、rel [off]+[siz] t=[type] [sym]+[add]
というフォーマットで全フィールドの値が含まれている。
例えばrel 40+4 t=15 type.string+0
という出力だと、40がオフセット(10進数)、4がサイズ、15がrelocateの方法、type.stringがシンボル名、+0がaddendを意味する。アセンブリ出力の相応する行は0x0025 00037 (relocations.go:12) LEAQ type.string(SB), AX
なので、AXレジスタに格納する文字列型シンボルのアドレスを書き換えようとしているのがわかる。
ちなみに最初は、fmt.Println("")
しているだけなのに文字列型シンボルのアドレスが必要になるのが謎だった。よく見たら、fmt.Printlnがinterface{}(のスライス)を引数に取っていて、interface{}を渡すためには型へのポインタとデータへのポインタの両方が必要になるためだった(参考:efaceのデータ構造)。
各種タイプ1 ("".S.M関連)
実はリロケーションのタイプはGo1.10時点で44個ある。全て確認するのは大変なので、ここではいくつかソースファイルをコンパイルした時に使われていたタイプに絞って、確認していく。
まずは先程のS構造体のMメソッドの例で出てきたタイプを確認する。以下に再掲する。
"".S.M STEXT size=110 args=0x0 locals=0x48 0x0000 00000 (relocations.go:11) TEXT "".S.M(SB), $72-0 0x0000 00000 (relocations.go:11) MOVQ (TLS), CX 0x0009 00009 (relocations.go:11) CMPQ SP, 16(CX) 0x000d 00013 (relocations.go:11) JLS 103 0x000f 00015 (relocations.go:11) SUBQ $72, SP 0x0013 00019 (relocations.go:11) MOVQ BP, 64(SP) 0x0018 00024 (relocations.go:11) LEAQ 64(SP), BP 0x001d 00029 (relocations.go:11) FUNCDATA $0, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x001d 00029 (relocations.go:11) FUNCDATA $1, gclocals·e226d4ae4a7cad8835311c6a4683c14f(SB) 0x001d 00029 (relocations.go:12) XORPS X0, X0 0x0020 00032 (relocations.go:12) MOVUPS X0, ""..autotmp_1+48(SP) 0x0025 00037 (relocations.go:12) LEAQ type.string(SB), AX 0x002c 00044 (relocations.go:12) MOVQ AX, ""..autotmp_1+48(SP) 0x0031 00049 (relocations.go:12) LEAQ "".statictmp_0(SB), AX 0x0038 00056 (relocations.go:12) MOVQ AX, ""..autotmp_1+56(SP) 0x003d 00061 (relocations.go:12) LEAQ ""..autotmp_1+48(SP), AX 0x0042 00066 (relocations.go:12) MOVQ AX, (SP) 0x0046 00070 (relocations.go:12) MOVQ $1, 8(SP) 0x004f 00079 (relocations.go:12) MOVQ $1, 16(SP) 0x0058 00088 (relocations.go:12) PCDATA $0, $1 0x0058 00088 (relocations.go:12) CALL fmt.Println(SB) 0x005d 00093 (relocations.go:13) MOVQ 64(SP), BP 0x0062 00098 (relocations.go:13) ADDQ $72, SP 0x0066 00102 (relocations.go:13) RET 0x0067 00103 (relocations.go:13) NOP 0x0067 00103 (relocations.go:11) PCDATA $0, $-1 0x0067 00103 (relocations.go:11) CALL runtime.morestack_noctxt(SB) 0x006c 00108 (relocations.go:11) JMP 0 0x0000 64 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 58 48 dH..%....H;a.vXH 0x0010 83 ec 48 48 89 6c 24 40 48 8d 6c 24 40 0f 57 c0 ..HH.l$@H.l$@.W. 0x0020 0f 11 44 24 30 48 8d 05 00 00 00 00 48 89 44 24 ..D$0H......H.D$ 0x0030 30 48 8d 05 00 00 00 00 48 89 44 24 38 48 8d 44 0H......H.D$8H.D 0x0040 24 30 48 89 04 24 48 c7 44 24 08 01 00 00 00 48 $0H..$H.D$.....H 0x0050 c7 44 24 10 01 00 00 00 e8 00 00 00 00 48 8b 6c .D$..........H.l 0x0060 24 40 48 83 c4 48 c3 e8 00 00 00 00 eb 92 $@H..H........ rel 5+4 t=16 TLS+0 rel 40+4 t=15 type.string+0 rel 52+4 t=15 "".statictmp_0+0 rel 89+4 t=8 fmt.Println+0 rel 104+4 t=8 runtime.morestack_noctxt+0
R_TLS_LEタイプ (t=16)
このタイプは、TLSのアドレスを書き込む。関数の頭で見かけることが多い。これは、TLSから現在のgoルーチンのデータ(runtime.g)を参照するため。runtime.gに含まれるstackguard0を利用して、スタックを伸長するかどうかを関数の頭で判定している。
先程の例だと、関数の頭の方に0x0000 00000 (relocations.go:11) MOVQ (TLS), CX
という命令があり、これに対してrel 5+4 t=16 TLS+0
というリロケーションが指定されている。t=16がR_TLS_LEタイプを示す。Relocate処理により-8が書き込まれる。-8だけだとよくわからないけど、実は機械語命令にはfsセグメントを示すプリフィックスがついているので、%fs:0xfffffffffffffff8
という意味になる(x86_64ではfsセグメントレジスタを切り替えてTLSを切り替える)。
ちなみにR_TLS_LEのLEというのはlocal execモデルのこと。テーブルとかを介さず、シンプルにTLSブロックにアクセスできる。-shared
を付けてコンパイルすると、initial execモデルのR_TLS_IEが使われたりする。このあたりについてはこのドキュメントが詳しい。
R_PCRELタイプ (t=15)
このタイプは、変数への相対アドレスを書き込む。変数や型を参照するときに使われている。
先ほどの例だと、0x0031 00049 (relocations.go:12) LEAQ "".statictmp_0(SB), AX
という命令がこのタイプに関係している。この命令はfmt.Printlnを呼ぶ準備の一環で、文字列データを指す"".statictmp_0
のアドレスをAXレジスタに格納している。この命令に対して、rel 52+4 t=15 "".statictmp_0+0
というリロケーションが指定されている。t=15はR_PCRELタイプを示すので、文字列データへの相対アドレスを書き込むよう指定されている。ビルド後のバイナリには、0x000417c8
という値が書き込まれていた。leaの次のアドレスは0x4831b8
、"".statictmp_0のアドレスは0x4c4980
だったので、0x4c4980 - 0x4831b8 = 0x000417c8
で一致する。
R_CALLタイプ (t=8)
このタイプは、関数への相対アドレスを書き込む。関数呼び出しでは大抵これが使われる様子。
先ほどの例だと、0x0058 00088 (relocations.go:12) CALL fmt.Println(SB)
という命令に対して、rel 89+4 t=8 fmt.Println+0
というリロケーションが指定されている。t=8がR_CALLタイプを示す。ビルド後のバイナリを見てみると、0xffff9de3
という値が書き込まれていた。Relative callで、callの次のアドレスが0x4831dd
、fmt.Printlnのアドレスが0x47cfc0
だったので、ぴったり一致する。
各種タイプ2 (callM関連)
次に、callM関数のアセンブリ出力中で出現したタイプを確認する。アセンブリ出力は以下。
"".callM STEXT size=66 args=0x10 locals=0x10 0x0000 00000 (relocations.go:15) TEXT "".callM(SB), $16-16 0x0000 00000 (relocations.go:15) MOVQ (TLS), CX 0x0009 00009 (relocations.go:15) CMPQ SP, 16(CX) 0x000d 00013 (relocations.go:15) JLS 59 0x000f 00015 (relocations.go:15) SUBQ $16, SP 0x0013 00019 (relocations.go:15) MOVQ BP, 8(SP) 0x0018 00024 (relocations.go:15) LEAQ 8(SP), BP 0x001d 00029 (relocations.go:15) FUNCDATA $0, gclocals·dc9b0298814590ca3ffc3a889546fc8b(SB) 0x001d 00029 (relocations.go:15) FUNCDATA $1, gclocals·69c1753bd5f81501d95132d08af04464(SB) 0x001d 00029 (relocations.go:15) MOVQ "".i+24(SP), AX 0x0022 00034 (relocations.go:16) MOVQ 24(AX), AX 0x0026 00038 (relocations.go:16) MOVQ "".i+32(SP), CX 0x002b 00043 (relocations.go:16) MOVQ CX, (SP) 0x002f 00047 (relocations.go:16) PCDATA $0, $1 0x002f 00047 (relocations.go:16) CALL AX 0x0031 00049 (relocations.go:17) MOVQ 8(SP), BP 0x0036 00054 (relocations.go:17) ADDQ $16, SP 0x003a 00058 (relocations.go:17) RET 0x003b 00059 (relocations.go:17) NOP 0x003b 00059 (relocations.go:15) PCDATA $0, $-1 0x003b 00059 (relocations.go:15) CALL runtime.morestack_noctxt(SB) 0x0040 00064 (relocations.go:15) JMP 0 0x0000 64 48 8b 0c 25 00 00 00 00 48 3b 61 10 76 2c 48 dH..%....H;a.v,H 0x0010 83 ec 10 48 89 6c 24 08 48 8d 6c 24 08 48 8b 44 ...H.l$.H.l$.H.D 0x0020 24 18 48 8b 40 18 48 8b 4c 24 20 48 89 0c 24 ff $.H.@.H.L$ H..$. 0x0030 d0 48 8b 6c 24 08 48 83 c4 10 c3 e8 00 00 00 00 .H.l$.H......... 0x0040 eb be .. rel 5+4 t=16 TLS+0 rel 47+0 t=11 +0 rel 60+4 t=8 runtime.morestack_noctxt+0
0x001d - 0x0026
でiface構造体に格納されたS.MメソッドのアドレスをAXレジスタに格納し、0x0026 - 0x002f
でS.Mの引数(S自身)をスタックに積み、0x002f
でS.Mへ飛んでいる。
R_CALLINDタイプ (t=11)
R_CALLINDタイプは特殊なタイプで、relocate処理はしない。レジスタを介したCALLの場合にこのタイプのリロケーションが作られるように見える。
このタイプのリロケーション情報が使われるのは、リンク時のスタックサイズをチェックするあたり。このチェックは、関数を呼ぶ際に、最悪でもスタックの伸長処理までたどり着ける程度にスタックサイズに余裕があることを確認しているようだ。
各種タイプ3 (type."".S関連)
最後に、S構造体の型に関連したタイプを確認する。アセンブリ出力は以下。
type."".S SRODATA size=112 0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0010 b2 68 b4 cd 07 01 01 99 00 00 00 00 00 00 00 00 .h.............. 0x0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0x0050 00 00 00 00 01 00 00 00 10 00 00 00 00 00 00 00 ................ 0x0060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ rel 24+8 t=1 runtime.algarray+16 rel 32+8 t=1 runtime.gcbits.+0 rel 40+4 t=5 type..namedata.*main.S.+0 rel 44+4 t=5 type.*"".S+0 rel 56+8 t=1 type."".S+96 rel 80+4 t=5 type..importpath."".+0 rel 96+4 t=5 type..namedata.M.+0 rel 100+4 t=24 type.func()+0 rel 104+4 t=24 "".(*S).M+0 rel 108+4 t=24 "".S.M+0
R_ADDRタイプ (t=1)
このタイプは、絶対アドレスを書き込む。ポインタを通して他のデータを参照する場合に、このタイプが使われている。
上記の例を含め、大抵の型は先頭のデータ構造が以下のようになっている(runtime/type.goより)。
type _type struct { size uintptr ptrdata uintptr // size of memory prefix holding all pointers hash uint32 tflag tflag align uint8 fieldalign uint8 kind uint8 alg *typeAlg // gcdata stores the GC type data for the garbage collector. // If the KindGCProg bit is set in kind, gcdata is a GC program. // Otherwise it is a ptrmask bitmap. See mbitmap.go for details. gcdata *byte str nameOff ptrToThis typeOff }
これを見ると、24バイト目のalgフィールドと32バイト目のgcdataフィールドがポインタになっている。そして先程のアセンブリ出力を見ると、rel 24+8 t=1 runtime.algarray+16
とrel 32+8 t=1 runtime.gcbits.+0
と書いてある。なので、ちょうどポインタを通して他のデータを参照しているところに、R_ADDRタイプのリロケーション指定があることが分かる。
R_ADDROFFタイプ (t=5)
このタイプは、リンク後の実行イメージのうち、moduledataが指す特定領域からのオフセットアドレスを書き込む。moduledataは実行イメージのレイアウトを示す構造体で、例えばtypes
というフィールドは型情報などが集められた領域を指す。R_ADDROFFタイプはこのような領域からのオフセットを書き込みたいときに使われる。
型がnameOff
やtypeOff
の場合にこのリロケーション情報が付く。先程の_type
構造体では、40バイト目のstrと44バイト目のptrToThisフィールドが該当する。アセンブリ出力を見ると、rel 40+4 t=5 type..namedata.*main.S.+0
とrel 44+4 t=5 type.*"".S+0
と書いてあるので、nameOff
とtypeOff
型のフィールドに対して、R_ADDROFFタイプのリロケーションが指定されていることが分かる。
R_METHODOFFタイプ (t=24)
このタイプは、ほぼR_ADDROFFタイプと同じ。R_ADDROFFタイプとの違いは、リンク時、使われていないメソッドを削除する処理で使われるかどうか。リロケーションに紐付いたシンボルが、削除されるメソッドの候補になる。
Sのような構造体の型には、末尾にメソッドの情報が付く。各メソッドは以下のようなデータ構造をもつ(runtime/symtab.goより)。
type method struct { name nameOff mtyp typeOff ifn textOff tfn textOff }
S構造体の場合、0x0060 - 0x006F
までがMメソッドの情報を保持している。この範囲では、rel 100+4 t=24 type.func()+0
、rel 104+4 t=24 "".(*S).M+0
、rel 108+4 t=24 "".S.M+0
がR_METHODOFFタイプのリロケーションなので、type.func()
、"".(*S).M
と"".S.M
がリンク時に削除されるメソッドの候補になることがわかる。
まとめ
Goのオブジェクトファイルのリロケーション情報について調べた。相変わらず直接役に立つことはなさそうだけど、コンパイルからリンク、実行までの仕組みを知る良いきっかけになった。
Goのオブジェクトファイルの中身を見てみる(シンボル編)
Goのオブジェクトファイルについて調べる機会があったのでメモ。今回は主に定義済みシンボルについて。Linux環境、Go 1.10が前提。
オブジェクトファイルのフォーマット
Goのオブジェクトファイルは独自フォーマットになっている。Linux環境でよく使われるELFではない。また、仕様が決まっているわけでもないので、将来フォーマットが変わるかもしれない。
Go1.10時点のフォーマットの概要としては、次のようになっている(objabi/doc.goより)。
- magic header: "\x00\x00go19ld" - byte 1 - version number - sequence of strings giving dependencies (imported packages) - empty string (marks end of sequence) - sequence of symbol references used by the defined symbols - byte 0xff (marks end of sequence) - sequence of integer lengths: - total data length - total number of relocations - total number of pcdata - total number of automatics - total number of funcdata - total number of files - data, the content of the defined symbols - sequence of defined symbols - byte 0xff (marks end of sequence) - magic footer: "\xff\xffgo19ld"
シンボル名のリスト(sequence of symbol references)、定義済みシンボルのリスト(sequence of defined symbols)、各定義済みシンボルのデータ(data)が主な構成要素になっている。ELFとは異なり、セクションヘッダー相当のものはないし、textとdataが分けられていない。リロケーション情報などはリロケーション用のセクションという形ではなく、各シンボル情報の一部として格納されている。フラットな構造というより、ネストした構造になっている。これは、GoオブジェクトファイルはELFのように実行可能である必要がなく、ファイルの一部領域をそのままメモリ上にマップするような操作を想定しなくていいからでは、と思った。
今回はこのフォーマットのうち、定義済みシンボルのリスト(sequence of defined symbols)を確認する。各シンボルは以下の情報をもっている(objabi/doc.goより)。
Each symbol is laid out as the following fields: - byte 0xfe (sanity check for synchronization) - type [byte] - name & version [symref index] - flags [int] 1<<0 dupok 1<<1 local 1<<2 add to typelink table - size [int] - gotype [symref index] - p [data block] - nr [int] - r [nr relocations, sorted by off]
今回はこのうち、relocations以外の情報を確認していく。また、text系のシンボルは他にもフィールドをもっているが、今回は省略する。
サンプルコード
今回は以下のようなサンプルコードを使う。各種シンボルタイプを確認するため、無駄に変数宣言している。
package main var val = 1 var ptr = &val var undefined int var undefinedPtr *int var strct struct{ a int } var str = "helloworld" func main() { print(val) print(*ptr) print(undefined) undefinedPtr = &undefined print(*undefinedPtr) print(strct.a) print(str) }
シンボル確認用のツール
シンボルについて詳細な情報を出力するツールが見当たらなかったので、自作した。go get github.com/ks888/goobj
でインストールすると、readgoobj
というコマンドでオブジェクトファイルのシンボル情報を出力できる。
実行すると、定義済みのシンボル一覧が出力される。
$ readgoobj symbols.o The list of defined symbols: Offset Size Type DupOK Local MakeTypeLink Name Version GoType 0x370 0x132 STEXT false false false "".main 0 0x4d5 0x4f STEXT false false false "".init 0 0x53f 0x21 SDWARFINFO false false false go.info."".main 0 0x560 0x0 SDWARFRANGE false false false go.range."".main 0 0x560 0xa SRODATA true true false go.string."helloworld" 0 0x56a 0x21 SDWARFINFO false false false go.info."".init 0 0x58b 0x0 SDWARFRANGE false false false go.range."".init 0 0x58b 0x8 SNOPTRDATA false false false "".val 0 type.int 0x593 0x8 SDATA false false false "".ptr 0 type.*int 0x59b 0x8 SNOPTRBSS false false false "".undefined 0 type.int 0x59b 0x8 SBSS false false false "".undefinedPtr 0 type.*int 0x59b 0x8 SNOPTRBSS false false false "".strct 0 type.struct { "".a int } 0x59b 0x10 SDATA false false false "".str 0 type.string 0x5ab 0x1 SNOPTRBSS false false false "".initdone· 0 type.uint8 0x5ab 0x1 SRODATA true true false runtime.gcbits.01 0 0x5ac 0x14 SRODATA true false false type..namedata.*struct { a int }- 0 0x5c0 0x38 SRODATA true false true type.*struct { "".a int } 0 0x5f8 0x0 SRODATA true true false runtime.gcbits. 0 0x5f8 0x4 SRODATA true false false type..namedata.a- 0 0x5fc 0x68 SRODATA true false true type.struct { "".a int } 0 0x664 0x8 SRODATA true false false gclocals·33cdeccccebe80329f1fdbee7f5874cb 0
ここからは、シンボルの各フィールドの意味を順番に確認していく。
Offset, Size
Offsetはシンボルが指すデータの開始位置、Sizeはデータのサイズを指す。
Type
Typeはシンボルの種類を表していて、Go1.10時点では以下の種類がある(objabi/symkind.goより)。
// An otherwise invalid zero value for the type Sxxx SymKind = iota // Executable instructions STEXT // Read only static data SRODATA // Static data that does not contain any pointers SNOPTRDATA // Static data SDATA // Statically data that is initially all 0s SBSS // Statically data that is initially all 0s and does not contain pointers SNOPTRBSS // Thread-local data that is initially all 0s STLSBSS // Debugging data SDWARFINFO SDWARFRANGE SDWARFLOC
Sxxxは通常は使用しない値だと思う。コンパイラも生成している様子はない。
STEXT
STEXTのシンボルはプログラムコードを指す。例えばobjdumpでmain関数を出力してみると、main関数の開始アドレスは0x370
であることがわかる。このアドレスは"".main
シンボルのOffset(0x370
)に一致しているので、"".main
シンボルがプログラムコードを指していることがわかる。
$ go tool objdump -s main symbols.o TEXT %22%22.main(SB) gofile../vagrant/symbols.go symbols.go:15 0x370 64488b0c2500000000 MOVQ FS:0, CX [5:9]R_TLS_LE // 省略
リンク時、このシンボルのデータはELFの.textセクションに格納される。以下のように実行バイナリをビルドしてreadelfしてみると、"".main
は、セクション1 (.text
) を指していることがわかる (""
はリンク時にパッケージ名で置き換えられる)。
$ go build symbols.go $ readelf --symbols --wide symbols | grep main.main 1305: 000000000044d980 306 FUNC GLOBAL DEFAULT 1 main.main
SRODATA
次に、SRODATAのシンボルは、定義した型や文字列などのリードオンリーのデータを指している。例えば、type.struct { "".a int }
は定義した型の情報を示している。型によるが、構造体の型の場合は以下のような情報がシンボルのデータとして保持される(reflect/type.goより)。
// structType represents a struct type. type structType struct { rtype `reflect:"struct"` pkgPath name fields []structField // sorted by offset }
リンク時、これらのデータは.rodataセクションに置かれる。実行時は、リンカが生成するmoduledataオブジェクトのtypes
、etypes
といったフィールドからこれらのデータが格納された領域を取得し、更にそこからのオフセットでデータを取得する。reflectパッケージはこの仕組みを使っているようだ。
SNOPTRDATA, SDATA, SBSS, SNOPTRBSS
SNOPTRDATA, SDATA, SBSS, SNOPTRBSSのシンボルは、パッケージレベルで宣言された変数を表す。コンパイル段階で値がわかっているかどうか、データがポインタを含むかどうかによって、4種類のシンボルがある。サンプルコードでは、var val = 1
がSNOPTRDATA、var ptr = &val
がSDATA、var undefined int
がSNOPTRBSS、var undefinedPtr *int
がSBSSに該当する。
リンク時、これらのデータはそれぞれ.noptrdataセクション、.dataセクション、.bssセクション、.noptrbssセクションに格納される。実行時はSRODATAと同じく、moduledataオブジェクトのnoptrdata
、enoptrdata
といったフィールドとオフセットでデータを取得できる。
ポインタの有無でセクションを分けているのは、主にGCの実行を効率化するためだと思う。GCのrootオブジェクトをmarkしていくコードをみると、noptr系のセクションはmark対象外になっている。
STLSBSS
STLSBSSのシンボルはTLSに保存するデータを指すのだと思うけど、実際に出力しているところは見つけられなかった。
SDWARFINFO, SDWARFRANGE, SDWARFLOC
SDWARFINFO, SDWARFRANGE, SDWARFLOCは、デバッグ用途のシンボル。STEXTのシンボルごとに一つずつ作られる。SDWARFINFOの情報を元にELFの.debug_infoセクションが、SDWARFRANGEの情報を元に.debug_rangesセクションが作られる。SDWARFLOCのシンボルはデフォルトでは生成されず、compile時に-dwarflocationlists
を付けると生成される。.debug_*セクションの意味はこことかが詳しい。
Flags (DupOK, Local, MakeTypeLink)
DupOKフラグは、他オブジェクトファイルにおける同名シンボルの定義を許可するフラグ。基本的にSRODATAタイプのシンボルには、これが付いている。シンボル名が同じならシンボルの指すデータも同じになるシンボルに、DupOKフラグが付いているように思える。例えば、go.string."abczyxv"
シンボルにはDupOKフラグが付いているが、シンボル名が文字列自体を含んでおり、シンボル名が同じなら指すデータも同じになる。
残りのフラグ(LocalとMakeTypeLink)は通常のビルド・実行ではあまり関係なくて、主にsharedモードやpluginモードでビルドしたモジュールを読み込む場合に意味が出てくるのだと思う。Localフラグは、シンボルがELFにおけるローカルシンボルであることを示すフラグ。あくまでELFの話であって、Localなシンボルは他のGo object fileから見えない、という意味ではないようだ。
MakeTypeLinkフラグは、他のライブラリで定義された同一の型をリンクするためのフラグ。ランタイムの初期化時に、フラグが付いている型の同一性を一通りチェックし、型が同じなら型オブジェクトが同じになるようにする。これにより、==
で比較するだけで型オブジェクトの同一性を確認できる。
Name, Version
Nameはシンボル名を示す。""
はリンク時にパッケージ名に置き換えられる。Versionはシンボルのスコープを決める値のようだけど、あまり使われているのを見たことがない。通常は0でグローバルスコープ。
Gotype
Gotypeはシンボルが変数の場合に、変数の型を示す。
まとめ
Goのオブジェクトファイルのフォーマットのうち、定義済みシンボルについて調べた。あまり役に立ちそうにないけど、Goにおける型の扱いやGC処理の実装などを知るきっかけになって良いと思った。
C言語でclosureを実現する
C言語は、仕様上closureをサポートしていない。GCC拡張によるサポートはあるけど、clangではエラーになる。ただライブラリレベルでclosureの機能を実現することはできるようで、libffcallというライブラリがその機能を提供している。この記事では、libffcallを使ってみた感触と、libffcallを参考に簡易closureライブラリを実装してみた感想をメモしておく。
libffcallを使ってみる
libffcallは名前の通りforeign functionを呼ぶためのライブラリで、その機能の一つとしてclosureを実現するためのcallbackという関数群を提供している。
例えば以下のように利用する。
#include <callback.h> #include <stdio.h> void add(void *data, va_alist alist) { int arg1 = *(int*)data; va_start_int(alist); int arg2 = va_arg_int(alist); *(int*)data = arg1 + arg2; va_return_int(alist, *(int*)data); } int main() { int in1 = 10, in2 = 10; int (*add1)() = alloc_callback(&add, &in1); int (*add2)() = alloc_callback(&add, &in2); int out1 = add1(1); add2(1); int out2 = add2(1); printf("out1: %d\n", out1); printf("out2: %d\n", out2); free_callback(add1); free_callback(add2); }
実行してみる。
$ gcc -o callback callback_main.c -lcallback $ ./callback out1: 11 out2: 12
alloc_callback()は、環境の準備+関数へのジャンプをする関数を実行時に生成する。この例では、add関数にin1変数をバインドして、add1という関数を生成している。add1を呼ぶと、add関数の引数を準備した上でadd関数を呼ぶ。引数dataにはalloc_callback()に与えたデータが入る。引数alistにはadd1に与えられた引数が入る。add1に与える引数は複数でもいい。戻り値はalist変数を通して返す。
機能面では近いように思うけど、よくあるclosureとは見た目がだいぶ違う。バインドする変数を明示的に指定しているし、その変数を関数の引数として明示的に受け取っている。また、バインドする変数のメモリ管理はしてくれない。やや読みやすいとは言えないコードになってしまうが、仕組みとしては面白いと思った。実行時コード生成には夢がある。
自前で実装してみる
libffcallのclosure周りの実装を見た感じでは、OSとCPUを限定すれば簡易的なものなら実装できそうだったのでやってみた。OSはLinux、CPUはx86_64を想定した。
以下が実装。
実行してみる。
$ gcc -fPIC -shared -o libmycallback.so mycallback.c $ gcc -L. -o mycallback_main mycallback_main.c -lmycallback $ LD_LIBRARY_PATH=. ./mycallback_main sum1: 10 sum2: 15
動いているようだ。最低限の実装なら意外と短い行数で済む。本家libffcallでは、OSごとのページサイズ、mmap/mprotectの違いや、CPUごとの呼び出し規約の違いに地道に対応している。また、上記の自前実装には関数とバインドする変数のアドレスが32 bitsまでという制限があるが、本家には当然そのような制限はない。
まとめ
C言語でclosureを実現する方法として、libffcallによる方法と、自前実装の方法を紹介した。C言語でも(構文はかなり違うけど)機能は近いものができることがわかった。