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

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

このコミットは、Go 1.3 のリリースノートである doc/go1.3.html に、unsafe.Pointer の厳密な使用に関する重要な注意書きを追加するものです。Go ランタイムがポインタをどのように扱うかについての新しい仮定と、それに違反するコードが引き起こす可能性のある問題(クラッシュやダングリングポインタ)について警告しています。また、go vet ツールがこれらの問題を特定するのに役立つことも言及しています。

コミット

commit 208a1ea564e8b1ce8d6d85a315a410f29d5e952e
Author: Russ Cox <rsc@golang.org>
Date:   Thu May 15 16:16:26 2014 -0400

    doc/go1.3.html: add note about unsafe.Pointer strictness
    
    The vet check is in CL 10470044.
    
    LGTM=bradfitz, r
    R=r, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/91480044

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

https://github.com/golang/go/commit/208a1ea564e8b1ce8d6d85a315a410f29d5e952e

元コミット内容

doc/go1.3.html ファイルに、Go 1.3 からのランタイムの変更点に関する以下の注意書きが追加されました。

  • Go 1.3 以降、ランタイムはポインタ型を持つ値がポインタを含み、その他の値はポインタを含まないと仮定します。
  • この仮定は、スタック拡張とガベージコレクションの正確な動作にとって不可欠です。
  • unsafe パッケージを使用して uintptr をポインタ値に格納するプログラムは不正であり、ランタイムがその動作を検出するとクラッシュします。
  • unsafe パッケージを使用してポインタを uintptr 値に格納するプログラムも不正ですが、実行中に診断するのがより困難です。ポインタがランタイムから隠蔽されるため、スタック拡張やガベージコレクションによってそれらが指すメモリが再利用され、ダングリングポインタが作成される可能性があります。
  • メモリに格納された uintptr 値を unsafe.Pointer に変換するコードは不正であり、書き直す必要があります。
  • このようなコードは go vet によって識別できます。

変更の背景

Go 1.3 では、Go ランタイムのガベージコレクタ (GC) とスタック管理の精度と堅牢性を向上させるための重要な変更が導入されました。これ以前のバージョンでは、ランタイムは unsafe.Pointer を介して操作されるポインタについて、より寛容な(しかし不正確な)仮定を持っていました。この寛容さは、特にガベージコレクションやゴルーチンのスタック拡張時に、ランタイムがメモリ内のポインタを正確に識別できないという問題を引き起こす可能性がありました。

正確なガベージコレクションと効率的なスタック拡張を実現するためには、ランタイムがメモリ内のどの値がポインタであり、どの値がそうでないかを確実に知る必要があります。unsafe.Pointer を不適切に使用すると、ランタイムがポインタを認識できなくなり、結果としてメモリリーク、クラッシュ、またはダングリングポインタといった深刻な問題が発生する可能性がありました。

このコミットは、Go 1.3 で導入されたこの新しい厳密なポインタモデルについて開発者に警告し、既存のコードベースが新しい仮定に準拠していることを確認するためのガイダンスを提供することを目的としています。特に、go vet ツールがこの移行を支援するために更新されたことも強調されています。

前提知識の解説

1. Goのポインタとunsafe.Pointer

Go言語は、C言語のような直接的なメモリ操作を避けるように設計されていますが、unsafeパッケージは低レベルの操作を可能にします。

  • ポインタ型 (*T): Goの通常のポインタは型安全であり、特定の型の変数へのメモリアドレスを指します。ランタイムはこれらのポインタを認識し、ガベージコレクションやスタック拡張時に適切に処理します。
  • unsafe.Pointer: これは特別なポインタ型で、任意の型のポインタを保持できます。void* (C言語) に似ており、型システムをバイパスして任意のメモリ位置を指すことができます。しかし、その名の通り「unsafe(安全でない)」であり、誤用するとメモリ破壊やクラッシュを引き起こす可能性があります。主に、C言語との連携、特定のデータ構造の最適化、またはランタイムの内部操作のために使用されます。
  • uintptr: これは符号なし整数型で、ポインタのビットパターンを保持するのに十分な大きさです。uintptr は単なる数値であり、ランタイムはそれがメモリアドレスを指しているとは認識しません。unsafe.Pointeruintptr の間で相互変換が可能ですが、これは非常に注意深く行う必要があります。

