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

[インデックス 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)によって中間表現に変換され、その後、各アーキテクチャ固有のバックエンド(例: 6g8g5gなど)によってアセンブリコードに変換されます。これらのバックエンドは、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構造体の内部的な表現、特にarraynelcapといったメンバーのオフセットが、offsetofマクロに依存するのではなく、明示的に計算された定数として扱われるように変更することで、ポータビリティを確保しようとしています。

技術的詳細

このコミットの核心は、Goコンパイラが内部的に使用するArray構造体(Goのスライスヘッダに相当)のメンバーオフセットの計算方法を変更した点にあります。

変更前は、offsetof(Array, member)のようにC標準のoffsetofマクロを使用して、arraynelcapといったメンバーのオフセットを取得していました。しかし、この方法はコンパイラやプラットフォームによって生成されるオフセットが異なる可能性があり、ポータビリティの問題を引き起こしていました。

変更後は、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.cbelexinit関数内で初期化されます。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_nelArray_arrayに置き換えられました。
    • bgen関数内でも、offsetof(Array, array)Array_arrayに置き換えられました。
  • src/cmd/6g/gg.h:

    • Array構造体のランタイム表現に関するコメントが追加されました。
    • 新しいグローバル変数Array_array, Array_nel, Array_cap, sizeof_ArrayEXTERN 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.cbelexinit関数で以下のように初期化されます。

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]->widthuint32のサイズを表します。 このコードは、Array構造体が「ポインタ(array)、uint32nel)、uint32cap)」という順序でメモリ上に配置されることを前提とし、それぞれのメンバーが適切なアライメントを持つようにオフセットを計算しています。

src/cmd/gc/go.hからArray構造体のC言語定義が削除されたことは、この構造体がもはやCコンパイラによって解釈されるべきものではなく、Goコンパイラ自身がそのメモリレイアウトを完全に管理するという強い意図を示しています。src/cmd/6g/gg.hに残されたコメントは、Goランタイムが期待するArray構造体の概念的なレイアウトを開発者に伝えるためのものです。

この一連の変更により、Goコンパイラは、異なるCコンパイラやプラットフォーム上でも、Goの配列/スライスのランタイム表現が常に同じメモリレイアウトを持つことを保証できるようになりました。これにより、「portability bug」が修正され、Goプログラムのクロスプラットフォーム互換性が向上しました。

関連リンク

参考にした情報源リンク

  • C言語 offsetof マクロ: https://en.cppreference.com/w/c/language/offsetof
  • Go言語のコンパイラとランタイムの歴史に関する一般的な知識
  • Go言語のSliceの内部構造に関する一般的な知識
  • Go言語のソースコード(特にsrc/cmd/ディレクトリ内のファイル構造)に関する一般的な知識
  • Go言語の初期のコミットメッセージとコード変更の分析