Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 15507] ファイルの概要

このコミットは、Go言語のランタイムにおけるCgo関連のシンボル解決メカニズムの変更に関するものです。特に、runtime/cgoパッケージ内で定義されている共通シンボル(関数ポインタ)のオーバーライド方法を、6cコンパイラでコンパイルされるコードに移動させることで、外部リンカ使用時の問題を解決しています。

コミット

commit 5bffa3b88e0d84e45139a891f25169399bfe10cc
Author: Russ Cox <rsc@golang.org>
Date:   Thu Feb 28 13:54:23 2013 -0800

    runtime/cgo: move common symbol overrides into 6c-compiled code
    
    There are some function pointers declared by 6c in
    package runtime without initialization and then also
    declared in package runtime/cgo with initialization,
    so that if runtime/cgo is linked in, the function pointers
    are non-nil, and otherwise they are nil. We depend on
    this property for implementing non-essential cgo hooks
    in package runtime.
    
    The declarations in package runtime are 6c-compiled
    and end up in .6 files. The declarations in package runtime/cgo
    are gcc-compiled and end up in .o files. Since 6l links the .6
    and .o files together, this all works.
    
    However, when we switch to "external linking" mode,
    6l will not see the .o files, and it would be up to the host linker
    to resolve the two into a single initialized symbol.
    Not all host linkers will do this (in particular OS X gcc will not).
    
    To fix this, move the cgo declarations into 6c-compiled code,
    so that they end up in .6 files, so that 6l gets them no matter what.
    
    R=golang-dev
    CC=golang-dev
    https://golang.org/cl/7440045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/5bffa3b88e0d84e45139a891f25169399bfe10cc

元コミット内容

runtime/cgo: move common symbol overrides into 6c-compiled code

このコミットは、GoランタイムのCgo関連の共通シンボル(関数ポインタ)の定義を、GCCでコンパイルされたCコードから、Goのツールチェインの一部である6cコンパイラでコンパイルされるCコード(Goのランタイムコード)に移動させることを目的としています。これにより、特に「外部リンキングモード」を使用する際のリンカの挙動の違いによる問題を解決します。

変更の背景

Go言語の初期のビルドシステムでは、Goのソースコードはgc(Goコンパイラ)によってコンパイルされ、Cgoで利用されるCコードは6c(GoのCコンパイラ)またはgcc(GNU C Compiler)によってコンパイルされていました。

問題は、runtimeパッケージ(Goのランタイム)とruntime/cgoパッケージ(Cgo関連の機能を提供する)の間で、特定の関数ポインタの宣言と初期化がどのように行われるかにありました。

  1. runtimeパッケージでの宣言: runtimeパッケージでは、Cgoのフック(Cgoが有効な場合にのみ使用される非必須の機能)として機能するいくつかの関数ポインタが、初期化されずに6cによって宣言されていました。これにより、runtime/cgoがリンクされない場合、これらの関数ポインタはnilのままでした。
  2. runtime/cgoパッケージでの初期化: 一方、runtime/cgoパッケージでは、これらの同じ関数ポインタがgccによってコンパイルされたCコード内で初期化されていました。

通常のGoのビルドプロセスでは、Goのリンカである6lが、6cによって生成された.6ファイル(Goのオブジェクトファイル)と、gccによって生成された.oファイル(Cのオブジェクトファイル)を結合していました。この結合プロセスにおいて、gccによって初期化されたシンボルが6cによって宣言されたシンボルを「オーバーライド」する形で解決され、runtime/cgoがリンクされている場合は関数ポインタが非nilとなり、Cgoの機能が有効になるという仕組みでした。

しかし、「外部リンキングモード」に切り替えると、6l.oファイルを直接処理せず、最終的なリンクはホストシステムのリンカ(例: ld)に委ねられます。この際、すべてのホストリンカが、初期化されていないシンボルと初期化されたシンボルを適切に結合し、初期化されたシンボルでオーバーライドするという挙動を保証するわけではありませんでした。特に、OS Xのgcc(実際にはclangが使われることが多い)は、この「共通シンボル」の解決において期待通りの動作をしないことが判明しました。これにより、Cgoの機能が正しく有効にならないという問題が発生しました。

