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

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

このコミットは、Goコンパイラ(gc)の内部定義において、runtimeパッケージの定義からunsafeパッケージへの依存を取り除くことを目的としています。具体的には、メモリ比較を行うmemequal系の組み込み関数の引数型をunsafe.Pointerから*any(当時のinterface{})に変更し、それに伴うコンパイラ内部の処理を修正しています。

変更されたファイルは以下の通りです。

  • src/cmd/gc/builtin.c.boot: コンパイラが起動時に読み込む組み込み関数の定義ファイル。
  • src/cmd/gc/runtime.go: runtimeパッケージのGo言語側の定義ファイル。
  • src/cmd/gc/subr.c: コンパイラのサブルーチン(補助関数)が定義されているC言語ファイル。主に型チェックやコード生成に関連する部分。
  • src/cmd/gc/walk.c: コンパイラのAST(抽象構文木)ウォーク処理が定義されているC言語ファイル。主に比較演算子の処理に関連する部分。

コミット

  • コミットハッシュ: 1d0f93b4be68263ec7e07255e8fe20e1168c9bba
  • 作者: Russ Cox (rsc@golang.org)
  • 日付: Mon Dec 19 15:52:15 2011 -0500

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

https://github.com/golang/go/commit/1d0f93b4be68263ec7e07255e8fe20e1168c9bba

元コミット内容

gc: avoid unsafe in defn of package runtime

Keeps -u tracking simple.

R=ken2
CC=golang-dev
https://golang.org/cl/5495094

変更の背景

この変更の主な背景は、Goコンパイラがunsafeパッケージの使用状況を追跡するメカニズム(コミットメッセージにある「-u tracking」)を簡素化することにあります。

Go言語のunsafeパッケージは、Goの型システムを迂回してメモリを直接操作する機能を提供します。これは非常に強力ですが、同時に型安全性を損ない、予期せぬバグやセキュリティ脆弱性を引き起こす可能性があります。そのため、Goツールチェインはunsafeパッケージの使用を厳密に監視し、特定の条件下で警告を発したり、ビルドを失敗させたりすることがあります。

runtimeパッケージはGoプログラムの実行を支える基盤であり、ガベージコレクション、スケジューリング、プリミティブな型操作など、低レベルな処理を多数含んでいます。歴史的に、runtimeパッケージの内部実装ではunsafeパッケージが多用されてきました。しかし、runtimeパッケージの定義自体がunsafeパッケージに依存していると、コンパイラがunsafeの使用状況を追跡する際に複雑さが増します。

このコミット以前は、runtimeパッケージの組み込み関数(特にmemequal系)の定義にunsafe.Pointerが直接使用されていました。これにより、コンパイラはruntimeパッケージの定義を処理する際にunsafeパッケージの存在を考慮する必要があり、-uフラグ(またはそれに類するunsafe使用状況の追跡機能)の実装が複雑になっていました。

この変更は、runtimeパッケージの定義からunsafeパッケージへの直接的な依存を取り除くことで、コンパイラがunsafeの使用状況をよりシンプルに追跡できるようにすることを目的としています。これにより、コンパイラの内部構造が整理され、将来的なメンテナンスや機能追加が容易になります。

前提知識の解説

Go言語のunsafeパッケージとunsafe.Pointer

unsafeパッケージは、Go言語の型安全性を意図的にバイパスするための機能を提供します。その中でもunsafe.Pointerは、任意の型のポインタと相互変換可能な特殊なポインタ型です。これにより、プログラマは型システムが通常許可しないメモリ操作(例: 異なる型のポインタ間の変換、ポインタ演算)を行うことができます。

  • unsafe.Pointerの主な用途:
    • C言語とのFFI (Foreign Function Interface)
    • 低レベルなシステムプログラミング(例: OSカーネルとのインタラクション)
    • パフォーマンスがクリティカルなコードでのメモリレイアウトの最適化
  • 危険性: unsafe.Pointerを使用すると、Goのメモリモデルやガベージコレクタの保証が破られる可能性があり、クラッシュ、データ破損、セキュリティ脆弱性につながることがあります。そのため、unsafeパッケージの使用は極力避け、必要な場合にのみ慎重に行うべきとされています。

