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

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

このコミットは、Goコンパイラ(cmd/gc)において、Native Client (NaCl) のamd64p32アーキテクチャ上で発生していたメソッド値クロージャのメモリ配置に関するバグを修正するものです。具体的には、ポインタのアライメントに関するコンパイラの仮定が、NaCl環境におけるuint64型などのより厳格なアライメント要件と合致しなかったために生じていた問題を解決します。

コミット

commit 6c0bcb1863fbc84447590226911db9baab7a5c97
Author: Russ Cox <rsc@golang.org>
Date:   Tue May 27 23:58:36 2014 -0400

    cmd/gc: fix method value closures on nacl amd64p32
    
    The code was assuming that pointer alignment is the
    maximum alignment, but on NaCl uint64 alignment is
    even more strict.
    
    Brad checked in the test earlier today; this fixes the build.
    
    Fixes #7863.
    
    TBR=iant
    CC=golang-codereviews
    https://golang.org/cl/98630046

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

https://github.com/golang/go/commit/6c0bcb1863fbc84447590226911db9baab7a5c97

元コミット内容

このコミットは、Goコンパイラ(cmd/gc)がNative Client (NaCl) のamd64p32アーキテクチャ上でメソッド値クロージャを生成する際に発生するバグを修正します。このバグは、コンパイラが「ポインタのアライメントがそのアーキテクチャにおける最大のアライメントである」と誤って仮定していたことに起因します。しかし、NaCl環境では、uint64型のような特定のデータ型がポインタよりもさらに厳格なアライメント要件(例えば8バイトアライメント)を持つ場合があり、この仮定が破綻していました。

この問題は、Bradによって追加された新しいテストによって露見し、ビルドが失敗するようになりました。本コミットは、このビルドエラーを修正し、NaCl amd64p32環境でのメソッド値クロージャの正しい動作を保証します。関連するIssueは#7863です。

変更の背景

Go言語では、obj.Methodのような「メソッド値」が呼び出される際に、レシーバ(obj)をキャプチャする特殊なクロージャが内部的に生成されます。このクロージャは、レシーバのポインタとメソッドのコードポインタを保持する小さな構造体としてメモリに配置されます。

Goコンパイラは通常、メモリ上のデータ配置を最適化し、CPUが効率的にアクセスできるようにアライメントを考慮します。多くの場合、ポインタのサイズ(例えば32ビット環境では4バイト、64ビット環境では8バイト)が、そのアーキテクチャにおける最大のアライメント要件と一致すると仮定されます。つまり、ポインタが4バイト境界に配置されていれば、他のほとんどのデータ型も問題なくアクセスできる、という考え方です。

しかし、Native Client (NaCl) のamd64p32環境は特殊なケースでした。amd64p32は、64ビットのx86アーキテクチャ上で動作しながら、ポインタが32ビット幅で扱われる環境です。このような環境では、ポインタ自体は4バイトアライメントで十分ですが、uint64のような8バイトのデータ型は、そのサイズのために8バイト境界に厳密にアライメントされる必要がある場合があります。

従来のGoコンパイラのコードは、メソッド値クロージャ内のレシーバのオフセットを、ポインタのサイズ(widthptr、この環境では4バイト)に合わせて設定していました。このため、もしレシーバが内部にuint64のような8バイトアライメントを必要とするフィールドを持つ構造体であった場合、クロージャ内のレシーバの開始アドレスが8バイト境界にアライメントされず、不正なメモリアクセスやクラッシュを引き起こす可能性がありました。

このアライメントの不一致によるバグは、Bradが追加した新しいテストによって顕在化しました。このテストがビルドを失敗させたため、Goコンパイラチームは緊急にこの問題を修正する必要がありました。

前提知識の解説

Goのメソッド値とクロージャ

Go言語において、構造体やインターフェースのメソッドは、その型に関連付けられた関数です。例えば、type MyStruct struct { ... }func (m MyStruct) MyMethod() { ... }という定義があったとします。

myObj := MyStruct{...}というインスタンスがある場合、myObj.MyMethodという式は、MyMethodという関数と、そのレシーバであるmyObjを「結合」した特殊な関数値を生成します。これを「メソッド値 (method value)」と呼びます。このメソッド値は、Goの内部では「クロージャ (closure)」として実装されます。

具体的には、メソッド値は以下のような構造を持つ小さなデータブロックとしてメモリに配置されます。

  1. レシーバのポインタ: myObjへのポインタ。
  2. メソッドのコードポインタ: MyMethod関数の実際のコードへのポインタ。