2. Goのガベージコレクション (GC)

Goのガベージコレクタは、プログラムが不要になったメモリを自動的に解放する役割を担います。Go 1.3 時点では、並行マーク&スイープ方式のGCが採用されており、プログラムの実行と並行して動作します。

  • 正確なGC (Precise GC): GoのGCは「正確」です。これは、GCがメモリ内のどの値がポインタであり、どの値がそうでないかを正確に識別できることを意味します。これにより、GCは到達可能なオブジェクトを誤って解放することなく、不要なメモリを確実に回収できます。
  • ポインタスキャン: GCは、ヒープ上のオブジェクトやスタック上の変数をスキャンし、そこに含まれるポインタをたどって到達可能なオブジェクトを特定します。このスキャンプロセス中に、ランタイムがポインタであると認識しない値(例: uintptr に格納されたポインタ)があると、GCはそのポインタが指すオブジェクトを到達不可能と誤判断し、 prematurely に解放してしまう可能性があります。

3. スタック拡張

Goのゴルーチンは、必要に応じて動的にサイズが変更されるスタックを持っています。ゴルーチンがより多くのスタック領域を必要とすると、ランタイムはより大きな新しいスタックを割り当て、既存のスタックの内容を新しいスタックにコピーし、古いスタックを解放します。このコピープロセス中に、スタック上のポインタは新しいメモリ位置を指すように更新される必要があります。もしランタイムがポインタであると認識しない値(例: uintptr に格納されたポインタ)がスタック上に存在すると、その値は単なる数値としてコピーされ、新しいスタック位置へのポインタとして更新されません。これにより、古いスタック位置を指すダングリングポインタが生成されます。

4. ダングリングポインタ (Dangling Pointer)

ダングリングポインタとは、解放されたメモリ領域を指すポインタのことです。メモリが解放された後、その領域は別のデータによって再利用される可能性があります。ダングリングポインタを逆参照すると、無効なメモリにアクセスしようとすることになり、プログラムのクラッシュ(セグメンテーション違反など)、データ破損、または予測不能な動作を引き起こす可能性があります。

5. go vet ツール

go vet はGoプログラムのソースコードを静的に解析し、疑わしい構造や潜在的なバグを検出するツールです。コンパイルエラーにはならないが、実行時に問題を引き起こす可能性のあるコードパターンを特定するのに役立ちます。Go 1.3 では、unsafe.Pointer の不適切な使用を検出するための新しいチェックが go vet に追加されました。

技術的詳細

Go 1.3 で導入された unsafe.Pointer の厳密な扱いは、Go ランタイムのメモリ管理モデルにおける根本的な変更を反映しています。この変更の核心は、ランタイムがメモリ内のポインタを「正確に」識別できる能力にあります。

ランタイムの新しい仮定

Go 1.3 以降、ランタイムは以下の厳密な仮定に基づいて動作します。

  1. ポインタ型 (*T) の値: これらの値は常に有効なポインタ(または nil)を含んでいると仮定されます。ランタイムは、ガベージコレクションのスキャンやスタック拡張時のポインタ更新において、これらの値を信頼できるポインタとして扱います。
  2. 非ポインタ型 (例: int, uintptr, struct など) の値: これらの値はポインタを含まないと仮定されます。たとえ uintptr がメモリアドレスの数値を保持していても、ランタイムはそれを単なる数値データとして扱い、ポインタとしてスキャンしたり更新したりしません。

unsafe.Pointer の誤用とその影響

