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

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

このコミットは、Go言語のランタイムコードをGCCコンパイラでより容易にコンパイルできるようにするための調整を含んでいます。具体的には、非ASCII文字を含むシンボル名のマクロによる置換、符号なし整数との比較に関連する変数の型変更、およびvoid*ポインタをより具体的なMLink*型に修正する変更が行われています。これらの変更は、Goの初期開発段階におけるコンパイラ互換性の問題を解決し、コードの堅牢性と可読性を向上させることを目的としています。

コミット

commit 9b8da82d72fa41452c6640fce33a80414f48cfca
Author: Ian Lance Taylor <iant@golang.org>
Date:   Tue Jan 13 09:55:24 2009 -0800

    Tweak code to make it easier to compile with gcc.
      + Use macros to name symbols with non-ASCII characters.
      + Make some variables unsigned, because they are compared
        against unsigned values.
      + Fix a few void* pointers to be MLink*.
    
    R=rsc
    DELTA=94  (44 added, 3 deleted, 47 changed)
    OCL=22303
    CL=22638

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

https://github.com/golang/go/commit/9b8da82d72fa41452c6640fce33a80414f48cfca

元コミット内容

このコミットは、Go言語のランタイムコードがGCCでコンパイルされる際の互換性を向上させるためのコード調整を行います。主な変更点は以下の通りです。

  1. 非ASCII文字を含むシンボル名のマクロ化: sys·のような非ASCII文字(中点)を含むシンボル名を、GCCでコンパイル可能にするためにマクロを使用してsys_のようなASCII文字のみのシンボル名に置き換えます。
  2. 変数の符号なし型への変更: 符号なしの値と比較される一部の変数をunsigned型に変更し、型不一致による警告や潜在的なバグを回避します。
  3. void*ポインタのMLink*への修正: 汎用ポインタであるvoid*を、メモリ管理のリンクリストで使用されるMLink*というより具体的な型に修正し、コードの型安全性を向上させます。

変更の背景

Go言語は、その初期段階において、ランタイムの大部分がC言語で記述されており、GCCなどの既存のCコンパイラを使用してコンパイルされていました。このコミットが行われた2009年1月は、Go言語がまだ一般に公開される前の非常に初期の段階であり、コンパイラツールチェーンの成熟度が現在のようには高くありませんでした。

当時のGCCは、識別子に非ASCII文字(特に·のような特殊文字)を使用することに対して、現在ほど寛容ではなかった可能性があります。Goのランタイムコードでは、システムコールや内部関数にsys·memclrのような命名規則が用いられていましたが、これがGCCでのコンパイルエラーや警告の原因となっていたと考えられます。

また、C言語における符号付き整数と符号なし整数の比較は、予期せぬ結果を招く可能性がある既知の問題です。特に、符号なし整数は常に非負であるため、符号付き整数との比較では、符号付き整数が負の値であっても符号なし整数に昇格されて比較されるため、論理的な誤りにつながることがあります。このコミットは、このような潜在的な問題を解消し、より堅牢なコードベースを構築することを目指しています。

void*の使用はC言語では一般的ですが、型安全性の観点からは推奨されません。特定のデータ構造(この場合はメモリ管理のリンクリスト)を扱うポインタがvoid*として宣言されていると、コンパイラが型チェックを十分に行えず、誤った型のデータが渡された場合に実行時エラーにつながる可能性があります。MLink*のような具体的な型に修正することで、コンパイラによる厳密な型チェックが可能になり、開発時のエラー発見に役立ちます。

これらの変更は、Goランタイムの基盤を安定させ、異なるコンパイラ環境での互換性を確保し、将来のGo言語の発展のための強固な土台を築く上で重要なステップでした。

前提知識の解説

1. GCCとGoの初期コンパイル環境

Go言語は、Googleによって開発されたプログラミング言語です。その初期のバージョンでは、ランタイム(Goプログラムの実行を支える低レベルなコード群)の多くがC言語で書かれていました。これは、Goコンパイラ自体がまだ完全に自己ホスト型ではなかったため、既存のCコンパイラ(主にGCC)に依存してランタイムをビルドする必要があったためです。

2. 非ASCII文字の識別子とC言語

