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のオブジェクトファイルのリロケーション情報について調べた。相変わらず直接役に立つことはなさそうだけど、コンパイルからリンク、実行までの仕組みを知る良いきっかけになった。