cgoを使ったCとGoのリンクの裏側 (1)

  • 127
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

cgoを用いるとCのライブラリをGoバイナリにリンクしたり、Goパッケージの一部をCで書いたりできる。更にGo 1.5以降では、GoのパッケージをC用の静的ライブラリまたは動的ライブラリにまとめておいて、Cからリンクすることもできる。

これらの機能はすべてgo buildコマンドに統合されているので、普段は特にcgoを使っていることを意識することは少ない。しかし、pure goのコードのビルドにしたところでその裏側ではコンパイラ、アセンブラ、リンカが走っているわけである。ではcgoの場合をこの水準で見るとどのような処理が行われているのだろうか。
要は、gcc(1)の裏ではcc1, as, collect2なんかが走ってるよね、cgoではどうなってるの? という話が本稿の話題である。

なお、Goのオブジェクトファイルがプラットフォーム独立な(ELFとかではない)フォーマットであることや、Goのパッケージをビルドすると独自メタデータ付きのarアーカイブにまとめられることなどは既知とする。

まず今回はCライブラリをGoにリンクする場合について見てみよう。なお検証はGo 1.6で行っている。

サンプルコード

次のような2つのファイルから成るGoパッケージgithub.com/yugui/cgo-explained/example1を考える。
この程度であれば実際にはファイルを分ける必要はないが、あとのリンクプロセスを見るために意図的にcgoを含まないgoファイルmain.goを作った。

import_example.go
package main

import (
    //#cgo LDFLAGS: -lm
    //#include <math.h>
    "C"
    "fmt"
)

func printSqrt(n int) {
    fmt.Println(C.sqrt(C.double(n)))
}

main.go
package main

func main() {
    printSqrt(10)
}

まあ、ソースコードの依存関係は下記の要領である。
deps.png

ビルドプロセス

上のパッケージをgo build github.com/yugui/cgo-explained/example1とビルドする場合、次のようなプロセスが走る。
各ステップの詳細については後述する。

  1. コード生成
    • go tool cgo import_example.go
  2. Cコードのコンパイル
    • gcc -c SOURCES
  3. Cコードのリンク
    • gcc -o _cgo_.o OBJS
  4. インポート宣言の生成
    • go tool cgo -dynimport ....
  5. Goコードのコンパイル
    • go tool compile -o example1.a -pack GO_FILES
  6. Cコードの再リンク
    • gcc -o _all.o OBJS
  7. Cオブジェクトをアーカイブに追加
    • go tool pack r example1.a _all.o
  8. アーカイブ内のオブジェクトをリンク
    • go tool link -o example example1.a

データフローは次のようになっている。各ファイルについては追々見ていく。

flow.png

コード生成

まずgo buildはパッケージ内の.goファイルのうちcgoを利用しているファイルのみをcgoコマンドに渡す。
上例では対象はimport_example.goだけで、概ね次のようなコマンドが走る。
複数のファイルがcgoを利用している場合はすべてまとめてコマンドライン引数として引き渡す。

$ env CGO_LDFLAGS=-lm go tool cgo -objdir $tmpobjdir \
>  -importpath github.com/yugui/cgo-explained/example1 \
>  import_example.go

このコマンドは$tmpobjdirディレクトリに次のファイルを生成する。

  • _cgo_export.c: 後で登場するが、今回は大した情報は入っていない。
  • _cgo_export.h: 同上
  • _cgo_flags: 環境変数から収集したCGO_LDFLAGSなどを保存する。cgoのstraceやソースを読む限り、保存しただけで誰も使ってない気がひしひしする。
  • _cgo_gotypes.go: Cの関数を呼び出すためのthunkが入っている。
  • _cgo_main.c: 後述の_cgo_.oをリンクするためのdummyに見える。
  • import_example.cgo1.go:
    元のimport_example.goからGo部分を抜き出したもの。Cの型や関数は_cgo_gotypes.goで定義されたものに置き換えられている。
  • import_example.cgo2.c: _cgo_gotypes.goに対応するC側のラッパーがある。 元のimport_example.goimport "C"の前にコメントとして書いたCコード片もここに来るらしい。

なお、このcgoコマンドは内部で更にCプリプロセッサを呼び出してimport "C"のところのCコード片を処理する。これによってGo側で呼び出した関数やGo側で参照した型が実際にはマクロであるケースや、それらが#if節内で宣言されていてもきちんと処理できるわけである。

_cgo_flags
_CGO_CFLAGS=
_CGO_LDFLAGS=-lm
_cgo_gotypes.go
//go:cgo_ldflag "-lm"
// Created by cgo - DO NOT EDIT

package main

import "unsafe"

import _ "runtime/cgo"

import "syscall"

var _ syscall.Errno
func _Cgo_ptr(ptr unsafe.Pointer) unsafe.Pointer { return ptr }

//go:linkname _Cgo_always_false runtime.cgoAlwaysFalse
var _Cgo_always_false bool
//go:linkname _Cgo_use runtime.cgoUse
func _Cgo_use(interface{})
type _Ctype_double float64

type _Ctype_void [0]byte

//go:linkname _cgo_runtime_cgocall runtime.cgocall
func _cgo_runtime_cgocall(unsafe.Pointer, uintptr) int32

/* 中略 */

//go:cgo_import_static _cgo_5df64109ab52_Cfunc_sqrt
//go:linkname __cgofn__cgo_5df64109ab52_Cfunc_sqrt _cgo_5df64109ab52_Cfunc_sqrt
var __cgofn__cgo_5df64109ab52_Cfunc_sqrt byte
var _cgo_5df64109ab52_Cfunc_sqrt = unsafe.Pointer(&__cgofn__cgo_5df64109ab52_Cfunc_sqrt)