C言語の標準では、識別子(変数名、関数名など)に使用できる文字に制限があります。伝統的に、識別子は英数字とアンダースコア(_)で構成されることが一般的でした。しかし、C99標準以降、Unicode文字を識別子に含めることが可能になりました。ただし、これはコンパイラの実装に依存する部分が大きく、特に古いGCCのバージョンでは、·(中点)のような非ASCII文字が識別子に含まれていると、コンパイルエラーや警告が発生する可能性がありました。

このコミットでは、Goランタイムの内部でsys·memclrのように中点(·)を含む関数名が使われていました。これはGoの初期のコンパイラ(6g, 8gなど)が独自に解釈するシンボル命名規則の一部だった可能性がありますが、GCCでは問題となるため、sys_memclrのようにアンダースコア(_)に置き換える必要がありました。

3. 符号付き/符号なし整数の比較

C言語では、整数型にはint(符号付き整数)とunsigned int(符号なし整数)があります。これらを比較する際、C言語の標準では「通常の算術変換 (usual arithmetic conversions)」というルールが適用されます。このルールの一つに、一方のオペランドが符号なし型で、もう一方が符号付き型の場合、符号付きオペランドが符号なし型に変換されてから比較が行われるというものがあります。

例えば、int a = -1; unsigned int b = 1; の場合、a < btrue になることを期待しますが、aunsigned int に変換されると、そのビットパターンが非常に大きな正の数として解釈されるため、a < bfalse になってしまうことがあります。このような挙動は、特にループカウンタや配列のインデックスなどで予期せぬバグを引き起こす可能性があります。

4. void*と型安全性

void*はC言語における汎用ポインタ型です。あらゆる型のデータへのポインタを保持できますが、その指し示すデータの型に関する情報を持たないため、逆参照する際には明示的な型キャストが必要です。これは非常に柔軟ですが、同時に型安全性を損なう可能性があります。

例えば、void*として宣言されたポインタに誤った型のデータへのアドレスが代入されても、コンパイラは警告を発しません。その結果、実行時に誤った型のデータとしてアクセスされ、クラッシュや未定義動作につながる可能性があります。特定のデータ構造(例: リンクリストのノード)を指すことが分かっている場合は、MLink*のような具体的な型を使用することで、コンパイラがより厳密な型チェックを行い、開発段階でエラーを発見しやすくなります。

5. Goランタイム

Goランタイムは、Goプログラムの実行を管理する低レベルなコードの集合体です。これには、ガベージコレクション、スケジューラ、メモリ割り当て、システムコールラッパーなどが含まれます。Goプログラムが実行される際、これらのランタイム機能がOSと連携し、プログラムの動作を支えます。初期のGoランタイムはC言語で書かれていましたが、後にGo言語自体で書き直されていきました。

技術的詳細

このコミットは、GoランタイムのCコードにおけるGCCとの互換性問題を解決するために、以下の3つの主要な技術的変更を導入しています。

1. 非ASCII文字シンボル名のマクロによる置換

Goの初期ランタイムでは、sys·memclrmalloc·Allocのように、関数名や変数名に中点(·)を含む命名規則が採用されていました。この中点はASCII文字セットには含まれないため、当時のGCCコンパイラがこれを識別子として適切に処理できない問題がありました。

この問題を解決するため、コミットではsrc/runtime/runtime.hに以下の形式のマクロ定義を追加しています。

#ifndef __GNUC__
#define sys_exit sys·exit
#define sys_gosched sys·gosched
// ... 他の多くのシンボル
#endif

この#ifndef __GNUC__プリプロセッサディレクティブは、「GCCコンパイラでない場合」に続くマクロ定義を有効にするという意味です。これにより、GCC以外のコンパイラ(例えば、Go自身のコンパイラツールチェーン)でコンパイルする際には、sys_exitというコードがsys·exitとして解釈されます。

一方で、GCCでコンパイルする際には、__GNUC__が定義されているため、これらのマクロは無視されます。その代わり、コード内のsys·exitのような呼び出しは、直接sys_exitに書き換えられています。

例えば、src/runtime/malloc.cでは、sys·memclrの呼び出しがsys_memclrに、sys·mmapの呼び出しがsys_mmapに直接変更されています。

--- a/src/runtime/malloc.c
+++ b/src/runtime/malloc.c
@@ -83,7 +83,7 @@ free(void *v)
  		if(sizeclass == 0) {
  			// Large object.
  			mstats.alloc -= s->npages<<PageShift;
- 			sys·memclr(v, s->npages<<PageShift);
+ 			sys_memclr(v, s->npages<<PageShift);
  			MHeap_Free(&mheap, s);
  			return;
  		}

