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 - 0x0026iface構造体に格納された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+16rel 32+8 t=1 runtime.gcbits.+0と書いてある。なので、ちょうどポインタを通して他のデータを参照しているところに、R_ADDRタイプのリロケーション指定があることが分かる。

R_ADDROFFタイプ (t=5)

このタイプは、リンク後の実行イメージのうち、moduledataが指す特定領域からのオフセットアドレスを書き込む。moduledataは実行イメージのレイアウトを示す構造体で、例えばtypesというフィールドは型情報などが集められた領域を指す。R_ADDROFFタイプはこのような領域からのオフセットを書き込みたいときに使われる。

型がnameOfftypeOffの場合にこのリロケーション情報が付く。先程の_type構造体では、40バイト目のstrと44バイト目のptrToThisフィールドが該当する。アセンブリ出力を見ると、rel 40+4 t=5 type..namedata.*main.S.+0rel 44+4 t=5 type.*"".S+0と書いてあるので、nameOfftypeOff型のフィールドに対して、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()+0rel 104+4 t=24 "".(*S).M+0rel 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オブジェクトtypesetypesといったフィールドからこれらのデータが格納された領域を取得し、更にそこからのオフセットでデータを取得する。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オブジェクトnoptrdataenoptrdataといったフィールドとオフセットでデータを取得できる。

ポインタの有無でセクションを分けているのは、主に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言語でも(構文はかなり違うけど)機能は近いものができることがわかった。