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処理の実装などを知るきっかけになって良いと思った。