[インデックス 1383] ファイルの概要
このコミットは、Goコンパイラ(特に6g
、AMD64アーキテクチャ向け)におけるポータビリティバグを修正するものです。具体的には、Goの内部的な配列(スライス)のランタイム表現に関するオフセット計算が、offsetof
マクロの使用によりプラットフォーム間で一貫性を欠く可能性があった問題を解決しています。
コミット
commit 6fa74e09736dbe70269ae016dbd05e3fda994965
Author: Ken Thompson <ken@golang.org>
Date: Fri Dec 19 14:04:25 2008 -0800
portability bug
cant assign to closed array
R=r
OCL=21634
CL=21634
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6fa74e09736dbe70269ae016dbd05e3fda994965
元コミット内容
portability bug
cant assign to closed array
R=r
OCL=21634
CL=21634
変更の背景
Go言語の初期開発段階において、コンパイラが生成するコードとランタイムが共有するデータ構造、特に配列やスライス(Goでは「array dope」または「slice header」と呼ばれる)のメモリレイアウトの一貫性は極めて重要でした。このコミット以前は、コンパイラ内部でこれらの構造体のメンバーへのオフセットを計算するためにC言語の標準マクロであるoffsetof
が使用されていました。
しかし、offsetof
マクロは、コンパイラの実装やターゲットアーキテクチャによっては、予期せぬパディングやアライメントの変更により、異なる環境で異なるオフセット値を返す可能性がありました。コミットメッセージにある「portability bug」と「cant assign to closed array」は、このoffsetof
の挙動の不一致が原因で、コンパイラが生成したコードがランタイムの期待するメモリレイアウトと合致せず、配列(スライス)への不正なアクセスや代入が発生していたことを示唆しています。
この問題は、GoのコンパイラがC言語で書かれており、Goのデータ構造をCの構造体として表現していたことに起因します。Goのランタイムは、これらのデータ構造の正確なメモリレイアウトに依存しているため、コンパイラとランタイムの間でレイアウトの不一致が生じると、深刻なバグにつながります。
前提知識の解説
offsetof
マクロ
offsetof
はC言語の標準ライブラリ(<stddef.h>
)で定義されているマクロです。その目的は、構造体(または共用体)の先頭から、指定されたメンバーまでのバイト単位のオフセットを計算することです。
例えば、offsetof(struct MyStruct, member_name)
のように使用します。
このマクロは通常、コンパイル時に定数として評価されますが、構造体のパディングやアライメント規則はコンパイラやターゲットアーキテクチャによって異なる場合があるため、厳密なメモリレイアウトが要求されるクロスプラットフォーム開発では注意が必要です。
Goのコンパイラアーキテクチャ(初期の6g
)
Go言語の初期のコンパイラは、C言語で書かれていました。6g
は、GoコンパイラのAMD64(x86-64)アーキテクチャ向けのバックエンドを指します。Goのソースコードは、まずgc
(Go Compiler frontend)によって中間表現に変換され、その後、各アーキテクチャ固有のバックエンド(例: 6g
、8g
、5g
など)によってアセンブリコードに変換されます。これらのバックエンドは、Goのデータ型をターゲットアーキテクチャのメモリレイアウトにマッピングする役割を担っていました。
Goの配列とスライス(Array Dope / Slice Header)
Go言語におけるスライスは、動的な配列のような振る舞いをしますが、実際には基盤となる配列へのポインタ、要素数(長さ)、そして容量(キャパシティ)の3つの要素から構成される「スライスヘッダ」または「array dope」と呼ばれる構造体です。 この構造体は、Goのランタイムがスライスを効率的に管理するために不可欠であり、そのメモリレイアウトはコンパイラとランタイムの間で厳密に合意されている必要があります。
概念的には、以下のようなC言語の構造体として表現できます(実際のGoの内部実装はこれとは異なる場合がありますが、概念は同じです):
typedef struct {
void* array; // データへのポインタ
int nel; // 要素数 (number of elements)
int cap; // 容量 (capacity)
} Array; // Goのスライスヘッダに相当
このコミットは、このArray
構造体の内部的な表現、特にarray
、nel
、cap
といったメンバーのオフセットが、offsetof
マクロに依存するのではなく、明示的に計算された定数として扱われるように変更することで、ポータビリティを確保しようとしています。
技術的詳細
このコミットの核心は、Goコンパイラが内部的に使用するArray
構造体(Goのスライスヘッダに相当)のメンバーオフセットの計算方法を変更した点にあります。
変更前は、offsetof(Array, member)
のようにC標準のoffsetof
マクロを使用して、array
、nel
、cap
といったメンバーのオフセットを取得していました。しかし、この方法はコンパイラやプラットフォームによって生成されるオフセットが異なる可能性があり、ポータビリティの問題を引き起こしていました。
変更後は、offsetof
の使用を廃止し、代わりにコンパイル時に明示的に計算されたオフセット定数を使用するようにしました。具体的には、src/cmd/6g/gg.h
に以下の新しいグローバル変数が導入されました。
Array_array
:Array
構造体のarray
メンバーへのオフセットArray_nel
:Array
構造体のnel
メンバーへのオフセットArray_cap
:Array
構造体のcap
メンバーへのオフセットsizeof_Array
:Array
構造体全体のサイズ
これらの変数は、src/cmd/6g/align.c
のbelexinit
関数内で初期化されます。rnd
関数(おそらくアライメントを考慮した丸めを行う関数)を使用して、各メンバーのサイズとアライメントに基づいてオフセットが計算されます。これにより、Array
構造体のメモリレイアウトがコンパイラによって厳密に制御され、異なる環境でも一貫したオフセットが保証されるようになります。
また、src/cmd/gc/go.h
からArray
構造体の定義が削除され、src/cmd/6g/gg.h
のコメントとしてその構造が記述されています。これは、Array
構造体がCの構造体としてではなく、Goコンパイラとランタイムが共有する「概念的なメモリレイアウト」として扱われるようになったことを示唆しています。これにより、Cコンパイラのパディングやアライメントの挙動に依存することなく、Goの内部データ構造のレイアウトをGoコンパイラ自身が完全に制御できるようになります。
この変更は、Goのコンパイラが生成するアセンブリコードにおいて、配列やスライスの各要素(データポインタ、長さ、容量)へのアクセスが、常に正しいメモリ位置を指すことを保証します。これにより、クロスプラットフォームでのGoプログラムの安定性と信頼性が向上します。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は以下のファイルに集中しています。
-
src/cmd/6g/align.c
:dowidth
関数内でsizeof(Array)
がsizeof_Array
に置き換えられました。belexinit
関数内で、新しいグローバル変数Array_array
,Array_nel
,Array_cap
,sizeof_Array
が初期化されるようになりました。これらの変数は、rnd
関数を使って計算されたオフセット値で設定されます。
-
src/cmd/6g/cgen.c
:cgen
関数内で、offsetof(Array, array)
がArray_array
に、offsetof(Array, nel)
がArray_nel
に、offsetof(Array, cap)
がArray_cap
に、sizeof(Array)
がsizeof_Array
にそれぞれ置き換えられました。agen
関数内でも同様に、offsetof(Array, nel)
とoffsetof(Array, array)
が対応するArray_nel
とArray_array
に置き換えられました。bgen
関数内でも、offsetof(Array, array)
がArray_array
に置き換えられました。
-
src/cmd/6g/gg.h
:Array
構造体のランタイム表現に関するコメントが追加されました。- 新しいグローバル変数
Array_array
,Array_nel
,Array_cap
,sizeof_Array
がEXTERN int
型で宣言されました。
-
src/cmd/6g/gsubr.c
:oindex
関数とoindex_const
関数内で、offsetof(Array, nel)
がArray_nel
に、offsetof(Array, array)
がArray_array
にそれぞれ置き換えられました。
-
src/cmd/gc/go.h
:Array
構造体の定義(typedef struct Array Array; struct Array { ... };
)が完全に削除されました。
-
src/cmd/gc/walk.c
:ascompat
関数にif(issarray(t1)) return 0;
という行が追加されました。これは、静的配列(issarray
)の場合の型互換性チェックに関する変更ですが、直接的なオフセット計算の変更とは異なる文脈のようです。ただし、配列関連の変更の一環として行われた可能性があります。
コアとなるコードの解説
このコミットの主要な変更は、Goの内部的な配列/スライス表現であるArray
構造体のメンバーへのアクセス方法を、C言語のoffsetof
マクロから、Goコンパイラ自身が管理する明示的なオフセット定数へと切り替えた点にあります。
例えば、src/cmd/6g/cgen.c
の以下の変更を見てみましょう。
変更前:
n2.xoffset = offsetof(Array,array);
変更後:
n2.xoffset = Array_array;
ここで、n2.xoffset
は、構造体内のメンバーへのオフセットを表すフィールドです。変更前はoffsetof
マクロがこのオフセットを計算していましたが、これはCコンパイラの挙動に依存していました。変更後は、Array_array
というグローバル変数がその役割を担います。
このArray_array
などの変数は、src/cmd/6g/align.c
のbelexinit
関数で以下のように初期化されます。
Array_array = rnd(0, types[tptr]->width);
Array_nel = rnd(Array_array+types[tptr]->width, types[TUINT32]->width);
Array_cap = rnd(Array_nel+types[TUINT32]->width, types[TUINT32]->width);
sizeof_Array = rnd(Array_cap+types[TUINT32]->width, maxround);
ここで、rnd
関数は、指定されたオフセットとアライメントに基づいて、次の有効なオフセットを計算するユーティリティ関数です。types[tptr]->width
はポインタのサイズ、types[TUINT32]->width
はuint32
のサイズを表します。
このコードは、Array
構造体が「ポインタ(array
)、uint32
(nel
)、uint32
(cap
)」という順序でメモリ上に配置されることを前提とし、それぞれのメンバーが適切なアライメントを持つようにオフセットを計算しています。
src/cmd/gc/go.h
からArray
構造体のC言語定義が削除されたことは、この構造体がもはやCコンパイラによって解釈されるべきものではなく、Goコンパイラ自身がそのメモリレイアウトを完全に管理するという強い意図を示しています。src/cmd/6g/gg.h
に残されたコメントは、Goランタイムが期待するArray
構造体の概念的なレイアウトを開発者に伝えるためのものです。
この一連の変更により、Goコンパイラは、異なるCコンパイラやプラットフォーム上でも、Goの配列/スライスのランタイム表現が常に同じメモリレイアウトを持つことを保証できるようになりました。これにより、「portability bug」が修正され、Goプログラムのクロスプラットフォーム互換性が向上しました。
関連リンク
- Go言語の初期のコミット履歴: https://github.com/golang/go/commits?author=ken%40golang.org&after=2008-12-19 (Ken Thompson氏の2008年12月19日以降のコミット)
- Go言語のSlice Headerに関する解説(一般的な情報): https://go.dev/blog/slices-intro (Go公式ブログのスライス入門)
参考にした情報源リンク
- C言語
offsetof
マクロ: https://en.cppreference.com/w/c/language/offsetof - Go言語のコンパイラとランタイムの歴史に関する一般的な知識
- Go言語のSliceの内部構造に関する一般的な知識
- Go言語のソースコード(特に
src/cmd/
ディレクトリ内のファイル構造)に関する一般的な知識 - Go言語の初期のコミットメッセージとコード変更の分析