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