この新しい仮定の下で、unsafe.Pointer の特定の誤用パターンが「不正」と見なされ、深刻な問題を引き起こす可能性があります。

1. uintptr をポインタ値に格納する (uintptr -> unsafe.Pointer -> *T)

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var x int = 42
	// x のアドレスを uintptr として取得
	addr := uintptr(unsafe.Pointer(&x))

	// 不正な操作: uintptr を unsafe.Pointer を介してポインタ型変数に格納
	// ランタイムは p が有効なポインタを指すと仮定するが、addr は単なる数値
	var p *int = (*int)(unsafe.Pointer(addr)) // Go 1.3 以降では危険

	// この p を使用すると、GCが誤動作したり、クラッシュしたりする可能性がある
	fmt.Println(*p) // 実際には動作するかもしれないが、未定義動作
}

このシナリオでは、uintptr は単なる数値であり、ランタイムはそれが指すメモリが有効なGoオブジェクトであるという保証を持ちません。しかし、それを unsafe.Pointer を介して *int 型の変数 p に変換して格納すると、ランタイムは p が有効なポインタであると仮定します。もし addr が有効なGoオブジェクトを指していない場合、またはGCがそのメモリを既に解放している場合、GCが p をスキャンしようとするとクラッシュする可能性があります。

2. ポインタを uintptr 値に格納する (*T -> unsafe.Pointer -> uintptr)

package main

import (
	"fmt"
	"runtime"
	"unsafe"
)

func main() {
	var data = new(int) // ヒープに int を割り当てる
	*data = 100

	// ポインタを uintptr に変換して隠蔽
	hiddenPtr := uintptr(unsafe.Pointer(data))

	// ここで data への他の参照がなくなると、GCは data が指すメモリを解放する可能性がある
	data = nil // data への唯一の参照を削除

	// GC を強制的に実行 (デモンストレーションのため)
	runtime.GC()

	// hiddenPtr は解放されたメモリを指すダングリングポインタになる
	// この操作は非常に危険で、クラッシュやデータ破損を引き起こす可能性がある
	// fmt.Println(*(*int)(unsafe.Pointer(hiddenPtr))) // 危険!
	fmt.Printf("Hidden pointer value: %v\n", hiddenPtr)
}

このシナリオはより危険です。Goのポインタ datauintptr に変換して hiddenPtr に格納すると、ランタイムは data が指すオブジェクトへの「可視の」参照を失います。もし data への他のポインタが存在しない場合、GCは data が指すメモリを到達不可能と判断し、解放してしまいます。しかし、hiddenPtr は依然としてその解放されたメモリを指しています。これが「ダングリングポインタ」です。後で hiddenPtrunsafe.Pointer を介してポインタに変換し、逆参照しようとすると、既に解放されたメモリにアクセスすることになり、クラッシュやデータ破損につながります。スタック拡張時にも同様の問題が発生し、スタック上の uintptr に隠されたポインタが更新されずにダングリングポインタになる可能性があります。

go vet による検出

Go 1.3 では、go vet ツールに新しい静的解析ルールが追加され、特にメモリに格納された uintptr 値を unsafe.Pointer に変換するパターンを検出できるようになりました。これは、上記で説明した「ポインタを uintptr 値に格納する」シナリオで、ランタイムからポインタが隠蔽される可能性のあるコードを特定するのに役立ちます。go vet はコンパイル時にこれらの潜在的な問題を警告することで、開発者が実行時エラーに遭遇する前にコードを修正できるようにします。

この厳密化は、Goのメモリ安全性とGCの効率性を保証するために不可欠なステップでした。開発者は、unsafeパッケージを使用する際には、これらの新しいルールを深く理解し、細心の注意を払う必要があります。

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

このコミットによるコードの変更は、doc/go1.3.html ファイルへのテキストの追加のみです。

