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

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

このコミットは、Goコンパイラ(cmd/gcおよびcmd/6g)における、非常に大きなスタックフレームの処理に関するバグ修正を目的としています。具体的には、スタックサイズや引数の最大サイズを管理する内部変数の型をint32からvlong(64ビット整数型)に変更することで、32ビット環境での制限を超えた大きなスタック割り当てに対応しています。これにより、特に64ビットシステム上で4GBに近いサイズの配列をスタックに確保しようとした際に発生していた「stack frame too large」エラーが適切に処理されるようになります。

コミット

commit d127ed53784bf8a6e376904af163b58a78179dd2
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Fri Jan 18 22:36:43 2013 +0100

    cmd/gc, cmd/6g: fix error on large stacks.
    
    Fixes #4666.
    
    R=golang-dev, daniel.morsing, rsc
    CC=golang-dev
    https://golang.org/cl/7141047

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

https://github.com/golang/go/commit/d127ed53784bf8a6e376904af163b58a78179dd2

元コミット内容

cmd/gc, cmd/6g: fix error on large stacks.

Fixes #4666.

R=golang-dev, daniel.morsing, rsc
CC=golang-dev
https://golang.org/cl/7141047

変更の背景

この変更は、Goコンパイラが非常に大きなスタックフレームを扱う際に発生するバグ(Issue #4666)を修正するために行われました。Go言語では、関数呼び出し時にローカル変数や引数がスタック上に割り当てられます。特定のシナリオ、特に巨大な配列をローカル変数として宣言した場合、その配列がスタック上に確保されることになります。

問題は、コンパイラ内部でスタックサイズや関連するオフセットを計算・管理するためにint32(32ビット符号付き整数)型が使用されていたことにありました。32ビット整数で表現できる最大値は約2GB(2^31 - 1バイト)です。しかし、64ビットシステムでは、理論上はこれよりもはるかに大きなメモリを扱うことが可能です。

ユーザーが4GBに近いサイズの配列(例: [1 << 30]int32、これは1GBのint32要素、つまり4GB)をスタックに確保しようとすると、コンパイラ内部のint32変数がオーバーフローを起こし、誤ったスタックサイズを計算したり、不正確なエラーを報告したりする可能性がありました。このコミットは、この32ビット整数の限界に起因する問題を解決し、より大きなスタック割り当てを正確に処理できるようにすることを目的としています。

前提知識の解説

  • Goコンパイラ (cmd/gc, cmd/6g):
    • cmd/gcはGo言語の主要なコンパイラであり、Goソースコードを機械語に変換します。
    • cmd/6gは、gcコンパイラの64ビットIntelアーキテクチャ(amd64)向けバックエンドです。Goコンパイラは、ターゲットアーキテクチャごとに異なるバックエンドを持っています。
  • スタックフレーム:
    • 関数が呼び出されるたびに、その関数専用のメモリ領域がスタック上に確保されます。これをスタックフレームと呼びます。スタックフレームには、関数のローカル変数、引数、戻りアドレスなどが格納されます。
    • スタックはLIFO(Last-In, First-Out)構造で、関数呼び出しのたびにスタックフレームが積まれ、関数が終了するとそのフレームが解放されます。
  • int32vlong:
    • int32は32ビットの符号付き整数型で、約-20億から+20億までの値を表現できます。
    • vlongはGoコンパイラ内部で使用される型で、通常は64ビットの整数型(int64またはuint64に相当)を指します。64ビット整数は、約-900京から+900京までの非常に大きな値を表現できます。
  • メモリ割り当て:
    • プログラムが実行時にメモリを使用する方法には、主にスタックとヒープの2種類があります。
    • スタック割り当て: コンパイル時にサイズが決定できる、短命なデータ(ローカル変数、関数引数など)に使用されます。高速ですが、サイズに制限があります。
    • ヒープ割り当て: 実行時にサイズが決定される、長命なデータ(動的に確保されるオブジェクトなど)に使用されます。ガベージコレクションによって管理されます。
    • Go言語では、変数がスタックに割り当てられるかヒープに割り当てられるかは、コンパイラのエスケープ解析によって決定されます。このコミットの文脈では、大きな配列がスタックに割り当てられるケースが問題となっています。
  • Issue #4666:
    • GoプロジェクトのIssueトラッカーで報告されたバグです。このコミットが修正対象としている具体的な問題を示します。通常、Issueには問題の詳細な説明、再現手順、議論などが含まれます。

技術的詳細

このコミットの核心は、Goコンパイラがスタックフレームのサイズを計算・管理する際に使用する内部変数のデータ型を拡張した点にあります。

以前のコンパイラでは、以下の重要な変数がint32型で宣言されていました。

  • stkdelta: スタックフレームの圧縮フェーズによって追加されるオフセット。
  • maxarg: 関数呼び出しにおける引数の最大サイズ。
  • stksize: 現在の関数のスタックフレームのサイズ。
  • c, q, odst, osrc (in cgen.c): コード生成に関連するオフセットやサイズ。

これらの変数がint32であるため、スタックフレームのサイズが2GBを超えると、これらの変数がオーバーフローを起こし、負の値になったり、予期せぬ小さな値になったりする可能性がありました。これにより、コンパイラが「stack frame too large」というエラーを誤って報告したり、あるいは本来エラーとすべきでない大きなスタックを誤って許可したりする可能性がありました。

この修正では、これらの変数の型をvlongに変更することで、64ビットの値を扱えるようにしました。これにより、Goコンパイラは最大で約9エクサバイト(2^63 - 1バイト)までのスタックサイズを正確に計算・管理できるようになり、現代の64ビットシステムにおける大容量メモリの利用に対応できるようになりました。

特に、test/fixedbugs/bug385_64.goに追加されたテストケースvar arr [1 << 30]int32は、正確に4GBの配列をスタックに確保しようとするものです。これは、32ビット整数の限界である2GBを大きく超え、かつ64ビットシステムで問題が発生しやすい4GBの境界を狙ったテストであり、この修正が意図した通りに機能することを確認しています。

また、test/fixedbugs/issue4348.go// skipに変更されたことは重要です。これは、Issue #4666の修正によって、以前は特定の振る舞いをしていた(あるいはコンパイラが許容していた)コードが、新しいスタック管理ロジックの下では不正なものとして扱われるようになったことを示唆しています。つまり、この修正は単にエラーを解消するだけでなく、コンパイラのスタック割り当てに関する厳密性を向上させた可能性があります。

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

このコミットで変更された主要なファイルとコードスニペットは以下の通りです。

  1. src/cmd/6g/cgen.c

    --- a/src/cmd/6g/cgen.c
    +++ b/src/cmd/6g/cgen.c
    @@ -1329,7 +1329,7 @@ void
     sgen(Node *n, Node *ns, int64 w)
     {
     	Node nodl, nodr, nodsi, noddi, cx, oldcx, tmp;
    -	int32 c, q, odst, osrc;
    +	vlong c, q, odst, osrc;
     
     	if(debug['g']) {
     		print("\nsgen w=%lld\n", w);
    

    sgen関数内のローカル変数c, q, odst, osrcの型がint32からvlongに変更されました。これらはコード生成プロセスにおけるオフセットやサイズに関連する変数と考えられます。

  2. src/cmd/gc/go.h

    --- a/src/cmd/gc/go.h
    +++ b/src/cmd/gc/go.h
    @@ -320,7 +320,7 @@ struct	Node
     	int32	lineno;
     	int32	endlineno;
     	vlong	xoffset;
    -	int32	stkdelta;	// offset added by stack frame compaction phase.
    +	vlong	stkdelta;	// offset added by stack frame compaction phase.
     	int32	ostk;
     	int32	iota;
     	uint32	walkgen;
    @@ -912,8 +912,8 @@ EXTERN	int	loophack;
     EXTERN	int32	iota;
     EXTERN	NodeList*	lastconst;
     EXTERN	Node*	lasttype;
    -EXTERN	int32	maxarg;
    -EXTERN	int32	stksize;		// stack size for current frame
    +EXTERN	vlong	maxarg;
    +EXTERN	vlong	stksize;		// stack size for current frame
     EXTERN	int32	blockgen;		// max block number
     EXTERN	int32	block;			// current block number
     EXTERN	int	hasdefer;		// flag that curfn has defer statetment
    

    Node構造体内のstkdelta、およびグローバル変数maxargstksizeの型がint32からvlongに変更されました。これらはコンパイラがスタックフレームのサイズと引数の最大サイズを追跡するために使用する最も重要な変数です。

  3. test/fixedbugs/bug385_64.go

    --- a/test/fixedbugs/bug385_64.go
    +++ b/test/fixedbugs/bug385_64.go
    @@ -8,11 +8,17 @@
     // license that can be found in the LICENSE file.
     
     // Issue 2444
    +// Issue 4666: issue with arrays of exactly 4GB.
     
     package main
    -func main() {  // ERROR "stack frame too large"
    +\n+func main() { // ERROR "stack frame too large"\n \tvar arr [1000200030]int32
     \tarr_bkup := arr
     \t_ = arr_bkup
     }\n \n+func F() { // ERROR "stack frame too large"\n+\tvar arr [1 << 30]int32\n+\t_ = arr[42]\n+}\n    ```
    新しいテストケース`func F()`が追加されました。この関数は`[1 << 30]int32`(4GB)という巨大な配列を宣言し、`"stack frame too large"`エラーが発生することを期待しています。これは、修正が正しく機能し、大きなスタック割り当てを検出できることを検証します。
    
    
  4. test/fixedbugs/issue4348.go

    --- a/test/fixedbugs/issue4348.go
    +++ b/test/fixedbugs/issue4348.go
    @@ -1,4 +1,7 @@
    -// compile
    +// skip
    +\n+// NOTE: this test is now skipped because the relevant code\n+// is rejected after fixing issue 4666.\n     
     // Copyright 2012 The Go Authors.  All rights reserved.
     // Use of this source code is governed by a BSD-style
    

    このテストファイルは// compileから// skipに変更され、Issue #4666の修正後にこのテストの関連コードが拒否されるようになったという注釈が追加されました。

コアとなるコードの解説

このコミットの最も重要な変更は、src/cmd/gc/go.hにおけるstkdelta, maxarg, stksizeの型をint32からvlongに変更した点です。

  • stkdelta:

    • Goコンパイラは、スタックフレームの最適化(例えば、不要なパディングの削除やアライメントの調整)を行うことがあります。stkdeltaは、これらの最適化によってスタックフレームのサイズに生じるオフセットを追跡するために使用されます。この値がint32の範囲を超えると、スタックフレームの正確なサイズ計算に問題が生じ、結果として誤ったコードが生成されたり、不正確なエラーが報告されたりする可能性がありました。vlongへの変更により、このオフセットが非常に大きくなっても正確に扱えるようになりました。
  • maxarg:

    • 関数呼び出しにおいて、引数リストが占める最大サイズを記録する変数です。Go言語では、引数もスタック上に配置されることが多いため、非常に多くの引数を持つ関数や、巨大な構造体を引数として渡す関数では、このmaxargint32の限界に達する可能性がありました。vlongへの変更により、より大きな引数リストを持つ関数も適切にコンパイルできるようになりました。
  • stksize:

    • 現在の関数(コンパイル中の関数)のスタックフレーム全体のサイズを保持する変数です。これは、ローカル変数、引数、戻り値、レジスタ退避領域など、スタックフレーム内のすべての要素の合計サイズを表します。この変数がint32の限界に達すると、コンパイラはスタックフレームが大きすぎると誤って判断したり、あるいはスタックオーバーフローの危険性があるにもかかわらずそれを検出できなかったりする可能性がありました。vlongへの変更は、Goプログラムが非常に大きなローカルデータ構造(例: 巨大な配列)をスタックに割り当てることを可能にし、64ビットシステムのリソースを最大限に活用できるようにします。

src/cmd/6g/cgen.cにおけるc, q, odst, osrcの変更も同様に、コード生成の過程で計算されるオフセットやサイズがint32の範囲を超えないようにするためのものです。これらの変数は、特定の命令のオペランドやメモリ参照の計算に使用されるため、その型が不適切だと、生成される機械語コードが不正になる可能性があります。

これらの変更は、Goコンパイラが64ビットシステム上でより堅牢に、かつ大規模なメモリ割り当てを効率的に扱えるようにするための重要な基盤となります。

関連リンク

参考にした情報源リンク

  • Go言語のIssueトラッカー (GitHub): https://github.com/golang/go/issues
  • Go言語のGerritコードレビューシステム: https://go-review.googlesource.com/
  • Go言語のコンパイラに関するドキュメントやソースコード(src/cmd/gcsrc/cmd/6gディレクトリ内)
  • 一般的なコンピュータアーキテクチャにおけるスタックとメモリ管理に関する情報
  • 32ビットと64ビット整数の表現範囲に関する情報