この問題を解決するため、runtime/cgoパッケージ内のCgo関連の関数ポインタの初期化を、gccでコンパイルされるCコードから、6cでコンパイルされるCコード(つまり、Goのツールチェインが直接管理するオブジェクトファイル)に移動させる必要がありました。これにより、6lが常にこれらのシンボルを適切に処理できるようになり、外部リンキングモードでもCgoの機能が正しく動作するようになります。

前提知識の解説

  • Goツールチェイン: Go言語のプログラムをビルドするためのツール群。主要なものに、go buildコマンド、Goコンパイラ(gc)、Goアセンブラ(go tool asm)、Goリンカ(go tool link、以前は6lなどアーキテクチャごとに存在)などがあります。
  • Cgo: GoプログラムからC言語のコードを呼び出す、またはC言語のコードからGoの関数を呼び出すためのGoの機能。Cgoを使用すると、GoとCの間の相互運用が可能になります。
  • 6cコンパイラ: Goの初期のツールチェインに含まれていたCコンパイラ。GoのランタイムやCgo関連のCコードをコンパイルするために使用されていました。現在はgo tool compileの一部として統合されています。生成されるオブジェクトファイルは.6(amd64の場合)のような拡張子を持つことがありました。
  • GCC (GNU C Compiler): 広く使われているC、C++、Objective-Cなどのコンパイラ。Cgoでは、GoのコードからCのライブラリを呼び出す際に、Cのコードをコンパイルするためにgccが使用されることがあります。生成されるオブジェクトファイルは通常.o拡張子を持ちます。
  • 6lリンカ: Goの初期のツールチェインに含まれていたリンカ。Goのオブジェクトファイル(.6など)とCのオブジェクトファイル(.o)を結合して実行可能ファイルを生成していました。現在はgo tool linkに統合されています。
  • 外部リンキングモード (External Linking Mode): Goのビルドモードの一つで、Goのリンカ(go tool link)が最終的な実行可能ファイルの生成をホストシステムのリンカ(例: Linuxのld、macOSのclang)に委ねるモード。Cgoを使用する場合や、C/C++で書かれた共有ライブラリにリンクする場合に利用されます。このモードでは、Goのリンカは中間的なオブジェクトファイルを生成し、それをホストリンカに渡します。
  • 共通シンボル (Common Symbols): リンカの文脈における共通シンボルとは、初期化されていないグローバル変数や、複数のオブジェクトファイルで宣言されているが初期化されていない変数などを指します。リンカはこれらの共通シンボルを解決し、最終的に一つの定義に結合します。リンカによっては、初期化された定義が初期化されていない定義をオーバーライドする挙動が期待されます。
  • 関数ポインタ: 関数を指し示すポインタ変数。C言語では、関数ポインタを介して関数を間接的に呼び出すことができます。Goのランタイムでは、Cgoの機能が有効かどうかによって、特定の関数ポインタがnilであるか、Cgoの機能を提供する関数を指すかが切り替わるように設計されていました。
  • #pragma cgo_static_import: Cgoの特殊なプラグマ(コンパイラ指示子)の一つ。これは、指定されたシンボルが静的にインポートされることをリンカに指示します。このコミットでは、このプラグマを使用して、x_cgo_initなどのCgo関連の関数が、Goのツールチェインによってコンパイルされたコードから参照されるようにしています。

技術的詳細

このコミットの核心は、Goのビルドシステムにおけるリンキングの挙動、特に「共通シンボル」の解決と「外部リンキングモード」の課題に対処することです。

Goのランタイム(runtimeパッケージ)は、Cgoが有効な場合にのみ使用される特定のフック(関数ポインタ)を持っています。これらのフックは、runtimeパッケージ内では初期化されずに宣言されており、runtime/cgoパッケージがリンクされると、そのパッケージ内で初期化された対応する関数ポインタがこれらのフックを「オーバーライド」する形で機能していました。

従来のGoのビルドプロセスでは、GoのCコンパイラである6cruntimeパッケージのCコードをコンパイルして.6ファイルを生成し、gccruntime/cgoパッケージのCコードをコンパイルして.oファイルを生成していました。Goのリンカである6lは、これら.6ファイルと.oファイルを結合する際に、gccによって初期化されたシンボルが6cによって宣言されたシンボルを適切にオーバーライドしていました。これは、6lがGoのツールチェインの一部として、GoのランタイムとCgoの連携を考慮して設計されていたためです。