--- a/doc/go1.3.html
+++ b/doc/go1.3.html
@@ -117,6 +117,26 @@ This means that a non-pointer Go value such as an integer will never be mistaken
 pointer and prevent unused memory from being reclaimed.
 </p>
 
+<p>
+Starting with Go 1.3, the runtime assumes that values with pointer type
+contain pointers and other values do not.
+This assumption is fundamental to the precise behavior of both stack expansion
+and garbage collection.
+Programs that use <a href="/pkg/unsafe/">package unsafe</a>
+to store <code>uintptrs</code> in pointer values are illegal and will crash if the runtime detects the behavior.
+Programs that use <a href="/pkg/unsafe/">package unsafe</a> to store pointers
+in <code>uintptr</code> values are also illegal but more difficult to diagnose during execution.
+Because the pointers are hidden from the runtime, a stack expansion or garbage collection
+may reclaim the memory they point at, creating
+<a href="http://en.wikipedia.org/wiki/Dangling_pointer">dangling pointers</a>.
+</p>
+
+<p>
+<em>Updating</em>: Code that converts a <code>uintptr</code> value stored in memory
+to <code>unsafe.Pointer</code> is illegal and must be rewritten.
+Such code can be identified by <code>go vet</code>.
+</p>
+
 <h3 id=\"liblink\">The linker</h3>
 
 <p>

具体的には、<p> タグで囲まれた2つの新しい段落が追加されています。

コアとなるコードの解説

追加されたHTMLスニペットは、Go 1.3 のランタイムにおけるポインタの扱いに関する重要な変更点を説明するものです。

  1. 最初の段落:

    • 「Go 1.3 以降、ランタイムはポインタ型を持つ値がポインタを含み、その他の値はポインタを含まないと仮定する」と明記しています。これは、ランタイムがメモリをスキャンしてポインタを識別する際の基本的なルール変更です。
    • この仮定が「スタック拡張とガベージコレクションの正確な動作にとって不可欠である」と強調しています。これは、GCがメモリを正確に管理し、スタックが安全に拡張されるために、ランタイムがポインタを確実に認識する必要があることを意味します。
    • unsafe パッケージを使用して「uintptr をポインタ値に格納する」プログラムは「不正であり、ランタイムが動作を検出するとクラッシュする」と警告しています。これは、ランタイムがポインタとして扱うべきでないものをポインタとして扱おうとすることで発生する問題です。
    • unsafe パッケージを使用して「ポインタを uintptr 値に格納する」プログラムも「不正」であると述べています。こちらは「実行中に診断するのがより困難」であり、ランタイムからポインタが隠蔽されるため、「スタック拡張やガベージコレクションによってそれらが指すメモリが再利用され、ダングリングポインタが作成される可能性がある」と説明しています。これは、ランタイムがポインタを認識できなくなり、メモリが誤って解放されることで発生する、より深刻な問題です。
    • ダングリングポインタへのWikipediaリンクも提供されています。
  2. 二番目の段落:

    • 「メモリに格納された uintptr 値を unsafe.Pointer に変換するコードは不正であり、書き直す必要がある」と、具体的なコード修正の必要性を提示しています。
    • 「このようなコードは go vet によって識別できる」と、開発者がこれらの問題のあるコードパターンを特定するためのツールが提供されていることを示しています。

このドキュメントの追加は、Go 1.3 のリリースに伴い、開発者が既存のコードを新しいランタイムの仮定に適合させるための重要な情報源となります。特に、unsafe パッケージを扱う低レベルのコードを書いている開発者にとっては、この変更がプログラムの安定性と正確性に直接影響するため、非常に重要です。

関連リンク

参考にした情報源リンク

  • Go 1.3 Release Notes (公式ドキュメント): https://go.dev/doc/go1.3
  • Go のガベージコレクションに関する一般的な情報源 (例: Go の公式ブログ、技術記事など)
  • unsafe.Pointer の使用に関するGoコミュニティの議論やガイドライン
  • go vet の機能に関する情報