Go言語のruntimeパッケージ

runtimeパッケージは、Goプログラムの実行環境を構成する中核的なライブラリです。Go言語で書かれたプログラムは、このruntimeパッケージが提供する機能の上に成り立っています。主な機能には以下のようなものがあります。

  • ガベージコレクション (GC): 不要になったメモリを自動的に解放する。
  • ゴルーチンとスケジューラ: 軽量な並行処理単位であるゴルーチンを管理し、CPUコアに効率的に割り当てる。
  • メモリ管理: ヒープメモリの割り当てと解放。
  • システムコール: OSとのインタラクション。
  • プリミティブな型操作: スライス、マップ、チャネルなどの組み込み型の低レベルな操作。
  • パニックとリカバリ: エラーハンドリングメカニズム。

runtimeパッケージの多くの部分はGo言語で書かれていますが、パフォーマンスやOSとの連携のためにC言語やアセンブリ言語で書かれた部分も含まれています。

Goコンパイラ (gc) の役割

gcは、Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担っています。コンパイラは、ソースコードの解析、抽象構文木(AST)の構築、型チェック、最適化、コード生成など、複数のフェーズを経て実行可能ファイルを生成します。

このコミットで変更されているファイル(builtin.c.boot, runtime.go, subr.c, walk.c)は、いずれもgcコンパイラの内部実装の一部です。

  • builtin.c.boot: コンパイラがGoプログラムをコンパイルする際に、runtimeパッケージの特定の関数や型を「組み込み」として認識させるための定義が含まれています。これは、runtimeパッケージがGo言語の他の部分とは異なる特別な扱いを受けるためです。
  • subr.cwalk.c: コンパイラのバックエンドに近い部分で、ASTの走査やコード生成の際に、特定の操作(例: 構造体の比較、メモリのコピー)をどのように実装するかを決定します。

any型(当時のinterface{}型)の役割

Go 1.18より前のバージョンでは、any型はinterface{}として知られていました。これは、任意の型の値を保持できる空のインターフェース型です。interface{}型の変数は、内部的に値の型情報と値そのもの(または値へのポインタ)を保持する構造体として表現されます。

このコミットでは、unsafe.Pointerの代わりに*any(つまり*interface{})を使用することで、型安全なポインタとして扱いつつ、任意の型のデータへのポインタを抽象的に表現しようとしています。これにより、コンパイラはunsafeパッケージの特別な処理を必要とせず、通常の型システムの一部としてこれらのポインタを扱うことができます。

-uフラグまたはunsafe使用状況の追跡

Goコンパイラには、unsafeパッケージの使用状況を追跡し、特定のルールに違反した場合に警告やエラーを出すための内部メカニズムが存在します。これは、Goの型安全性を維持し、unsafeパッケージの誤用を防ぐための重要な機能です。

コミットメッセージの「Keeps -u tracking simple」は、このunsafe使用状況の追跡ロジックを簡素化することを指しています。runtimeパッケージの定義自体がunsafe.Pointerに依存していると、コンパイラはruntimeの特殊性を考慮しながらunsafeの使用を追跡する必要があり、そのロジックが複雑になります。unsafe.Pointer*anyに置き換えることで、runtimeの定義が通常のGoの型システムに沿った形になり、追跡ロジックがより直接的になります。

技術的詳細

このコミットの技術的な核心は、Goコンパイラがruntimeパッケージの組み込み関数、特にmemequal(メモリ領域の比較)系の関数をどのように扱うかを変更することにあります。

以前は、これらの関数は以下のようなシグネチャを持っていました(簡略化)。