そして、src/runtime/runtime.h内の関数宣言も、sys·goexitからsys_goexitのように変更されています。

--- a/src/runtime/runtime.h
+++ b/src/runtime/runtime.h
@@ -343,44 +343,80 @@ void	noteclear(Note*);
 void	notesleep(Note*);
 void	notewakeup(Note*);
 
+/*
+ * Redefine methods for the benefit of gcc, which does not support
+ * UTF-8 characters in identifiers.
+ */
+#ifndef __GNUC__
+#define sys_exit sys·exit
+// ...
+#endif
+
 /*
  * low level go -called
  */
-void	sys·goexit(void);
-void	sys·gosched(void);
-void	sys·exit(int32);
-void	sys·write(int32, void*, int32);
-void	sys·breakpoint(void);
-uint8*	sys·mmap(byte*, uint32, int32, int32, int32, uint32);
-void	sys·memclr(byte*, uint32);
-void	sys·setcallerpc(void*, void*);
-void*	sys·getcallerpc(void*);
+void	sys_goexit(void);
+void	sys_gosched(void);
+void	sys_exit(int32);
+void	sys_write(int32, void*, int32);
+void	sys_breakpoint(void);
+uint8*	sys_mmap(byte*, uint32, int32, int32, int32, uint32);
+void	sys_memclr(byte*, uint32);
+void	sys_setcallerpc(void*, void*);
+void*	sys_getcallerpc(void*);

このアプローチにより、単一のソースコードベースで、GCCとGo独自のコンパイラの両方に対応できるようになっています。

2. 変数の符号なし型への変更

src/runtime/malloc.csrc/runtime/msize.cでは、一部のループカウンタやインデックス変数がint32からuint32に変更されています。

