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

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

このコミットは、Goランタイムにおける変数定義の重複を避けるための変更です。特に、gccgoランタイムと-fno-commonがデフォルトであるDarwin環境(macOS)において、リンカエラーや未定義の動作を防ぐことを目的としています。

コミット

commit 4019d0e4243cea82b033e12da75d49f82419f2cd
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Sat Jan 26 09:57:06 2013 +0800

    runtime: avoid defining the same variable in more than one translation unit
    For gccgo runtime and Darwin where -fno-common is the default.
    
    R=iant, dave
    CC=golang-dev
    https://golang.org/cl/7094061

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

https://github.com/golang/go/commit/4019d0e4243cea82b033e12da75d49f82419f2cd

元コミット内容

runtime: avoid defining the same variable in more than one translation unit
For gccgo runtime and Darwin where -fno-common is the default.

R=iant, dave
CC=golang-dev
https://golang.org/cl/7094061

変更の背景

このコミットの背景には、C言語のコンパイルとリンクにおける「共通ブロック(common block)」の扱い、特にgccgoコンパイラとDarwin(macOS)環境のリンカの挙動が関係しています。

C言語では、初期化されていないグローバル変数や静的変数は、複数の翻訳単位(translation unit、通常は.cファイル)で同じ名前で定義された場合、リンカによって「共通ブロック」として扱われ、最終的に一つの実体としてリンクされることがあります。これは、異なるファイルで同じ名前の変数を定義しても、それが同じ変数として扱われるという便利な機能です。

しかし、特定のコンパイラやリンカの組み合わせ、特にgccgo(Go言語のフロントエンドとしてGCCを使用するもの)や、Darwin環境のリンカがデフォルトで有効にしている-fno-commonオプションを使用する場合、この共通ブロックの挙動が変わります。

-fno-commonオプションは、初期化されていないグローバル変数であっても、それが定義されている翻訳単位ごとに個別のシンボルとして扱われるように強制します。つまり、複数の翻訳単位で同じ名前の変数が定義されていると、リンカはそれらを別々の変数とみなし、最終的に「多重定義(multiple definition)」エラーを引き起こす可能性があります。これは、同じ名前の変数がメモリ上に複数存在することになり、プログラムの動作が予測不能になるため、通常は避けなければなりません。

GoランタイムのCコードは、複数の.cファイルや.gocファイル(GoのランタイムコードでC言語の構文を使用するもの)に分割されており、これらのファイル間でグローバル変数を共有する際に、この「多重定義」の問題が発生する可能性がありました。このコミットは、この問題を解決し、Goランタイムが異なる環境やコンパイラ設定でも正しくビルド・実行されるようにするためのものです。

前提知識の解説

翻訳単位 (Translation Unit)

C/C++言語において、翻訳単位とは、プリプロセッサによって処理された後の単一のソースファイル(通常は.cまたは.cppファイル)と、それがインクルードするすべてのヘッダファイル(.h)を合わせたものです。コンパイラは、この翻訳単位ごとにオブジェクトファイル(.o)を生成します。

リンカ (Linker)

リンカは、コンパイラが生成した複数のオブジェクトファイルと、必要なライブラリを結合して、最終的な実行可能ファイルや共有ライブラリを生成するツールです。リンカの主な役割は以下の通りです。

  • シンボル解決: あるオブジェクトファイルで定義された関数や変数を、別のオブジェクトファイルから参照できるように、それらのアドレスを解決します。
  • 再配置: コード内のアドレス参照を、最終的なメモリ配置に合わせて調整します。
  • 共通ブロックの処理: 複数の翻訳単位で定義された共通ブロック(初期化されていないグローバル変数など)を適切に処理します。

共通ブロック (Common Block)

C言語において、初期化されていないグローバル変数(例: int global_var;)は、デフォルトで「共通ブロック」として扱われることがあります。これは、複数のソースファイルで同じ名前の初期化されていないグローバル変数が定義されていても、リンカがそれらを一つの実体として結合し、メモリ上に一つだけ存在するようにする仕組みです。これにより、プログラマは異なるファイルから同じグローバル変数にアクセスできます。

-fno-commonオプション

GCCコンパイラにおける-fno-commonオプションは、この共通ブロックのデフォルトの挙動を変更します。このオプションが有効な場合、初期化されていないグローバル変数であっても、それが定義されている翻訳単位ごとに個別のシンボルとして扱われます。つまり、複数の翻訳単位で同じ名前の初期化されていないグローバル変数が定義されていると、リンカはそれらを別々の変数とみなし、多重定義エラー(multiple definition of 'variable_name')を発生させます。

