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

[インデックス 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ビットシステムではwidthptrwidthregも8バイト(64ビット)です。

4. rnd 関数

コミットメッセージに登場するrnd関数は、おそらくGoコンパイラの内部で使用される丸め関数です。rnd(value, alignment)のような形式で使われ、valuealignmentの倍数に丸める役割を持っていたと考えられます。今回の文脈では、スタックオフセットを特定の境界に丸めるために使われていました。

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環境での誤動作の原因となっていました。この修正により、lon->xoffsetの値をそのまま使用するようになり、スタックのゼロ初期化範囲が正しく決定されるようになりました。

test/fixedbugs/issue8155.go の追加

このテストファイルは、修正が正しく適用されたことを検証するために追加されました。

  • bad 関数: この関数は、スタック上にポインタの配列([11]*int[1]*int)を確保し、そのアドレスをpに格納します。runtime.GC()を呼び出すことでガベージコレクションを強制し、スタックゼロ初期化の挙動をテストします。もしスタックゼロ初期化が正しく行われていない場合、pが指すメモリ領域にゴミデータが残り、**pをデリファレンスした際に予期せぬ値(またはクラッシュ)が発生する可能性があります。このテストは、bad(false)が呼び出された際に、x11のままであることを期待しています。これは、pnilであるべきであり、**pのデリファレンスが行われないことを意味します。もしスタックが正しくゼロ初期化されていなければ、pnilでない値になり、問題が露呈する可能性があります。

  • 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の役割)
  • コミットメッセージとコードの差分