しかし、「外部リンキングモード」では、6lは中間的なオブジェクトファイルを生成するだけで、最終的なリンクはホストシステムのリンカに委ねられます。ホストリンカは、Goのツールチェインとは独立した挙動をするため、すべてのリンカが「初期化されていない共通シンボルを、初期化された同じ名前のシンボルでオーバーライドする」という挙動を保証するわけではありません。特に、macOSのgcc(実体はclang)は、この挙動が期待通りではないことが知られていました。これにより、外部リンキングモードでビルドされたGoプログラムでCgoの機能が正しく動作しないという問題が発生しました。

この問題を解決するために、コミットでは以下の戦略が取られました。

  1. Cgo関連シンボルの定義場所の変更: _cgo_init, _cgo_malloc, _cgo_free, _cgo_thread_start, _cgo_setenvといったCgo関連の重要な関数ポインタの初期化を、gccでコンパイルされるCファイル(例: gcc_darwin_386.cなど)から、6cでコンパイルされるCファイル(callbacks.cや新設されたsetenv.c)に移動させました。
  2. #pragma cgo_static_importの利用: 移動先のCファイルでは、#pragma cgo_static_import x_cgo_initのように、対応するx_cgo_プレフィックスを持つ関数(実際のCgoの処理を行う関数)を静的にインポートする指示を追加しました。これにより、6cコンパイラと6lリンカが、これらのシンボルをGoのツールチェイン内で適切に解決できるようになります。
  3. 関数ポインタの直接初期化: void (*_cgo_init)(G*) = x_cgo_init;のように、関数ポインタを直接、x_cgo_プレフィックスを持つ関数で初期化しています。これにより、6cによってコンパイルされたオブジェクトファイル内に、初期化された関数ポインタの定義が含まれることになります。

この変更により、6lは常に初期化されたCgo関連の関数ポインタの定義を認識し、外部リンキングモードであってもホストリンカの挙動に依存することなく、Cgoのフックが正しく設定されるようになります。結果として、Goのビルドシステムがより堅牢になり、異なるプラットフォームやリンカ環境下でもCgoが安定して動作するようになりました。

コアとなるコードの変更箇所

このコミットでは、主にsrc/pkg/runtime/cgo/ディレクトリ内の複数のCファイルが変更されています。

追加されたファイル:

  • src/pkg/runtime/cgo/setenv.c:
    • x_cgo_setenv関数の静的インポートと、_cgo_setenv関数ポインタの初期化がここで行われるようになりました。

変更されたファイル:

  • src/pkg/runtime/cgo/callbacks.c:
    • _cgo_init, _cgo_malloc, _cgo_free, _cgo_thread_startといったCgo関連の主要な関数ポインタの宣言と、対応するx_cgo_プレフィックスを持つ関数への初期化が追加されました。
    • これに伴い、#pragma cgo_static_importディレクティブが追加され、これらの関数が静的にインポートされることを示しています。
  • src/pkg/runtime/cgo/gcc_darwin_386.c, gcc_darwin_amd64.c, gcc_freebsd_386.c, gcc_freebsd_amd64.c, gcc_linux_386.c, gcc_linux_amd64.c, gcc_netbsd_386.c, gcc_netbsd_amd64.c, gcc_openbsd_386.c, gcc_openbsd_amd64.c, gcc_windows_386.c, gcc_windows_amd64.c:
    • これらのファイルから、void (*_cgo_init)(G*) = x_cgo_init;のような_cgo_init関数ポインタの初期化行が削除されました。これは、callbacks.cに移動されたためです。
  • src/pkg/runtime/cgo/gcc_freebsd_arm.c, gcc_linux_arm.c, gcc_netbsd_arm.c:
    • これらのファイルから、void (*_cgo_load_gm)(void) = x_cgo_load_gm;void (*_cgo_save_gm)(void) = x_cgo_save_gm;といった_cgo_load_gm_cgo_save_gm関数ポインタの初期化行が削除されました。
    • 同様に、_cgo_init関数ポインタの初期化行も削除されました。
  • src/pkg/runtime/cgo/gcc_setenv.c:
    • void (*_cgo_setenv)(char**) = x_cgo_setenv;の行が削除されました。これは、新設されたsetenv.cに移動されたためです。
    • ファイルの著作権表示が// Copyright 20111から// Copyright 2011に修正されています(タイプミス修正)。
  • src/pkg/runtime/cgo/gcc_util.c:
    • void (*_cgo_malloc)(void*) = x_cgo_malloc;
    • void (*_cgo_free)(void*) = x_cgo_free;
    • void (*_cgo_thread_start)(ThreadStart*) = x_cgo_thread_start;
    • これらの関数ポインタの初期化行が削除されました。これらはcallbacks.cに移動されたためです。