このオプションは、特に共有ライブラリの作成時や、厳密なシンボル管理が必要な場合に有用ですが、既存のコードベースが共通ブロックの挙動に依存している場合、ビルドエラーを引き起こす可能性があります。

externキーワード

C言語のexternキーワードは、変数が現在の翻訳単位ではなく、別の翻訳単位で定義されていることをコンパイラに伝えます。これにより、コンパイラはその変数の定義を現在のファイルで探すのではなく、リンカが他のオブジェクトファイルからその定義を見つけることを期待します。externを使用することで、変数の多重定義を防ぎつつ、複数のファイル間で変数を共有できます。

gccgo

gccgoは、Go言語のプログラムをコンパイルするためのGCCフロントエンドです。Go言語のコードをC言語の中間表現に変換し、その後GCCのバックエンドを使用してネイティブコードにコンパイルします。gccgoは、標準のGoコンパイラ(gc)とは異なるコンパイルパスを使用するため、GCCのリンカの挙動やオプション(例: -fno-common)の影響を直接受けます。

Darwin (macOS) のリンカ

Darwin(macOS)のリンカ(ld)は、デフォルトで-fno-commonに似た挙動をします。これは、初期化されていないグローバル変数をデフォルトで共通ブロックとして扱わず、多重定義を厳しくチェックする傾向があることを意味します。そのため、GoランタイムのCコードがDarwin環境でビルドされる際に、このリンカの挙動が問題となる可能性がありました。

技術的詳細

このコミットは、GoランタイムのCコードにおいて、複数の翻訳単位で同じグローバル変数が定義されていることによる問題を解決します。具体的には、以下の変数が対象となっています。

  • runtime·checking
  • runtime·allg
  • runtime·lastg
  • runtime·allm
  • runtime·goos
  • runtime·ncpu

これらの変数は、Goランタイムの様々な部分で共有されるグローバルな状態を表すものです。例えば、runtime·allgはすべてのゴルーチン(goroutine)のリストを、runtime·ncpuはCPUの数を表します。

問題は、これらの変数が複数の.cファイルや.gocファイルで「定義」されているように見えていた点です。C言語では、グローバル変数の宣言と定義は区別されます。

  • 宣言 (Declaration): 変数の名前と型をコンパイラに知らせるもの。メモリを割り当てない。例: extern int x;
  • 定義 (Definition): 変数の名前と型を知らせ、実際にメモリを割り当てるもの。例: int x;

もし複数のファイルで同じ変数を「定義」してしまうと、リンカはメモリ上にその変数を複数作成しようとし、多重定義エラーが発生します。

このコミットの解決策は、これらのグローバル変数のうち、実際にメモリを割り当てる「定義」を一つの翻訳単位(この場合はsrc/pkg/runtime/proc.csrc/pkg/runtime/malloc.goc)に限定し、他の翻訳単位ではexternキーワードを使って「宣言」のみを行うように変更することです。

これにより、リンカはこれらの変数が単一の場所で定義されていることを認識し、多重定義エラーを回避できます。特に、gccgoコンパイラが使用され、かつDarwin環境のように-fno-commonがデフォルトで有効な場合に、この変更が重要となります。

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

このコミットでは、以下の4つのファイルが変更されています。

  1. src/pkg/runtime/malloc.goc
  2. src/pkg/runtime/malloc.h
  3. src/pkg/runtime/proc.c
  4. src/pkg/runtime/runtime.h

それぞれのファイルでの変更点は以下の通りです。

src/pkg/runtime/malloc.goc

--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -19,6 +19,8 @@ package runtime
 #pragma dataflag 16 /* mark mheap as 'no pointers', hiding from garbage collector */
 MHeap runtime·mheap;
 
+int32	runtime·checking;
+
 extern MStats mstats;	// defined in zruntime_def_$GOOS_$GOARCH.go
 
 extern volatile intgo runtime·MemProfileRate;

runtime·checking変数の定義がここに追加されました。これにより、この変数の実体はmalloc.gocで一つだけ存在することになります。

src/pkg/runtime/malloc.h

--- a/src/pkg/runtime/malloc.h
+++ b/src/pkg/runtime/malloc.h
@@ -446,7 +446,7 @@ void	runtime·markallocated(void *v, uintptr n, bool noptr);\n void	runtime·checkallocated(void *v, uintptr n);\n void	runtime·markfreed(void *v, uintptr n);\n void	runtime·checkfreed(void *v, uintptr n);\n-int32	runtime·checking;\n+extern	int32	runtime·checking;\n void	runtime·markspan(void *v, uintptr size, uintptr n, bool leftover);\n void	runtime·unmarkspan(void *v, uintptr size);\n bool	runtime·blockspecial(void*);\

