[インデックス 17272] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)におけるnilポインタのチェックメカニズムを改善し、&x
のようなアドレス取得操作がx
がnilの場合にパニックを引き起こすように変更するものです。これは、golang.org/s/go12nil
で議論されたGo 1.2のnilチェックの挙動変更に対応するためのもので、冗長なチェックを後で最適化することを前提として、まず全ての必要なチェックを挿入することに焦点を当てています。
コミット
- コミットハッシュ:
999a36f9afe858f1928e5ea74b2d9b41c9090873
- Author: Russ Cox rsc@golang.org
- Date: Thu Aug 15 14:38:32 2013 -0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/999a36f9afe858f1928e5ea74b2d9b41c9090873
元コミット内容
cmd/gc: &x panics if x does
See golang.org/s/go12nil.
This CL is about getting all the right checks inserted.
A followup CL will add an optimization pass to
remove redundant checks.
R=ken2
CC=golang-dev
https://golang.org/cl/12970043
変更の背景
Go言語では、nilポインタのデリファレンスはランタイムパニックを引き起こします。しかし、Go 1.2以前のバージョンでは、&x.Field
のようなアドレス取得操作において、x
がnilポインタである場合に一貫性のない挙動を示すことがありました。特に、構造体のフィールドが非常に大きい場合など、nil
ポインタから派生したアドレスが、OSが保護するメモリ領域(通常はアドレス0に近い領域)の外に出てしまい、OSによるセグメンテーション違反(SEGV)が発生しないケースがありました。これにより、プログラマが期待するnilポインタデリファレンスによるパニックが発生せず、未定義の動作や予測不能なクラッシュにつながる可能性がありました。
この問題を解決し、nilポインタデリファレンスの挙動を一貫させるため、golang.org/s/go12nil
で新しいnilチェックのセマンティクスが提案されました。このコミットは、その提案に基づき、&x.Field
や&x[i]
のような操作が、x
がnilの場合に必ずパニックを引き起こすように、コンパイラが適切なnilチェックを挿入するように変更することを目的としています。この変更は、まず全ての必要なチェックを挿入し、その後に冗長なチェックを削除する最適化パスを追加するという段階的なアプローチの一部です。
前提知識の解説
- nilポインタデリファレンス: プログラムが、有効なメモリ位置を指していない(
nil
である)ポインタを通じてメモリにアクセスしようとするときに発生するエラーです。Goでは、これは通常「runtime error: invalid memory address or nil pointer dereference」というパニックを引き起こします。 - ランタイムパニック: Goプログラムが回復不能なエラーに遭遇した際に、プログラムの実行を停止させるメカニズムです。nilポインタデリファレンスは典型的なパニックの原因です。
unmappedzero
: Goのランタイムにおいて、アドレス0に近い特定のメモリ領域は意図的にマップされていません。これは、nilポインタのデリファレンスをOSレベルのセグメンテーション違反として捕捉するための一般的な手法です。しかし、大きなオフセットを持つアクセスの場合、このunmappedzero
の範囲外のアドレスにアクセスしようとすることがあり、その場合はOSによる自動的なnilチェックが機能しません。- コンパイラのコード生成: Goコンパイラは、Goのソースコードを機械語に変換する過程で、様々な最適化やランタイムチェックを挿入します。このコミットは、そのコード生成の段階でnilチェックを挿入する方法を変更しています。
- 擬似命令 (Pseudo-op): アセンブラやコンパイラの内部で使われる、実際の機械語命令には直接対応しないが、特定の処理やコンパイラの挙動を指示するための命令です。
ACHECKNIL
はこのコミットで導入された擬似命令です。
技術的詳細
このコミットの主要な変更点は、Goコンパイラがnilポインタチェックを挿入する方法を、明示的なメモリ参照(checkref
やcheckoffset
)から、より抽象的なACHECKNIL
擬似命令とcgen_checknil
関数による挿入に移行したことです。
-
ACHECKNIL
擬似命令の導入:src/cmd/{5,6,8}l/{5,6,8}.out.h
およびsrc/cmd/{5,6,8}g/prog.c
に、新しい擬似命令ACHECKNIL
が追加されました。これは、特定のレジスタまたはメモリ位置がnilでないことをチェックするためのプレースホルダーとして機能します。src/cmd/gc/go.h
では、OCHECKNOTNIL
がOCHECKNIL
に名称変更され、nilチェックの意図をより明確にしています。
-
cgen_checknil
関数の導入:src/cmd/gc/pgen.c
にcgen_checknil
関数が追加されました。この関数は、指定されたノード(ポインタ)に対してACHECKNIL
擬似命令を生成する役割を担います。- この関数は、
disable_checknil
というグローバル変数によって一時的に無効にすることができます。これは、レース検出器がnilチェックを回避する場合などに使用されます。 ODOT
やOINDEX
のような操作で、ポインタのベースがnilでないことを保証するために、このcgen_checknil
が呼び出されるようになりました。
-
expandchecks
関数の導入とnilチェックの展開:src/cmd/{5,6,8}g/ggen.c
にexpandchecks
関数が追加されました。この関数は、コード生成の最終段階(regopt
とpeep
の実行後)で呼び出されます。expandchecks
は、生成されたプログラム内の全てのACHECKNIL
擬似命令を走査し、それらを実際のnilポインタチェックを行うアセンブリ命令(例:CMP arg, $0
とMOV.EQ arg, 0(arg)
またはJNE
とMOV AX, 0
)に展開します。- これにより、nilチェックのロジックがコンパイラのバックエンドに集約され、より一貫性のある、そして将来的に最適化しやすい形になりました。
-
冗長なnilチェックの削除:
src/cmd/{5,6,8}g/cgen.c
およびsrc/cmd/{5,6,8}g/gsubr.c
から、以前の明示的なcheckref
やcheckoffset
の呼び出し、および手動で挿入されていたnilチェックのコードが削除されました。これらは、新しいcgen_checknil
とexpandchecks
のメカニズムに置き換えられました。- 特に、
unmappedzero
の範囲外へのアクセスに対する明示的なチェックが削除され、ACHECKNIL
による統一的なアプローチが採用されました。
-
インターフェースメソッド呼び出しのnilチェック:
src/cmd/gc/closure.c
では、インターフェースメソッド呼び出しにおいて、nilインターフェースに対するメソッド呼び出しがパニックを引き起こすようにchecknotnil
がchecknil
に変更されました。これにより、混乱を招くようなラッパー内でのパニックではなく、より早い段階でパニックが発生するようになります。
-
テストケースの追加:
test/nilcheck.go
とtest/nilptr2.go
という新しいテストファイルが追加されました。これらは、様々なnilポインタデリファレンスのシナリオ(ポインタ、配列、構造体、大きなオフセットなど)で、コンパイラが正しくnilチェックを挿入し、期待通りにパニックが発生するかどうかを検証します。
コアとなるコードの変更箇所
このコミットは、Goコンパイラのコード生成部分に広範な変更を加えています。主要な変更ファイルと関数は以下の通りです。
src/cmd/{5,6,8}g/cgen.c
:agen
,igen
,agenr
関数から、以前の明示的なnilチェック(unmappedzero
に関連するコードブロック)が削除され、cgen_checknil
の呼び出しが追加されました。src/cmd/{5,6,8}g/ggen.c
:expandchecks
関数が追加され、ACHECKNIL
擬似命令を実際のnilチェックアセンブリ命令に展開するロジックが実装されました。また、cgen_callinter
などにもcgen_checknil
が追加されています。src/cmd/{5,6,8}g/gsubr.c
:checkref
関数とcheckoffset
関数が削除され、naddr
関数内の関連する呼び出しも削除されました。odot
やoindex
などのコード生成ロジックにもcgen_checknil
が追加されています。src/cmd/{5,6,8}g/prog.c
:ACHECKNIL
擬似命令がprogtable
に追加され、そのプロパティが定義されました。src/cmd/{5,6,8}l/{5,6,8}.out.h
:ACHECKNIL
がアセンブリ命令の列挙型に追加されました。src/cmd/gc/go.h
:OCHECKNOTNIL
がOCHECKNIL
に名称変更され、cgen_checknil
関数のプロトタイプが追加されました。また、debug_checknil
とdisable_checknil
という新しいグローバル変数が宣言されました。src/cmd/gc/lex.c
: コマンドライン引数-d nil
でdebug_checknil
を有効にするためのロジックが追加されました。src/cmd/gc/pgen.c
:compile
関数内でexpandchecks
が呼び出されるようになり、cgen_checknil
関数が実装されました。src/cmd/gc/subr.c
:checknotnil
関数がchecknil
に名称変更され、OCHECKNOTNIL
がOCHECKNIL
に更新されました。src/cmd/gc/walk.c
:walkexpr
関数内で、OIND
やODOTPTR
などの操作に対してchecknil
が呼び出されるようになりました。test/nilcheck.go
: 新しいnilチェックの挙動を検証するためのテストケースが追加されました。test/nilptr2.go
: nilポインタデリファレンスがパニックを引き起こすことを確認するためのランタイムテストが追加されました。
コアとなるコードの解説
このコミットの核心は、Goコンパイラがnilポインタチェックを挿入するプロセスを、手動で散在していたチェックから、より構造化された統一的なメカニズムへと移行させた点にあります。
-
ACHECKNIL
(擬似命令): これは、コンパイラのフロントエンド(gc
)が、特定のポインタがnilでないことを確認する必要がある箇所に挿入する「マーク」のようなものです。この段階では、具体的なアセンブリ命令は生成されません。これは、コンパイラがコードを生成する際に、後で実際のチェック命令に置き換えられるべき場所を示すための抽象的な指示です。 -
cgen_checknil(Node *n)
: この関数は、GoのAST(抽象構文木)を走査する際に、nilチェックが必要な箇所で呼び出されます。例えば、ポインタのデリファレンス(*p
)、構造体のフィールドアクセス(p.field
)、配列のインデックスアクセス(p[i]
)などです。cgen_checknil
は、引数n
が指すポインタに対してACHECKNIL
擬似命令を生成し、コードストリームに挿入します。これにより、コンパイラのフロントエンドは、nilチェックの具体的な実装詳細を気にすることなく、必要な場所にチェックを「要求」できます。 -
expandchecks(Prog *firstp)
: この関数は、コンパイラのバックエンド(各アーキテクチャ固有のコードジェネレータ、例:5g
,6g
,8g
)で実行されます。firstp
は、生成されたアセンブリ命令のリストの先頭を指します。expandchecks
は、この命令リストを走査し、ACHECKNIL
擬似命令を見つけると、それを実際のCPU命令に展開します。- 例えば、x86-64 (6g) の場合、
ACHECKNIL
は以下のような命令シーケンスに展開されます。CMP arg, $0 // ポインタがnil (0) かどうかを比較 JNE 2(PC) // nilでなければ、次の命令をスキップして続行 (最適化) MOV AX, 0(arg) // nilであれば、アドレス0をデリファレンスしてパニックを引き起こす
- この
MOV AX, 0(arg)
のような命令は、通常、OSによって保護されたアドレス0へのアクセスを試みるため、セグメンテーション違反(SEGV)を引き起こし、Goランタイムがそれを捕捉してnilポインタデリファレンスパニックとして報告します。
- 例えば、x86-64 (6g) の場合、
この新しいメカニズムにより、nilチェックの挿入ロジックがコンパイラの異なるステージ間で明確に分離され、よりモジュール化されました。フロントエンドはチェックの必要性を宣言し、バックエンドがそれを効率的なアセンブリ命令に変換します。これにより、将来的な最適化(例えば、冗長なnilチェックの削除)が容易になります。
関連リンク
- Go 1.2 Field Selectors and Nil Checks (design document): https://go.dev/s/go12nil
- Go CL 12970043: https://golang.org/cl/12970043
参考にした情報源リンク
- https://go.dev/s/go12nil
- https://golang.org/cl/12970043
- https://medium.com/@joshua.s.gordon/understanding-nil-pointer-dereference-in-go-3a2d8c7b7e7d
- https://www.pythonanywhere.com/forums/topic/28009/
- https://stackoverflow.com/questions/20900300/what-is-a-nil-pointer-dereference-in-go
- https://medium.com/@joshua.s.gordon/go-nil-pointer-dereference-panic-3a2d8c7b7e7d