このクロージャが呼び出されると、内部にキャプチャされたレシーバに対してメソッドが実行されます。

メモリのアライメント

メモリのアライメントとは、コンピュータのメモリ上でデータが配置される際、特定のメモリアドレス境界に配置される必要があるという規則のことです。CPUは、特定のデータ型(例: 32ビット整数、64ビット浮動小数点数、ポインタ)を効率的かつ正しく読み書きするために、そのデータが特定のバイト数の倍数のアドレス(例えば、4バイトデータは4の倍数のアドレス、8バイトデータは8の倍数のアドレス)に配置されていることを期待します。

  • アライメントの重要性:
    • パフォーマンス: CPUは、アライメントされたデータにアクセスする方が高速です。アライメントされていないデータへのアクセスは、複数のメモリアクセスが必要になったり、特別なハードウェア処理が必要になったりするため、パフォーマンスが低下します。
    • 正確性: 一部のアーキテクチャでは、アライメントされていないデータへのアクセスがハードウェア例外(バスエラー、アライメントフォルトなど)を引き起こし、プログラムがクラッシュする原因となります。
    • ポータビリティ: 異なるアーキテクチャ間でのコードの移植性を確保するためにも、アライメントの規則を理解し、適切に処理することが重要です。

Goコンパイラ (gc)

gcは、Go言語の公式なコンパイラツールチェーンの一部です。Goのソースコードを解析し、中間表現に変換し、最終的にターゲットアーキテクチャの機械語コードを生成します。この過程で、gcはGoプログラムのメモリレイアウトを決定し、変数や構造体のフィールドがメモリ上でどのように配置されるかを管理します。これには、各データ型のアライメント要件を考慮し、必要に応じてパディング(隙間)を挿入して、データが正しくアライメントされるようにする役割も含まれます。

Native Client (NaCl) と amd64p32

  • Native Client (NaCl): Googleが開発した、Webブラウザ内でネイティブコードを安全に実行するためのサンドボックス技術です。NaClは、C/C++などの言語で書かれたアプリケーションを、ブラウザのセキュリティサンドボックス内で実行できるように設計されています。これにより、Webアプリケーションがネイティブのパフォーマンスを発揮しつつ、セキュリティ上のリスクを最小限に抑えることができます。NaCl環境は、通常のオペレーティングシステムとは異なる独自のABI(Application Binary Interface)やメモリモデルを持つことがあり、これが特定のアライメント要件の違いにつながることがあります。
  • amd64p32: これは、64ビットのx86アーキテクチャ(AMD64またはIntel 64)上で動作するが、ポインタのサイズが32ビットに制限されている環境を指します。通常のamd64環境ではポインタは64ビット(8バイト)ですが、amd64p32ではポインタが32ビット(4バイト)になります。このような環境は、特定の組み込みシステムや、NaClのようなサンドボックス環境で採用されることがあります。ポインタが32ビットであるにもかかわらず、CPU自体は64ビットのレジスタや命令セットを持つため、uint64のような8バイトのデータ型は、ポインタよりも厳格な8バイトアライメントを要求することがあります。このポインタサイズとデータ型のアライメント要件の不一致が、今回のバグの根本原因となりました。

技術的詳細

Goコンパイラ(gc)のsrc/cmd/gc/closure.cファイルには、Goのメソッド値がクロージャとしてどのように表現され、メモリに配置されるかを決定するロジックが含まれています。特に、makepartialcall関数は、メソッド値クロージャの内部構造を構築する役割を担っています。

メソッド値クロージャは、レシーバのポインタとメソッドのコードポインタを格納するための内部的な構造体として扱われます。この構造体内の各フィールド(ここではレシーバのポインタ)は、適切なメモリアライメントで配置される必要があります。

従来のコードでは、クロージャ内のレシーバ変数のオフセット(cv->xoffset)をwidthptr(ポインタのサイズ、amd64p32では4バイト)に設定していました。これは、Goコンパイラが「ポインタのアライメントがそのアーキテクチャにおける最大のアライメントである」という一般的な仮定に基づいていました。つまり、ポインタが4バイト境界に配置されていれば、その後に続くデータも問題なくアクセスできると考えていたのです。

