[インデックス 19504] ファイルの概要
このコミットでは、以下の2つのファイルが変更されました。
src/cmd/6g/ggen.c
: Goコンパイラ(6g、amd64アーキテクチャ向け)のコード生成部分。スタックのゼロ初期化ロジックが修正されました。test/fixedbugs/issue8155.go
: Issue 8155を修正したことを検証するための新しいテストファイル。
コミット
- コミットハッシュ:
ac0e12d15800ac0e5795e823ab0e99c1eb70667b
- Author: Russ Cox rsc@golang.org
- Date: Thu Jun 5 16:40:23 2014 -0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ac0e12d15800ac0e5795e823ab0e99c1eb70667b
元コミット内容
cmd/6g: fix stack zeroing on native client
I am not sure what the rounding here was
trying to do, but it was skipping the first
pointer on native client.
The code above the rounding already checks
that xoffset is widthptr-aligned, so the rnd
was a no-op everywhere but on Native Client.
And on Native Client it was wrong.
Perhaps it was supposed to be rounding down,
not up, but zerorange handles the extra 32 bits
correctly, so the rnd does not seem to be necessary
at all.
This wouldn't be worth doing for Go 1.3 except
that it can affect code on the playground.
Fixes #8155.
LGTM=r, iant
R=golang-codereviews, r, iant
CC=dvyukov, golang-codereviews, khr
https://golang.org/cl/108740047
変更の背景
このコミットは、Goコンパイラ(特に6g
、AMD64アーキテクチャ向け)におけるスタックのゼロ初期化処理のバグを修正するものです。このバグは、特にGoogle Native Client (NaCl) 環境で顕在化し、スタック上の最初のポインタのゼロ初期化がスキップされるという問題を引き起こしていました。
Go言語のガベージコレクタは、スタック上のポインタを正確に識別し、それらが指すメモリを追跡する必要があります。そのため、関数呼び出し時に確保されるスタックフレームの未使用領域、特にポインタを含む可能性のある領域は、ガベージコレクタが誤った値をポインタとして解釈しないように、ゼロで初期化される必要があります(スタックゼロ初期化)。
問題は、スタックゼロ初期化の範囲を決定する際に使用されていたrnd
(おそらく「round」の略)関数による丸め処理にありました。この丸め処理がNative Client環境において誤動作し、本来ゼロ初期化されるべき領域がスキップされてしまっていたのです。コミットメッセージによると、この丸め処理は他の環境では実質的に何もしない(no-op)でしたが、Native Clientでは誤った動作をしていました。
この問題は、Go Playgroundのような環境でも影響を及ぼす可能性があったため、Go 1.3のリリースに向けて修正の優先度が高められました。
前提知識の解説
1. スタックゼロ初期化 (Stack Zeroing)
Go言語のガベージコレクタは、正確なガベージコレクション(Precise GC)を行います。これは、メモリ上のどの値がポインタであり、どの値がポインタでないかを正確に識別できることを意味します。関数が呼び出されると、その関数に必要なローカル変数や引数を格納するためのスタックフレームが確保されます。このスタックフレームには、以前の関数の実行によって残された「ゴミ」データが含まれている可能性があります。
もしこのゴミデータの中に、たまたま有効なポインタのように見える値があった場合、ガベージコレクタがそれをポインタとして追跡し、実際には到達不能なメモリを解放せずに保持してしまう可能性があります。これを防ぐため、Goランタイムは新しいスタックフレームが確保された際に、その領域をゼロで初期化します。これにより、ポインタとして解釈される可能性のある値が確実にnil
(ゼロ値)になり、ガベージコレクタが誤って追跡することを防ぎます。
2. Google Native Client (NaCl)
Google Native Client (NaCl) は、ウェブブラウザ内でC/C++コードを安全に実行するためのサンドボックス技術です。NaClは、特定のCPUアーキテクチャ(x86、ARMなど)向けにコンパイルされたネイティブコードを、ブラウザのセキュリティモデル内で実行できるように設計されています。Go言語もNaClをターゲットとしてコンパイルすることが可能でした。
NaCl環境では、メモリのアドレス空間やポインタの扱いに関して、通常のデスクトップOS環境とは異なる制約や特性を持つことがあります。特に、ポインタのサイズ(32ビットか64ビットか)やアライメント(メモリ上の配置)に関する挙動が、通常の環境と異なる場合があり、これが今回のバグの原因となりました。
3. ポインタのアライメント (Pointer Alignment)
コンピュータのメモリは、バイト単位でアドレス指定されますが、CPUが効率的にデータを読み書きするためには、特定のデータ型が特定のメモリアドレスに配置されている必要があります。これをアライメントと呼びます。例えば、64ビットの整数やポインタは、8バイト境界に配置されていると効率が良い、といった具合です。
Goコンパイラは、スタック上の変数を配置する際に、適切なアライメントを考慮します。widthptr
はポインタの幅(サイズ)、widthreg
はレジスタの幅(サイズ)を意味するGoコンパイラ内部の定数です。通常、64ビットシステムではwidthptr
もwidthreg
も8バイト(64ビット)です。
4. rnd
関数
コミットメッセージに登場するrnd
関数は、おそらくGoコンパイラの内部で使用される丸め関数です。rnd(value, alignment)
のような形式で使われ、value
をalignment
の倍数に丸める役割を持っていたと考えられます。今回の文脈では、スタックオフセットを特定の境界に丸めるために使われていました。
5. zerorange
関数
zerorange
関数は、指定されたメモリ範囲をゼロで埋めるGoランタイムの内部関数です。スタックゼロ初期化の実際の処理はこの関数によって行われます。コミットメッセージにある「zerorange
handles the extra 32 bits correctly」という記述は、zerorange
関数自体は、たとえ丸めによって余分な32ビット(4バイト)が含まれても、正しくゼロ初期化できる能力があることを示唆しています。つまり、問題はzerorange
の呼び出し側、すなわちゼロ初期化する範囲を決定するロジックにあったということです。
技術的詳細
このバグは、src/cmd/6g/ggen.c
内のdefframe
関数、具体的にはスタックフレームのゼロ初期化範囲を決定する部分に存在していました。
問題のコードは以下の部分です。
// merge with range we already have
lo = rnd(n->xoffset, widthreg);
ここでlo
はゼロ初期化を開始するスタックオフセットを示しています。n->xoffset
は現在の変数のスタックオフセット、widthreg
はレジスタの幅(通常8バイト)です。
コミットメッセージによると、このrnd
関数による丸め処理がNative Client環境で問題を引き起こしていました。
- 通常の環境:
xoffset
は既にwidthptr
(ポインタの幅、通常8バイト)にアライメントされているため、rnd(n->xoffset, widthreg)
は実質的にn->xoffset
と同じ値を返していました。つまり、この丸め処理は「no-op」(何もしない操作)でした。 - Native Client環境: Native Clientでは、ポインタのサイズが32ビット(4バイト)であるにもかかわらず、
widthreg
が8バイトとして扱われるなど、アライメントやサイズの計算に差異があった可能性があります。この差異により、rnd
関数が意図しない丸めを行い、スタック上の最初のポインタのゼロ初期化範囲が誤って計算され、スキップされてしまっていたと考えられます。
コミットメッセージの「Perhaps it was supposed to be rounding down, not up」という記述は、rnd
関数が期待される丸め方向(例えば切り捨て)とは異なる丸め(例えば切り上げ)を行っていた可能性を示唆しています。しかし、最終的な結論としては、zerorange
関数が余分な32ビットを正しく処理できるため、そもそもこのrnd
による丸め自体が不要であると判断されました。
つまり、xoffset
が既に適切にアライメントされているため、余計な丸め処理は不要であり、それがNative Client環境で誤動作する原因となっていた、という結論に至ったわけです。
コアとなるコードの変更箇所
--- a/src/cmd/6g/ggen.c
+++ b/src/cmd/6g/ggen.c
@@ -47,7 +47,7 @@ defframe(Prog *ptxt)
if(lo != hi && n->xoffset + n->type->width >= lo - 2*widthreg) {
// merge with range we already have
- lo = rnd(n->xoffset, widthreg);
+ lo = n->xoffset;
continue;
}
// zero old range
--- /dev/null
+++ b/test/fixedbugs/issue8155.go
@@ -0,0 +1,48 @@
+// run
+
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Issue 8155.
+// Alignment of stack prologue zeroing was wrong on 64-bit Native Client
+// (because of 32-bit pointers).
+
+package main
+
+import "runtime"
+
+func bad(b bool) uintptr {
+ var p **int
+ var x1 uintptr
+ x1 = 1
+ if b {
+ var x [11]*int
+ p = &x[0]
+ }
+ if b {
+ var x [1]*int
+ p = &x[0]
+ }
+ runtime.GC()
+ if p != nil {
+ x1 = uintptr(**p)
+ }
+ return x1
+}
+
+func poison() uintptr {
+ runtime.GC()
+ var x [20]uintptr
+ var s uintptr
+ for i := range x {
+ x[i] = uintptr(i+1)
+ s += x[i]
+ }
+ return s
+}
+
+func main() {
+ poison()
+ bad(false)
+}
コアとなるコードの解説
src/cmd/6g/ggen.c
の変更
主要な変更は、defframe
関数内の以下の行です。
- lo = rnd(n->xoffset, widthreg);
+ lo = n->xoffset;
この変更により、スタックゼロ初期化の開始オフセットlo
を計算する際に、rnd
関数による丸め処理が完全に削除されました。コミットメッセージが説明しているように、n->xoffset
は既に適切なアライメントが保証されているため、この丸めは不要であり、Native Client環境での誤動作の原因となっていました。この修正により、lo
はn->xoffset
の値をそのまま使用するようになり、スタックのゼロ初期化範囲が正しく決定されるようになりました。
test/fixedbugs/issue8155.go
の追加
このテストファイルは、修正が正しく適用されたことを検証するために追加されました。
-
bad
関数: この関数は、スタック上にポインタの配列([11]*int
や[1]*int
)を確保し、そのアドレスをp
に格納します。runtime.GC()
を呼び出すことでガベージコレクションを強制し、スタックゼロ初期化の挙動をテストします。もしスタックゼロ初期化が正しく行われていない場合、p
が指すメモリ領域にゴミデータが残り、**p
をデリファレンスした際に予期せぬ値(またはクラッシュ)が発生する可能性があります。このテストは、bad(false)
が呼び出された際に、x1
が1
のままであることを期待しています。これは、p
がnil
であるべきであり、**p
のデリファレンスが行われないことを意味します。もしスタックが正しくゼロ初期化されていなければ、p
がnil
でない値になり、問題が露呈する可能性があります。 -
poison
関数: この関数は、スタック上に大量のuintptr
を確保し、値を書き込むことで、スタックメモリを「汚染」します。これは、bad
関数が実行される前に、スタックにゴミデータを意図的に残すことで、スタックゼロ初期化のテストをより確実に機能させるためのものです。
このテストは、特に「64-bit Native Client (because of 32-bit pointers)」というコメントがあるように、Native Client環境でのポインタサイズの違いに起因するアライメントの問題を狙ったものです。
関連リンク
- Go CL (Code Review) リンク: https://golang.org/cl/108740047
- Go Issue 8155: コミットメッセージに「Fixes #8155」とありますが、古いIssueのため、現在のGoのIssueトラッカーで直接検索しても見つからない可能性があります。しかし、このコミットがその問題を解決したことは明確です。
参考にした情報源リンク
- Go言語のガベージコレクションとスタックゼロ初期化に関する一般的な情報
- Google Native Client (NaCl) に関する一般的な情報
- Goコンパイラの内部構造に関する一般的な情報 (特に
cmd/6g
の役割) - コミットメッセージとコードの差分