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

[インデックス 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ポインタチェックを挿入する方法を、明示的なメモリ参照(checkrefcheckoffset)から、より抽象的なACHECKNIL擬似命令とcgen_checknil関数による挿入に移行したことです。

  1. 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では、OCHECKNOTNILOCHECKNILに名称変更され、nilチェックの意図をより明確にしています。
  2. cgen_checknil関数の導入:

    • src/cmd/gc/pgen.ccgen_checknil関数が追加されました。この関数は、指定されたノード(ポインタ)に対してACHECKNIL擬似命令を生成する役割を担います。
    • この関数は、disable_checknilというグローバル変数によって一時的に無効にすることができます。これは、レース検出器がnilチェックを回避する場合などに使用されます。
    • ODOTOINDEXのような操作で、ポインタのベースがnilでないことを保証するために、このcgen_checknilが呼び出されるようになりました。
  3. expandchecks関数の導入とnilチェックの展開:

    • src/cmd/{5,6,8}g/ggen.cexpandchecks関数が追加されました。この関数は、コード生成の最終段階(regoptpeepの実行後)で呼び出されます。
    • expandchecksは、生成されたプログラム内の全てのACHECKNIL擬似命令を走査し、それらを実際のnilポインタチェックを行うアセンブリ命令(例: CMP arg, $0MOV.EQ arg, 0(arg) または JNEMOV AX, 0)に展開します。
    • これにより、nilチェックのロジックがコンパイラのバックエンドに集約され、より一貫性のある、そして将来的に最適化しやすい形になりました。
  4. 冗長なnilチェックの削除:

    • src/cmd/{5,6,8}g/cgen.cおよびsrc/cmd/{5,6,8}g/gsubr.cから、以前の明示的なcheckrefcheckoffsetの呼び出し、および手動で挿入されていたnilチェックのコードが削除されました。これらは、新しいcgen_checknilexpandchecksのメカニズムに置き換えられました。
    • 特に、unmappedzeroの範囲外へのアクセスに対する明示的なチェックが削除され、ACHECKNILによる統一的なアプローチが採用されました。
  5. インターフェースメソッド呼び出しのnilチェック:

    • src/cmd/gc/closure.cでは、インターフェースメソッド呼び出しにおいて、nilインターフェースに対するメソッド呼び出しがパニックを引き起こすようにchecknotnilchecknilに変更されました。これにより、混乱を招くようなラッパー内でのパニックではなく、より早い段階でパニックが発生するようになります。
  6. テストケースの追加:

    • test/nilcheck.gotest/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関数内の関連する呼び出しも削除されました。odotoindexなどのコード生成ロジックにも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: OCHECKNOTNILOCHECKNILに名称変更され、cgen_checknil関数のプロトタイプが追加されました。また、debug_checknildisable_checknilという新しいグローバル変数が宣言されました。
  • src/cmd/gc/lex.c: コマンドライン引数-d nildebug_checknilを有効にするためのロジックが追加されました。
  • src/cmd/gc/pgen.c: compile関数内でexpandchecksが呼び出されるようになり、cgen_checknil関数が実装されました。
  • src/cmd/gc/subr.c: checknotnil関数がchecknilに名称変更され、OCHECKNOTNILOCHECKNILに更新されました。
  • src/cmd/gc/walk.c: walkexpr関数内で、OINDODOTPTRなどの操作に対してchecknilが呼び出されるようになりました。
  • test/nilcheck.go: 新しいnilチェックの挙動を検証するためのテストケースが追加されました。
  • test/nilptr2.go: nilポインタデリファレンスがパニックを引き起こすことを確認するためのランタイムテストが追加されました。

コアとなるコードの解説

このコミットの核心は、Goコンパイラがnilポインタチェックを挿入するプロセスを、手動で散在していたチェックから、より構造化された統一的なメカニズムへと移行させた点にあります。

  1. ACHECKNIL (擬似命令): これは、コンパイラのフロントエンド(gc)が、特定のポインタがnilでないことを確認する必要がある箇所に挿入する「マーク」のようなものです。この段階では、具体的なアセンブリ命令は生成されません。これは、コンパイラがコードを生成する際に、後で実際のチェック命令に置き換えられるべき場所を示すための抽象的な指示です。

  2. cgen_checknil(Node *n): この関数は、GoのAST(抽象構文木)を走査する際に、nilチェックが必要な箇所で呼び出されます。例えば、ポインタのデリファレンス(*p)、構造体のフィールドアクセス(p.field)、配列のインデックスアクセス(p[i])などです。cgen_checknilは、引数nが指すポインタに対してACHECKNIL擬似命令を生成し、コードストリームに挿入します。これにより、コンパイラのフロントエンドは、nilチェックの具体的な実装詳細を気にすることなく、必要な場所にチェックを「要求」できます。

  3. 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ポインタデリファレンスパニックとして報告します。

この新しいメカニズムにより、nilチェックの挿入ロジックがコンパイラの異なるステージ間で明確に分離され、よりモジュール化されました。フロントエンドはチェックの必要性を宣言し、バックエンドがそれを効率的なアセンブリ命令に変換します。これにより、将来的な最適化(例えば、冗長なnilチェックの削除)が容易になります。

関連リンク

参考にした情報源リンク