runtime·checkingの定義がextern宣言に変更されました。これにより、malloc.hをインクルードする他のファイルでは、runtime·checkingが外部で定義されている変数として扱われます。

src/pkg/runtime/proc.c

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -24,6 +24,13 @@ static	int32	debug	= 0;\n 
 int32	runtime·gcwaiting;\n 
+G*	runtime·allg;\n+G*	runtime·lastg;\n+M*	runtime·allm;\n+\n+int8*	runtime·goos;\n+int32	runtime·ncpu;\n+\n // Go scheduler\n //\n // The go scheduler's job is to match ready-to-run goroutines (`g's)\

runtime·allg, runtime·lastg, runtime·allm, runtime·goos, runtime·ncpuの定義がここに追加されました。これらの変数の実体はproc.cで一つだけ存在することになります。

src/pkg/runtime/runtime.h

--- a/src/pkg/runtime/runtime.h
+++ b/src/pkg/runtime/runtime.h
@@ -562,15 +562,15 @@ struct Panic\n  */\n extern	String	runtime·emptystring;\n extern	uintptr runtime·zerobase;\n-G*	runtime·allg;\n-G*	runtime·lastg;\n-M*	runtime·allm;\n+extern	G*	runtime·allg;\n+extern	G*	runtime·lastg;\n+extern	M*	runtime·allm;\n extern	int32	runtime·gomaxprocs;\n extern	bool	runtime·singleproc;\n extern	uint32	runtime·panicking;\n extern	int32	runtime·gcwaiting;		// gc is waiting to run\n-int8*	runtime·goos;\n-int32	runtime·ncpu;\n+extern	int8*	runtime·goos;\n+extern	int32	runtime·ncpu;\n extern	bool	runtime·iscgo;\n extern 	void	(*runtime·sysargs)(int32, uint8**);\n extern	uint32	runtime·maxstring;\

runtime·allg, runtime·lastg, runtime·allm, runtime·goos, runtime·ncpuの定義がextern宣言に変更されました。これにより、runtime.hをインクルードする他のファイルでは、これらの変数が外部で定義されている変数として扱われます。

コアとなるコードの解説

このコミットの核心は、C言語のリンケージ規則、特にグローバル変数の定義と宣言の区別を厳密に適用することにあります。

GoランタイムのCコードは、複数のソースファイルに分散しています。これらのファイルは、共通のヘッダファイル(例: malloc.h, runtime.h)をインクルードすることで、ランタイムの内部構造やグローバル変数にアクセスします。

変更前は、一部のグローバル変数が、複数のソースファイルで「定義」されているかのように見えていました。例えば、malloc.hmalloc.gocの両方でint32 runtime·checking;のような記述があった場合、これは両方のファイルでruntime·checkingという変数を定義しようとしているとリンカに解釈される可能性があります。

通常のCコンパイラとリンカの組み合わせでは、初期化されていないグローバル変数は「共通ブロック」として扱われ、複数の定義があっても最終的に一つの実体としてリンクされることが期待されます。しかし、gccgoやDarwin環境のリンカが-fno-commonオプションをデフォルトで有効にしている場合、この共通ブロックの挙動が無効になります。その結果、同じ変数が複数のファイルで定義されていると、リンカはそれを多重定義エラーとして報告します。

このコミットは、この問題を解決するために、以下の原則を適用しています。

  1. 単一定義規則 (One Definition Rule - ODR): C/C++において、関数や変数はプログラム全体で一度だけ定義されなければならないという原則です。このコミットは、この原則をグローバル変数に適用しています。
  2. externキーワードの適切な使用:
    • 変数の実体(メモリ割り当てを伴う定義)は、特定の.cまたは.gocファイル(例: malloc.goc, proc.c)に限定されます。
    • 他のファイルでその変数を使用したい場合は、ヘッダファイル(例: malloc.h, runtime.h)でexternキーワードを使って「宣言」のみを行います。これにより、コンパイラは変数が存在することを知り、リンカがその定義を見つけることを期待します。

例えば、runtime·checkingの場合、src/pkg/runtime/malloc.gocint32 runtime·checking;として定義され、src/pkg/runtime/malloc.hではextern int32 runtime·checking;として宣言されます。これにより、malloc.hをインクルードする他のファイルはruntime·checkingを使用できますが、その定義はmalloc.gocにのみ存在するため、多重定義エラーは発生しません。

この変更により、Goランタイムは、gccgoやDarwin環境のような、より厳密なリンケージ規則を持つ環境でも正しくビルドされ、安定して動作するようになります。これは、Go言語が様々なプラットフォームで動作することを保証するための重要なステップです。

関連リンク

参考にした情報源リンク