func memequal(eq *bool, size uintptr, x, y unsafe.Pointer)

ここで、xyは比較対象のメモリ領域へのポインタであり、unsafe.Pointer型でした。これは、Goの型システムでは異なる型のメモリ領域を直接比較することができないため、unsafeパッケージの力を借りて型チェックを回避していました。

このコミットでは、これらの関数のシグネチャを以下のように変更します。

func memequal(eq *bool, size uintptr, x, y *any)

unsafe.Pointer*any*interface{})に置き換えられています。この変更は、コンパイラ内部で以下のような影響をもたらします。

  1. 型安全性の向上(コンパイラ内部の視点): unsafe.Pointerはコンパイラにとって「何でもあり」の型であり、特別な処理が必要でした。*anyはGoの通常の型システムの一部であるため、コンパイラはこれらのポインタをより標準的な方法で型チェックし、処理することができます。これにより、コンパイラの内部ロジックが簡素化されます。
  2. unsafeパッケージ依存の排除: runtimeパッケージの定義からunsafeパッケージへの直接的なimportが不要になります。これは、runtimeパッケージがGoの基盤であるため、その定義が可能な限りクリーンで、特殊な依存関係を持たないことが望ましいという設計思想に基づいています。
  3. -uトラッキングの簡素化: コンパイラがunsafeの使用を追跡する際、runtimeパッケージの組み込み関数がunsafe.Pointerを使用しているという特殊なケースを考慮する必要がなくなります。これにより、unsafe使用状況の追跡ロジックがよりシンプルになり、バグの発生リスクが低減します。
  4. コンパイラ内部のAST変換と型チェックの調整: src/cmd/gc/subr.csrc/cmd/gc/walk.cの変更は、このシグネチャ変更に対応するためのものです。
    • eqmemfunc関数は、memequal系の関数を呼び出す際に、引数の型をunsafe.Pointerから*anyに適切に変換するように変更されます。
    • eqmem関数やwalkcompare関数では、メモリ比較を行う際に、引数として渡されるポインタがunsafe.Pointerにキャストされるのではなく、直接*anyとして扱われるようになります。これには、typecheckの呼び出しや、引数をリストに追加する際の処理の変更が含まれます。

この変更は、Go言語の設計哲学である「シンプルさ」と「型安全性」をコンパイラの内部構造にも適用しようとする試みの一環と言えます。unsafeパッケージは必要悪として存在しますが、その使用は最小限に抑え、特にGoのコア部分であるruntimeパッケージの定義からは可能な限り排除するという方向性を示しています。

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

src/cmd/gc/builtin.c.boot

--- a/src/cmd/gc/builtin.c.boot
+++ b/src/cmd/gc/builtin.c.boot
@@ -1,7 +1,6 @@
 char *runtimeimport =
 	"package runtime\\n"\
 	"import runtime \\\"runtime\\\"\\n"\
-"	import unsafe \\\"unsafe\\\"\\n"\
 	"func @\\\"\\\".new(@\\\"\\\".typ *byte) *any\\n"\
 	"func @\\\"\\\".panicindex()\\n"\
 	"func @\\\"\\\".panicslice()\\n"\
@@ -91,12 +90,12 @@ char *runtimeimport =
 	"func @\\\"\\\".sliceslice(@\\\"\\\".old []any, @\\\"\\\".lb uint64, @\\\"\\\".hb uint64, @\\\"\\\".width uint64) []any\\n"\
 	"func @\\\"\\\".slicearray(@\\\"\\\".old *any, @\\\"\\\".nel uint64, @\\\"\\\".lb uint64, @\\\"\\\".hb uint64, @\\\"\\\".width uint64) []any\\n"\
 	"func @\\\"\\\".closure()\\n"\