--- a/src/runtime/malloc.c
+++ b/src/runtime/malloc.c
@@ -195,7 +195,7 @@ mal(uint32 n)
 
  	if(0) {
  		byte *p;
- 		int32 i;
+ 		uint32 i;
  		p = v;
  		for(i=0; i<n; i++) {
  		 	if(p[i] != 0) {
--- a/src/runtime/msize.c
+++ b/src/runtime/msize.c
@@ -57,7 +57,8 @@ SizeToClass(int32 size)
 void
 InitSizes(void)
 {
- 	int32 align, sizeclass, size, i, nextsize, n;
+ 	int32 align, sizeclass, size, nextsize, n;
+ 	uint32 i;
  	uintptr allocsize, npages;

これは、これらの変数が符号なしの値(例えば、サイズやインデックス)と比較される際に、C言語の「通常の算術変換」ルールによって発生する可能性のある警告や、論理的な誤りを回避するためです。uint32にすることで、変数が常に非負の値として扱われることが保証され、コードの意図がより明確になり、潜在的なバグが減少します。

3. void*ポインタのMLink*への修正

src/runtime/malloc.hsrc/runtime/mcentral.cでは、メモリ管理に関連するリンクリストのポインタがvoid*からMLink*に変更されています。

--- a/src/runtime/malloc.h
+++ b/src/runtime/malloc.h
@@ -208,7 +208,7 @@ struct MSpan
  	MSpan	*prev;		// in a span linked list
  	PageID	start;		// starting page number
  	uintptr	npages;		// number of pages in span
- 	void	*freelist;	// list of free objects
+ 	MLink	*freelist;	// list of free objects
  	uint32	ref;		// number of allocated objects in this span
  	uint32	sizeclass;	// size class
  	uint32	state;		// MSpanInUse or MSpanFree
--- a/src/runtime/mcentral.c
+++ b/src/runtime/mcentral.c
@@ -92,7 +92,7 @@ MCentral_Alloc(MCentral *c)
 // The objects are linked together by their first words.
 // On return, *pstart points at the first object and *pend at the last.
 void
-MCentral_FreeList(MCentral *c, int32 n, void *start)
+MCentral_FreeList(MCentral *c, int32 n, MLink *start)
 {
  	MLink *v, *next;

MLinkは、Goランタイムのメモリ管理において、解放されたオブジェクトを連結するためのリンクリストのノードとして使用される構造体です。void*を使用すると、コンパイラはfreelistが指すデータの型を認識できず、誤った操作が行われる可能性があります。MLink*に明示的に型を指定することで、コンパイラはより厳密な型チェックを実行でき、コードの型安全性が向上し、デバッグが容易になります。これは、メモリ管理のようなクリティカルな部分において特に重要です。

これらの変更は、GoランタイムのCコードの品質と移植性を向上させるための、初期段階における重要な改善点を示しています。

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

1. 非ASCII文字シンボル名のマクロ化と使用箇所の変更

src/runtime/runtime.hでのマクロ定義の追加:

--- a/src/runtime/runtime.h
+++ b/src/runtime/runtime.h
@@ -343,44 +343,80 @@ void	noteclear(Note*);
 void	notesleep(Note*);
 void	notewakeup(Note*);
 
+/*
+ * Redefine methods for the benefit of gcc, which does not support
+ * UTF-8 characters in identifiers.
+ */
+#ifndef __GNUC__
+#define sys_exit sys·exit
+#define sys_gosched sys·gosched
+#define sys_memclr sys·memclr
+#define sys_write sys·write
+#define sys_breakpoint sys·breakpoint
+#define sys_bytestorune sys·bytestorune
+#define sys_catstring sys·catstring
+#define sys_cmpstring sys·cmpstring
+#define sys_getcallerpc sys·getcallerpc
+#define sys_goexit sys·goexit
+#define sys_indexstring sys·indexstring
+#define sys_intstring sys·intstring
+#define sys_mal sys·mal
+#define sys_mmap sys·mmap
+#define sys_printarray sys·printarray
+#define sys_printbool sys·printbool
+#define sys_printfloat sys·printfloat
+#define sys_printhex sys·printhex
+#define sys_printint sys·printint
+#define sys_printpc sys·printpc
+#define sys_printpointer sys·printpointer
+#define sys_printstring sys·printstring
+#define sys_printuint sys·printuint
+#define sys_readfile sys·readfile
+#define sys_semacquire sys·semacquire
+#define sys_semrelease sys·semrelease
+#define sys_setcallerpc sys·setcallerpc
+#define sys_slicestring sys·slicestring
+#define sys_stringtorune sys·stringtorune
+#endif
+
 /*
  * low level go -called
  */
-void	sys·goexit(void);
-void	sys·gosched(void);
-void	sys·exit(int32);
-void	sys·write(int32, void*, int32);
-void	sys·breakpoint(void);
-uint8*	sys·mmap(byte*, uint32, int32, int32, int32, uint32);
-void	sys·memclr(byte*, uint32);
-void	sys·setcallerpc(void*, void*);
-void*	sys·getcallerpc(void*);
+void	sys_goexit(void);
+void	sys_gosched(void);
+void	sys_exit(int32);
+void	sys_write(int32, void*, int32);
+void	sys_breakpoint(void);
+uint8*	sys_mmap(byte*, uint32, int32, int32, int32, uint32);
+void	sys_memclr(byte*, uint32);
+void	sys_setcallerpc(void*, void*);
+void*	sys_getcallerpc(void*);

src/runtime/malloc.cでのsys·memclrの呼び出し箇所の変更:

--- a/src/runtime/malloc.c
+++ b/src/runtime/malloc.c
@@ -83,7 +83,7 @@ free(void *v)
  		if(sizeclass == 0) {
  			// Large object.
  			mstats.alloc -= s->npages<<PageShift;
- 			sys·memclr(v, s->npages<<PageShift);
+ 			sys_memclr(v, s->npages<<PageShift);
  			MHeap_Free(&mheap, s);
  			return;
  		}

2. 変数の符号なし型への変更

src/runtime/malloc.cでのiの型変更:

--- a/src/runtime/malloc.c
+++ b/src/runtime/malloc.c
@@ -195,7 +195,7 @@ mal(uint32 n)
 
  	if(0) {
  		byte *p;
- 		int32 i;
+ 		uint32 i;
  		p = v;
  		for(i=0; i<n; i++) {
  		 	if(p[i] != 0) {

3. void*ポインタのMLink*への修正

src/runtime/malloc.hでのfreelistの型変更:

--- a/src/runtime/malloc.h
+++ b/src/runtime/malloc.h
@@ -208,7 +208,7 @@ struct MSpan
  	MSpan	*prev;		// in a span linked list
  	PageID	start;		// starting page number
  	uintptr	npages;		// number of pages in span
- 	void	*freelist;	// list of free objects
+ 	MLink	*freelist;	// list of free objects
  	uint32	ref;		// number of allocated objects in this span
  	uint32	sizeclass;	// size class
  	uint32	state;		// MSpanInUse or MSpanFree

コアとなるコードの解説

1. 非ASCII文字シンボル名のマクロ化と使用箇所の変更

src/runtime/runtime.hに追加されたマクロは、Goランタイムが複数のコンパイラ環境でビルドされることを可能にするための巧妙な解決策です。

  • #ifndef __GNUC__: このプリプロセッサディレクティブは、コンパイル時に__GNUC__マクロが定義されているかどうかをチェックします。__GNUC__はGCCコンパイラによって自動的に定義されるマクロです。したがって、このブロック内のコードは「GCC以外のコンパイラでコンパイルされている場合」にのみ有効になります。
  • #define sys_exit sys·exit: この行は、sys_exitという識別子をsys·exitに置き換えるマクロを定義しています。Goの初期のコンパイラは、sys·exitのような中点を含むシンボル名を生成・認識していました。GCC以外のコンパイラでビルドする際には、ソースコード内のsys_exitという記述が、Goコンパイラが期待するsys·exitというシンボル名に変換されます。
  • 関数宣言の変更: void sys·goexit(void); から void sys_goexit(void); への変更は、GCCが非ASCII文字を識別子として扱えない問題に対応するためです。GCCでコンパイルする際には、マクロが有効にならないため、ソースコード自体がGCCが認識できるsys_形式の識別子を使用する必要があります。

これにより、Goのソースコードは、Go独自のツールチェーンとGCCの両方でコンパイルできる「ユニバーサルな」形式を維持しています。

2. 変数の符号なし型への変更

src/runtime/malloc.cmal関数内のi変数の型がint32からuint32に変更された例は、C言語における符号付き/符号なし整数の比較の潜在的な問題を回避するための典型的な修正です。

// 変更前
// int32 i;
// for(i=0; i<n; i++) { ... }

// 変更後
uint32 i;
for(i=0; i<n; i++) { ... }

このiはループカウンタとして使用されており、n(おそらくサイズや要素数を示す符号なしの値)と比較されます。int32iが負の値になった場合(これは通常、ループの条件が誤っているか、非常に大きなnに対してint32がオーバーフローした場合に起こりえます)、i < nの比較がC言語のルールに従って予期せぬ結果を招く可能性があります。uint32にすることで、iが常に非負であることが保証され、nとの比較がより安全かつ直感的になります。これは、コードの堅牢性を高め、デバッグを容易にするための重要な改善です。

3. void*ポインタのMLink*への修正

src/runtime/malloc.hMSpan構造体内のfreelistメンバーと、src/runtime/mcentral.cMCentral_FreeList関数のstart引数の型がvoid*からMLink*に変更されたことは、型安全性の向上を目的としています。

  • MSpan構造体: MSpanはGoのメモリ管理において、連続したページ(メモリブロック)の範囲を表す構造体です。freelistは、このページ範囲内に存在する解放されたオブジェクトを連結するリンクリストの先頭を指します。
  • MLink: MLinkは、Goランタイムのメモリ管理で使用される、リンクリストのノードを表す構造体(または型エイリアス)です。通常、リンクリストの各要素の先頭に配置され、次の要素へのポインタを保持します。
  • 型安全性の向上: void*は任意の型のポインタを保持できますが、その型情報が失われます。そのため、freelistが実際にMLink型のオブジェクトを指しているにもかかわらずvoid*として宣言されていると、コンパイラはfreelistを介した操作(例: freelist->nextのようなアクセス)に対して厳密な型チェックを行えません。MLink*に型を明示することで、コンパイラはfreelistMLink型のオブジェクトを指すことを認識し、誤ったアクセスや操作に対してコンパイル時に警告やエラーを発するようになります。これにより、潜在的な実行時エラーを未然に防ぎ、コードの信頼性を高めることができます。

これらの変更は、Goランタイムの低レベルなメモリ管理コードの正確性と保守性を向上させる上で不可欠なものです。

関連リンク

参考にした情報源リンク

  • GCC documentation on identifiers (general information): https://gcc.gnu.org/onlinedocs/
  • C Standard (C99 and later) for Unicode in identifiers: ISO/IEC 9899:1999 (C99)
  • Signed vs Unsigned Integer Comparison in C:
    • Stack Overflow discussions on C signed/unsigned comparison.
  • void* in C and type safety:
    • General C programming best practices and discussions on void*.