func _Cfunc_sqrt(p0 _Ctype_double) (r1 _Ctype_double) {
        _cgo_runtime_cgocall(_cgo_5df64109ab52_Cfunc_sqrt, uintptr(unsafe.Pointer(&p0)))
        if _Cgo_always_false {
                _Cgo_use(p0)
        }
        return
}
_cgo_main.c
int main() { return 0; }
void crosscall2(void(*fn)(void*, int), void *a, int c) { }
void _cgo_wait_runtime_init_done() { }
char* _cgo_topofstack(void) { return (char*)0; }
void _cgo_allocate(void *a, int c) { }
void _cgo_panic(void *a, int c) { }
void _cgo_reginit(void) { }
import_example.cgo1.go
// Created by cgo - DO NOT EDIT

//line /home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go:1
package main
//line /home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go:4

//line /home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go:3
import (
//line /home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go:7
        "fmt"
)
//line /home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go:11

//line /home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go:10
func printSqrt(n int) {
        fmt.Println(_Cfunc_sqrt(_Ctype_double(n)))
}
import_example.cgo2.c
#line 4 "/home/vagrant/go/src/github.com/yugui/cgo-explained/example1/import_example.go"

#include <math.h>

/* (中略) */

void
_cgo_5df64109ab52_Cfunc_sqrt(void *v)
{
        struct {
                double p0;
                double r;
        } __attribute__((__packed__, __gcc_struct__)) *a = v;
        char *stktop = _cgo_topofstack();
        __typeof__(a->r) r = sqrt(a->p0);
        a = (void*)((char*)a + (_cgo_topofstack() - stktop));
        a->r = r;
}

Cコードのコンパイル/リンク

これは通常通りの手順で、先に生成された_cgo_export.c, _cgo_main.c, import_example.cgo2.cを各々コンパイルした後、リンクして実行ファイル_cgo_.oを生成する。import_example.goで指定したLDFLAGS-lmはここでgccに引き渡されているので、無事にsqrt(3)を解決できる。

ここで重要なのはCのリンカにシンボルの解決を丸投げしてローダブルなオブジェクトを作ることである。実行ファイルをビルドしているのはその手段にすぎない。要は一度リンクさせてみてからldd的なことをしたいのだろう。

インポート宣言の生成

上の_cgo_.oを入力として次のようにcgoコマンドを呼び出している。

$ go tool cgo -objdir $tmpobjdir -dynpackage main \
>  -dynimport $tmpobjdir/_cgo_.o -dynout $tmpobjdir/_cgo_import.go

生成されたファイルは次のようである。
どうもGoのリンカに渡すディレクティブのようだ。

_cgo_import.go
package main
//go:cgo_import_dynamic __libc_start_main __libc_start_main#GLIBC_2.2.5 "libc.so.6"
//go:cgo_import_dynamic sqrt sqrt#GLIBC_2.2.5 "libm.so.6"
//go:cgo_import_dynamic _ _ "libm.so.6"
//go:cgo_import_dynamic _ _ "libc.so.6"

Goコードのコンパイル

パッケージ内でcgoを含まない.goファイルと、最初のステップで生成されたGoコードと、先ほど作った_cgo_import.goをまとめてコンパイルする。

$ go tool compile -o example1.a \
>  -p github.com/yugui/cgo-explained/example1 -pack \
>  _cgo_gotypes.go import_example.cgo1.go _cgo_import.go main.go

この時点ではexample1.aの中身は下記の通り

$ ar x example1.a
__.PKGDEF
_go_.o

Cコードの再リンク

先ほどコンパイルしたCのオブジェクトファイルをリンクし直して別のファイル_all.oを作る。

先ほどの_cgo_.oとの違いは_cgo_main.oや依存ライブラリは一切リンクしないことだ。-Wl,-rを指定しているので、ほぼ_cgo_export.oimport_example.cgo2.oを1つのファイルにまとめているだけである。

$ gcc -nostdlib -o _all.o _cgo_export.o import_example.cgo2.o -Wl,-r

Cオブジェクトをアーカイブに追加

_all.oexample.aに追加する。

他のステップと違ってgo buildはサブプロセスを呼び出してはいないようだが、概ね次のようなコマンドに相当するだろう。

$ go tool pack r example1.a $tmpobjdir/_all.o

アーカイブ内のオブジェクトをリンク

さて、最後にアーカイブ内のオブジェクトや依存パッケージのオブジェクトをリンクする。

$ go tool link -o example1 example1.a

このGoリンカはcgoを使っていない場合は自分で最後までバイナリをリンクする。
しかし、今回はC由来の_all.oをリンクするために一度ELFに落としてからgccに丸投げする。 

結果として次のようなサブプロセスが走る。

$ gcc -m64 -gdwarf-2 -o example1 -rdynamic \
>  /tmp/go-link-978656076/go.o /tmp/go-link-978656076/000000.o \
>  /tmp/go-link-978656076/000001.o -lm -g -O2 -lpthread

まとめ

先のデータフローをもう一度見てみよう。

flow.png

go buildはうまくCコンパイラツールチェインを利用していることが分かる。

まずグルーコードを生成する。それからCの世界はCコンパイラ/リンカに任せてすべての依存関係を解決させ、_cgo_.oを作る。次に、_cgo_.oを解析して_cgo_import.goのためのデータを抽出する。
さらに、その後もGoとCは分離したまま、それぞれ1つずつのオブジェクトファイルにまとめる。そして最後に実行ファイルを作る段になって両者をリンクする。ここでもlibmみたいなCの世界とやりとりする必要があるのでライブラリのロードパスやらはすべてCのリンカに丸投げである。

次回予告

以上でめでたく実行ファイルexample1を得られた。次回はGoの関数をCのソースコードが利用する場合について見てみたい。