-"	func @\\\"\\\".memequal(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x @\\\"unsafe\\\".Pointer, @\\\"\\\".y @\\\"unsafe\\\".Pointer)\\n"\
-"	func @\\\"\\\".memequal8(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x @\\\"unsafe\\\".Pointer, @\\\"\\\".y @\\\"unsafe\\\".Pointer)\\n"\
-"	func @\\\"\\\".memequal16(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x @\\\"unsafe\\\".Pointer, @\\\"\\\".y @\\\"unsafe\\\".Pointer)\\n"\
-"	func @\\\"\\\".memequal32(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x @\\\"unsafe\\\".Pointer, @\\\"\\\".y @\\\"unsafe\\\".Pointer)\\n"\
-"	func @\\\"\\\".memequal64(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x @\\\"unsafe\\\".Pointer, @\\\"\\\".y @\\\"unsafe\\\".Pointer)\\n"\
-"	func @\\\"\\\".memequal128(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x @\\\"unsafe\\\".Pointer, @\\\"\\\".y @\\\"unsafe\\\".Pointer)\\n"\
+"	func @\\\"\\\".memequal(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x *any, @\\\"\\\".y *any)\\n"\
+"	func @\\\"\\\".memequal8(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x *any, @\\\"\\\".y *any)\\n"\
+"	func @\\\"\\\".memequal16(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x *any, @\\\"\\\".y *any)\\n"\
+"	func @\\\"\\\".memequal32(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x *any, @\\\"\\\".y *any)\\n"\
+"	func @\\\"\\\".memequal64(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x *any, @\\\"\\\".y *any)\\n"\
+"	func @\\\"\\\".memequal128(@\\\"\\\".eq *bool, @\\\"\\\".size uintptr, @\\\"\\\".x *any, @\\\"\\\".y *any)\\n"\
 	"func @\\\"\\\".int64div(? int64, ? int64) int64\\n"\
 	"func @\\\"\\\".uint64div(? uint64, ? uint64) uint64\\n"\
 	"func @\\\"\\\".int64mod(? int64, ? int64) int64\\n"\

runtimeimport文字列からimport unsafe \"unsafe\"\\nの行が削除され、memequal系の関数の引数型が@\"unsafe\".Pointerから*anyに変更されています。これは、コンパイラがruntimeパッケージの組み込み関数を認識する際の定義を変更するものです。

src/cmd/gc/runtime.go

--- a/src/cmd/gc/runtime.go
+++ b/src/cmd/gc/runtime.go
@@ -8,8 +8,6 @@
 
 package PACKAGE
 
-import "unsafe"
-
 // emitted by compiler, not referred to by go programs
 
 func new(typ *byte) *any