コアとなるコードの解説

このコミットの最も重要な変更は、src/pkg/runtime/cgo/callbacks.cと新設されたsrc/pkg/runtime/cgo/setenv.cに集約されています。

src/pkg/runtime/cgo/callbacks.cの変更点:

// 変更前: これらの関数ポインタの初期化は、各OS/アーキテクチャ固有のgcc_*.cファイルで行われていた。
// 変更後: ここで一元的に初期化される。

#pragma cgo_static_import x_cgo_init
extern void x_cgo_init(G*);
void (*_cgo_init)(G*) = x_cgo_init;

#pragma cgo_static_import x_cgo_malloc
extern void x_cgo_malloc(void*);
void (*_cgo_malloc)(void*) = x_cgo_malloc;

#pragma cgo_static_import x_cgo_free
extern void x_cgo_free(void*);
void (*_cgo_free)(void*) = x_cgo_free;

#pragma cgo_static_import x_cgo_thread_start
extern void x_cgo_thread_start(void*);
void (*_cgo_thread_start)(void*) = x_cgo_thread_start;
  • #pragma cgo_static_import x_cgo_init: このプラグマは、x_cgo_initというシンボルが静的にインポートされることをGoのツールチェイン(特に6cコンパイラと6lリンカ)に指示します。これにより、x_cgo_initがGoのツールチェインによってコンパイルされたオブジェクトファイル内に存在することが保証されます。
  • extern void x_cgo_init(G*);: x_cgo_init関数の外部宣言です。この関数は、Cgoの初期化処理を実際に担当する関数であり、Goのランタイムから呼び出されます。
  • void (*_cgo_init)(G*) = x_cgo_init;: ここが最も重要な変更点です。_cgo_initという関数ポインタが、x_cgo_init関数を指すように初期化されています。この初期化は、6cコンパイラによって処理されるCファイル内で行われるため、生成される.6ファイル(Goのオブジェクトファイル)にこの初期化された関数ポインタの定義が含まれます。
    • これにより、runtimeパッケージで宣言されている初期化されていない_cgo_initシンボルが、runtime/cgoパッケージのこの初期化された定義によって確実にオーバーライドされるようになります。
    • このメカニズムは、Goのリンカ6lが直接処理するため、外部リンキングモードでホストリンカの挙動に依存することなく、Cgoのフックが正しく設定されることを保証します。

同様の変更が、_cgo_malloc, _cgo_free, _cgo_thread_startについても行われています。

src/pkg/runtime/cgo/setenv.cの追加と変更点:

// Copyright 2011 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build darwin freebsd linux netbsd openbsd

#pragma cgo_import_static x_cgo_setenv

void x_cgo_setenv(char**);
void (*_cgo_setenv)(char**) = x_cgo_setenv;
  • このファイルは新しく追加され、_cgo_setenv関数ポインタの初期化を専門に行います。
  • +buildディレクティブにより、このファイルが特定のOS(Darwin, FreeBSD, Linux, NetBSD, OpenBSD)でのみビルドされることが示されています。
  • ここでも#pragma cgo_import_static x_cgo_setenvvoid (*_cgo_setenv)(char**) = x_cgo_setenv;のパターンが使用されており、_cgo_setenv関数ポインタが6cコンパイルされたコード内で初期化されることを保証しています。

これらの変更により、Cgoの重要なフック関数ポインタの初期化が、Goのツールチェインが完全に制御できる領域(6cコンパイルされたコード)に移動され、外部リンキングモードにおけるリンカの挙動の差異による問題を根本的に解決しています。

関連リンク

  • Go言語のCgoに関する公式ドキュメント: https://go.dev/blog/cgo
  • Goのビルドプロセスに関する一般的な情報: https://go.dev/doc/code
  • Goのツールチェインの歴史と進化に関する情報(より深い理解のために)

参考にした情報源リンク