しかし、NaCl amd64p32環境では、この仮定が破綻しました。この環境では、uint64のような8バイトのデータ型は、8バイト境界に厳密にアライメントされる必要がありました。もしメソッド値クロージャのレシーバが、内部にuint64フィールドを持つ構造体であった場合、そのレシーバ全体も8バイトアライメントを要求します。

従来のコードでは、レシーバのオフセットが4バイトに固定されていたため、レシーバが8バイトアライメントを要求する場合でも、4バイト境界に配置されてしまう可能性がありました。これにより、レシーバ内のuint64フィールドへのアクセスがアライメントされていない状態となり、ハードウェア例外や不正な動作を引き起こす原因となっていました。

このコミットは、makepartialcall関数において、レシーバの型が要求するアライメント(cv->type->align)が、一般的なポインタのアライメント(widthptr)よりも大きい場合に、レシーバのオフセットをそのより厳格なアライメント値に調整することで、この問題を解決しています。

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

--- a/src/cmd/gc/closure.c
+++ b/src/cmd/gc/closure.c
@@ -374,6 +374,8 @@ makepartialcall(Node *fn, Type *t0, Node *meth)
 	cv = nod(OCLOSUREVAR, N, N);
 	cv->xoffset = widthptr;
 	cv->type = rcvrtype;
+	if(cv->type->align > widthptr)
+		cv->xoffset = cv->type->align;
 	ptr = nod(ONAME, N, N);
 	ptr->sym = lookup("rcvr");
 	ptr->class = PAUTO;
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -1438,6 +1438,8 @@ funccompile(Node *n, int isclosure)
 	
 	// record offset to actual frame pointer.
 	// for closure, have to skip over leading pointers and PC slot.
+	// TODO(rsc): this is the old jit closure handling code.
+	// with the new closures, isclosure is always 0; delete this block.
 	nodfp->xoffset = 0;
 	if(isclosure) {
 		NodeList *l;

コアとなるコードの解説

このコミットの核心的な変更は、src/cmd/gc/closure.cmakepartialcall関数に追加された以下の2行です。

	if(cv->type->align > widthptr)
		cv->xoffset = cv->type->align;
  • cvは、メソッド値クロージャ内のレシーバ変数を表すノードです。
  • cv->xoffsetは、クロージャのメモリブロック内における、このレシーバ変数の開始オフセット(バイト単位)を示します。
  • widthptrは、現在のアーキテクチャにおけるポインタのサイズ(バイト単位)を表す定数です。amd64p32環境では4バイトです。
  • cv->type->alignは、レシーバの型(rcvrtype)がメモリ上で要求する最小のアライメント要件(バイト単位)を示します。例えば、レシーバがuint64を含む構造体である場合、この値は8バイトになる可能性があります。

このif文のロジックは以下の通りです。

  1. 条件判定: cv->type->align > widthptr

    • これは、「レシーバの型が要求するアライメントが、一般的なポインタのアライメント(widthptr)よりも大きいか?」をチェックしています。
    • 通常の環境では、ポインタのアライメントが最大アライメントであるため、この条件はほとんどの場合falseになります。
    • しかし、NaCl amd64p32のような特殊な環境で、レシーバがuint64のようなより厳格なアライメント(8バイト)を要求する場合、cv->type->alignが8、widthptrが4となり、この条件がtrueになります。
  2. オフセットの調整: cv->xoffset = cv->type->align;

    • もし上記の条件がtrueであれば、レシーバのオフセット(cv->xoffset)を、その型が要求するより厳格なアライメント値(cv->type->align)に設定し直します。
    • これにより、クロージャ内のレシーバが常にその型が要求する適切なメモリアライメントで配置されるようになり、アライメントの不一致による問題が解消されます。

src/cmd/gc/dcl.cの変更は、古いJITクロージャハンドリングコードに関するTODOコメントの追加のみであり、直接的なバグ修正とは関係ありません。これは、将来的なコードクリーンアップのためのメモです。

この修正により、Goコンパイラは、NaCl amd64p32のような特殊な環境においても、メソッド値クロージャ内のレシーバを正しくアライメントして配置できるようになり、安定した動作が保証されるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (Goのクロージャ、メモリモデルに関する一般的な情報)
  • Native Client (NaCl) の公式ドキュメント (NaClのアーキテクチャとアライメントに関する一般的な情報)
  • x86-64アーキテクチャのABIドキュメント (amd64p32のような特殊なポインタモードに関する情報)
  • Goコンパイラのソースコード (特にsrc/cmd/gc/ディレクトリ内のファイル)