@@ -123,12 +121,12 @@ func slicearray(old *any, nel uint64, lb uint64, hb uint64, width uint64) (ary [\
 
 func closure() // has args, but compiler fills in
 
-func memequal(eq *bool, size uintptr, x, y unsafe.Pointer)
-func memequal8(eq *bool, size uintptr, x, y unsafe.Pointer)
-func memequal16(eq *bool, size uintptr, x, y unsafe.Pointer)
-func memequal32(eq *bool, size uintptr, x, y unsafe.Pointer)
-func memequal64(eq *bool, size uintptr, x, y unsafe.Pointer)
-func memequal128(eq *bool, size uintptr, x, y unsafe.Pointer)
+func memequal(eq *bool, size uintptr, x, y *any)
+func memequal8(eq *bool, size uintptr, x, y *any)
+func memequal16(eq *bool, size uintptr, x, y *any)
+func memequal32(eq *bool, size uintptr, x, y *any)
+func memequal64(eq *bool, size uintptr, x, y *any)
+func memequal128(eq *bool, size uintptr, x, y *any)
 
 // only used on 32-bit
 func int64div(int64, int64) int64

import "unsafe"の行が削除され、memequal系の関数のシグネチャがunsafe.Pointerから*anyに変更されています。これは、runtimeパッケージのGo言語側の定義からunsafeへの直接的な依存を取り除くものです。

src/cmd/gc/subr.c

--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -2636,20 +2636,27 @@ eqfield(Node *p, Node *q, Node *field, Node *eq)\n }\n \n static Node*\n-eqmemfunc(vlong size)\n+eqmemfunc(vlong size, Type *type)\n {\n 	char buf[30];\n+\tNode *fn;\n \n 	switch(size) {\n+\tdefault:\n+\t\tfn = syslook(\"memequal\", 1);\n+\t\tbreak;\n 	case 1:\n 	case 2:\n 	case 4:\n 	case 8:\n 	case 16:\n 		snprint(buf, sizeof buf, \"memequal%d\", (int)size*8);\n-\t\treturn syslook(buf, 0);\n+\t\tfn = syslook(buf, 1);\n+\t\tbreak;\n 	}\n-\treturn syslook(\"memequal\", 0);\n+\targtype(fn, type);\n+\targtype(fn, type);\n+\treturn fn;\n }\n \n // Return node for\n@@ -2663,12 +2670,14 @@ eqmem(Node *p, Node *q, Node *field, vlong size, Node *eq)\n 	nx->etype = 1;  // does not escape\n 	ny = nod(OADDR, nod(OXDOT, q, field), N);\n 	ny->etype = 1;  // does not escape\n+\ttypecheck(&nx, Erv);\n+\ttypecheck(&ny, Erv);\
 \n-\tcall = nod(OCALL, eqmemfunc(size), N);\n+\tcall = nod(OCALL, eqmemfunc(size, nx->type->type), N);\
 \tcall->list = list(call->list, eq);\n \tcall->list = list(call->list, nodintconst(size));\n-\tcall->list = list(call->list, conv(nx, types[TUNSAFEPTR]));\n-\tcall->list = list(call->list, conv(ny, types[TUNSAFEPTR]));\n+\tcall->list = list(call->list, nx);\n+\tcall->list = list(call->list, ny);\
 \n \tnif = nod(OIF, N, N);\n \tnif->ninit = list(nif->ninit, call);\

eqmemfunc関数のシグネチャにType *typeが追加され、memequal系の関数ノードを取得する際に、その型情報も考慮されるようになりました。また、eqmem関数内でmemequalを呼び出す際、nxny(比較対象のノード)がtypes[TUNSAFEPTR]unsafe.Pointer型)に変換されるのではなく、直接引数として渡されるようになりました。これに伴い、typecheckが追加されています。

src/cmd/gc/walk.c

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -2408,8 +2408,12 @@ eqfor(Type *t)\n \tif(a != AMEM && a != -1)\n \t\tfatal(\"eqfor %T\", t);\n \n-\tif(a == AMEM)\n-\t\treturn syslook(\"memequal\", 0);\n+\tif(a == AMEM) {\n+\t\tn = syslook(\"memequal\", 1);\n+\t\targtype(n, t);\n+\t\targtype(n, t);\n+\t\treturn n;\n+\t}\n \n \tsym = typesymprefix(\".eq\", t);\n \tn = newname(sym);\n@@ -2417,8 +2421,8 @@ eqfor(Type *t)\n \tntype = nod(OTFUNC, N, N);\n \tntype->list = list(ntype->list, nod(ODCLFIELD, N, typenod(ptrto(types[TBOOL]))));\n \tntype->list = list(ntype->list, nod(ODCLFIELD, N, typenod(types[TUINTPTR])));\n-\tntype->list = list(ntype->list, nod(ODCLFIELD, N, typenod(types[TUNSAFEPTR])));\n-\tntype->list = list(ntype->list, nod(ODCLFIELD, N, typenod(types[TUNSAFEPTR])));\n+\tntype->list = list(ntype->list, nod(ODCLFIELD, N, typenod(ptrto(t))));\n+\tntype->list = list(ntype->list, nod(ODCLFIELD, N, typenod(ptrto(t))));\n \ttypecheck(&ntype, Etype);\n \tn->type = ntype->type;\n \treturn n;\n@@ -2536,8 +2540,8 @@ walkcompare(Node **np, NodeList **init)\n \ta->etype = 1;  // does not escape\n \tcall->list = list(call->list, a);\n \tcall->list = list(call->list, nodintconst(t->width));\n-\tcall->list = list(call->list, conv(l, types[TUNSAFEPTR]));\n-\tcall->list = list(call->list, conv(r, types[TUNSAFEPTR]));\n+\tcall->list = list(call->list, l);\n+\tcall->list = list(call->list, r);\
 \ttypecheck(&call, Etop);\n \twalkstmt(&call);\n \t*init = list(*init, call);\

eqfor関数内でmemequal関数ノードを取得する際に、引数型を*anyとして設定するようになりました。また、walkcompare関数内で比較を行う際に、lr(比較対象の左辺と右辺)がtypes[TUNSAFEPTR]に変換されることなく、直接引数として渡されるようになりました。

コアとなるコードの解説

このコミットのコアとなる変更は、Goコンパイラがmemequal系の組み込み関数を扱う方法を根本的に変更した点にあります。

  1. unsafeパッケージのインポート削除とシグネチャ変更:

    • src/cmd/gc/builtin.c.bootsrc/cmd/gc/runtime.goにおける変更は、runtimeパッケージの定義からunsafeパッケージへの直接的な依存を排除します。これにより、runtimeパッケージがGoの型システム内でより「通常の」パッケージとして扱われるようになります。
    • memequal系の関数の引数型をunsafe.Pointerから*anyに変更することで、これらの関数がGoの型システムに則った形で定義されるようになります。*anyは任意の型へのポインタを抽象的に表現できるため、unsafe.Pointerが提供していた「任意のメモリ領域を指す」という機能性を、型安全な範囲で代替しています。
  2. コンパイラ内部の型処理の調整:

    • src/cmd/gc/subr.csrc/cmd/gc/walk.cにおける変更は、このシグネチャ変更に対応するためのコンパイラ内部の調整です。
    • 以前は、memequalを呼び出す際に、比較対象のポインタを明示的にunsafe.Pointer型に変換(キャスト)していました。これは、unsafe.Pointerが特殊な型であるため、コンパイラがその変換を認識し、適切なコードを生成する必要があったためです。
    • 変更後は、比較対象のポインタ(nx, ny, l, r)は*any型として直接memequalに渡されます。これにより、コンパイラは通常のポインタ型とインターフェース型の変換ルールに従って処理を行うことができます。
    • 特にsubr.ceqmemfunceqmemwalk.ceqforwalkcompareでは、conv(..., types[TUNSAFEPTR])のようなunsafe.Pointerへの明示的な変換が削除され、代わりにtypecheckが追加されています。これは、コンパイラが*any型への変換を通常の型チェックプロセスの一部として処理できるようになったことを意味します。

この一連の変更により、Goコンパイラはruntimeパッケージの定義を処理する際にunsafeパッケージの特殊なセマンティクスを考慮する必要がなくなり、コンパイラの内部ロジックが簡素化され、-uトラッキング(unsafe使用状況の追跡)の実装がより直接的かつ容易になりました。これは、Go言語の進化において、コンパイラとランタイムの間のインターフェースをよりクリーンで型安全なものにするための重要なステップと言えます。

関連リンク

参考にした情報源リンク

  • Go言語のunsafeパッケージに関する公式ドキュメント: https://pkg.go.dev/unsafe
  • Go言語のruntimeパッケージに関する公式ドキュメント: https://pkg.go.dev/runtime
  • Go言語のinterface{}any)に関する公式ドキュメント: https://pkg.go.dev/builtin#any
  • Goコンパイラの内部構造に関する一般的な情報(Goのバージョンによって異なる可能性があるため、当時の情報源を探すのが理想的ですが、一般的な概念は共通しています)