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

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

src/pkg/runtime/stack.c

コミット

commit 060a988011b34ded3e002e1a4cb138b7ed21b176
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jun 12 21:12:53 2014 -0400

    runtime: revise CL 105140044 (defer nil) to work on Windows
    
    It appears that something about Go on Windows
    cannot handle the fault cause by a jump to address 0.
    The way Go represents and calls functions, this
    never happened at all, until CL 105140044.
    
    This CL changes the code added in CL 105140044
    to make jump to 0 impossible once again.
    
    Fixes #8047. (again, on Windows)
    
    TBR=bradfitz
    R=golang-codereviews, dave
    CC=adg, golang-codereviews, iant, r
    https://golang.org/cl/105120044

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

https://github.com/golang/go/commit/060a988011b34ded3e002e1a4cb138b7ed21b176

元コミット内容

runtime: revise CL 105140044 (defer nil) to work on Windows

It appears that something about Go on Windows
cannot handle the fault cause by a jump to address 0.
The way Go represents and calls functions, this
never happened at all, until CL 105140044.

This CL changes the code added in CL 105140044
to make jump to 0 impossible once again.

Fixes #8047. (again, on Windows)

TBR=bradfitz
R=golang-codereviews, dave
CC=adg, golang-codereviews, iant, r
https://golang.org/cl/105120044

変更の背景

このコミットは、Goランタイムにおけるdefer nilnilの関数値をdeferで呼び出す)の挙動に関するWindows固有の問題を修正するために導入されました。

Go言語では、defer文でnilの関数値を指定した場合、その関数が実際に実行される(呼び出し元の関数がリターンする)までパニックを遅延させるという設計になっています(関連する議論はGo Issue #8109で確認できます)。以前の変更(Change List: CL 105140044)は、この「遅延パニック」の挙動を実装するために、nil関数が呼び出されるべき場所で、Goランタイムが内部的にアドレス0へのジャンプを試みるようなコードパスを導入しました。

しかし、Windows環境では、プログラムがアドレス0(NULLポインタが指すアドレス)に直接ジャンプしようとすると、オペレーティングシステムが保護違反(アクセス違反)を検出し、Goプログラム全体がクラッシュするという問題が発生しました。これは、他のOSでは必ずしも同じように即座にクラッシュするとは限らない、Windows特有の挙動でした。

このコミット(CL 105120044)は、CL 105140044によって引き起こされたこのWindows固有のクラッシュを解決することを目的としています。具体的には、アドレス0への直接ジャンプを再び不可能にし、defer nilがWindows上でも期待通りにGoランタイムが管理するパニックとして処理されるように修正しています。

前提知識の解説

Goランタイム (Go Runtime)

Goランタイムは、Goプログラムの実行を管理する低レベルなシステムです。これには、ガベージコレクション、ゴルーチン(軽量スレッド)のスケジューリング、スタック管理、プリミティブな同期メカニズムなどが含まれます。Goプログラムは、OSのシステムコールを直接呼び出すのではなく、ランタイムを介してこれらの操作を行います。

defer

deferはGo言語のキーワードで、そのdefer文を含む関数がリターンする直前に、指定された関数(またはメソッド)を実行するようにスケジュールします。これは、リソースの解放(ファイルやネットワーク接続のクローズ)、ロックの解除、エラーハンドリングなど、クリーンアップ処理によく使用されます。deferされた関数は、呼び出し元の関数が正常に終了した場合でも、パニックによって終了した場合でも実行されます。

nil関数値とdeferの挙動

Goでは、関数を変数に代入したり、関数の引数として渡したりすることができます。このとき、関数値がnilである場合があります。defer文でnilの関数値を指定した場合、Goの仕様では、defer文が評価された時点ではパニックは発生せず、実際にそのdeferされた関数が実行される(つまり、呼び出し元の関数がリターンする)時点でパニック(panic: runtime error: invalid memory address or nil pointer dereference)が発生します。

アドレス0へのジャンプとNULLポインタ参照

コンピュータのメモリ空間において、アドレス0は通常、特別な意味を持ちます。オペレーティングシステムは、不正なメモリアクセスを防ぐために、アドレス0を含む特定のメモリ領域へのアクセスを制限しています。プログラムが意図せず、または誤ってアドレス0を指すポインタ(NULLポインタ)を逆参照しようとしたり、アドレス0に直接ジャンプしようとしたりすると、OSは保護違反(例: セグメンテーション違反、アクセス違反)を発生させ、プログラムの実行を停止させます。これは、プログラムのバグやセキュリティ上の脆弱性を防ぐための重要なメカニズムです。特にWindowsでは、アドレス0への直接ジャンプは厳しく制限されており、即座にクラッシュを引き起こす傾向があります。

CL (Change List)

CLは、Goプロジェクトにおけるコード変更の単位を指します。GoプロジェクトはGerritというコードレビューシステムを使用しており、各変更はCLとして提出され、レビューを経てマージされます。コミットメッセージに記載されるhttps://golang.org/cl/XXXXXという形式のURLは、そのCLのGerritページへのリンクです。

GobufFuncVal

これらはGoランタイム内部で使用されるデータ構造です。

  • Gobuf: ゴルーチンの実行コンテキスト(レジスタの状態、スタックポインタ、プログラムカウンタなど)を保存するための構造体です。ゴルーチンの切り替え時に、現在のゴルーチンの状態を保存し、別のゴルーチンの状態を復元するために使用されます。
  • FuncVal: 関数ポインタとその関連データ(クロージャの場合の環境ポインタなど)をカプセル化する構造体です。Goの関数値は、単なるコードへのポインタだけでなく、その関数が実行されるために必要な追加情報も含むことがあります。

#pragma textflag NOSPLIT

これはGoコンパイラ/リンカに対する指示(プラグマ)です。NOSPLITは、この関数がスタックの分割(stack split)を行わないことを意味します。Goランタイムは、ゴルーチンのスタックが不足しそうになったときに、自動的にスタックを拡張する「スタック分割」というメカニズムを持っています。しかし、非常に短い関数や、スタックの切り替えがパフォーマンスに重大な影響を与える可能性のある低レベルなランタイム関数では、このスタック分割を無効にすることがあります。NOSPLITは、そのような関数がスタックの拡張をトリガーしないようにします。

*(byte*)0 = 0;

C言語のコードで、アドレス0のメモリ位置にバイト値0を書き込もうとする操作です。これは意図的にNULLポインタ参照を引き起こすための慣用的な方法です。この操作は、通常、オペレーティングシステムによって保護されたメモリ領域への書き込みを試みるため、保護違反(セグメンテーション違反やアクセス違反)を発生させ、プログラムをクラッシュさせます。Goランタイムでは、このような意図的なクラッシュを利用して、特定の条件下でパニックを発生させるために使用されることがあります。

技術的詳細

このコミットの技術的な核心は、Goランタイムがdefer nilのパニックを処理する方法を、WindowsのOSレベルの制約に合わせて調整した点にあります。

CL 105140044が導入したdefer nilの「遅延パニック」のメカニズムでは、nil関数が実際に呼び出されるべきタイミングで、Goランタイムは内部的にその関数ポインタ(この場合はnil、つまりアドレス0)にジャンプしようとしました。これは、Goの関数呼び出し規約と、nil関数が呼び出されたときにパニックを発生させるという意図に基づいています。

しかし、Windowsオペレーティングシステムは、セキュリティと安定性のために、アドレス0への直接的なコード実行(ジャンプ)を厳しく制限しています。このような試みは、Windowsのメモリ保護機構によって即座に検出され、アクセス違反(Access Violation)として処理され、GoプログラムがOSレベルでクラッシュしてしまいました。これは、Goランタイムが期待する「パニック」とは異なり、より深刻な、制御不能な終了でした。

このコミット(CL 105120044)は、この問題を解決するために、src/pkg/runtime/stack.c内のruntime·gostartcallfn関数を変更しました。この関数は、ゴルーチンの呼び出しスタックをセットアップする際に使用されます。変更前は、FuncVal(関数値)がnilの場合、直接nilアドレスを呼び出し先として設定していました。

変更後、FuncValnilの場合には、直接nilアドレスを呼び出す代わりに、新しく導入されたruntime·nilfuncという特別なランタイム関数を呼び出すように変更されました。

runtime·nilfuncは、以下の特徴を持っています。

  1. #pragma textflag NOSPLITでマークされています。これは、この関数がスタック分割を行わないことを保証し、低レベルなランタイム処理の予測可能性を高めます。
  2. 関数本体には*(byte*)0 = 0;というC言語のコードが含まれています。このコードは、意図的にアドレス0への書き込みを試み、NULLポインタ参照によるパニックを発生させます。

この修正により、defer nilが実行されるべきタイミングで、Windows上でもアドレス0への直接ジャンプによるOSレベルのクラッシュではなく、runtime·nilfuncが実行され、その内部で意図的にNULLポインタ参照パニックが引き起こされます。これにより、Goランタイムがこのパニックを捕捉し、Goの通常のパニック処理フロー(スタックトレースの出力など)に従って処理できるようになり、Windows上でのdefer nilの挙動が他のプラットフォームと一貫するようになりました。

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

--- a/src/pkg/runtime/stack.c
+++ b/src/pkg/runtime/stack.c
@@ -9,6 +9,7 @@
  #include "funcdata.h"\n #include "typekind.h"\n #include "type.h"\n+#include "../../cmd/ld/textflag.h"\n \n enum\n {\n@@ -851,6 +852,13 @@ runtime·newstack(void)\n  \t*(int32*)345 = 123;\t// never return\n }\n \n+#pragma textflag NOSPLIT\n+void\n+runtime·nilfunc(void)\n+{\n+\t*(byte*)0 = 0;\n+}\n+\n // adjust Gobuf as if it executed a call to fn\n // and then did an immediate gosave.\n void\n@@ -858,9 +866,10 @@ runtime·gostartcallfn(Gobuf *gobuf, FuncVal *fv)\n {\n  \tvoid *fn;\n \n-\tfn = nil;\n  \tif(fv != nil)\n  \t\tfn = fv->fn;\n+\telse\n+\t\tfn = runtime·nilfunc;\n  \truntime·gostartcall(gobuf, fn, fv);\n }\n \n```

## コアとなるコードの解説

このコミットによる主要な変更点は以下の2つです。

1.  **`runtime·nilfunc`関数の追加**:
    `src/pkg/runtime/stack.c`に、`runtime·nilfunc`という新しい関数が追加されました。
    ```c
    #pragma textflag NOSPLIT
    void
    runtime·nilfunc(void)
    {
    	*(byte*)0 = 0;
    }
    ```
    *   `#pragma textflag NOSPLIT`: このプラグマは、Goコンパイラ/リンカに対して、この関数がスタック分割を行わないことを指示します。これは、この関数が非常に低レベルなランタイム処理の一部であり、スタックの自動拡張によるオーバーヘッドや予測不能な挙動を避けるために使用されます。
    *   `*(byte*)0 = 0;`: この行が`runtime·nilfunc`の核心です。これはC言語のコードで、アドレス0のメモリ位置にバイト値0を書き込もうとします。前述の通り、これは意図的にNULLポインタ参照を引き起こし、オペレーティングシステムによって保護違反(アクセス違反)を発生させるためのものです。Goランタイムは、このOSレベルの保護違反を捕捉し、Goの通常のパニック(`panic: runtime error: invalid memory address or nil pointer dereference`)として処理します。

2.  **`runtime·gostartcallfn`関数の変更**:
    `runtime·gostartcallfn`関数は、ゴルーチンの呼び出しスタックをセットアップする際に使用されるランタイム関数です。この関数が、`nil`の関数値が渡された場合の挙動を変更しました。
    ```diff
    --- a/src/pkg/runtime/stack.c
    +++ b/src/pkg/runtime/stack.c
    @@ -858,9 +866,10 @@ runtime·gostartcallfn(Gobuf *gobuf, FuncVal *fv)
     {
      	void *fn;
     
    -	fn = nil;
      	if(fv != nil)
      		fn = fv->fn;
    +	else
    +		fn = runtime·nilfunc;
      	runtime·gostartcall(gobuf, fn, fv);
     }
    ```
    *   変更前は、`fv`(`FuncVal`、関数値)が`nil`の場合、`fn`(関数ポインタ)に直接`nil`を代入していました。これにより、`runtime·gostartcall`が最終的に`nil`アドレスへのジャンプを試み、Windowsでクラッシュを引き起こしていました。
    *   変更後は、`fv`が`nil`の場合、`fn`に新しく追加された`runtime·nilfunc`のアドレスを代入するようになりました。
    *   この変更により、`nil`関数が呼び出されるべきタイミングで、直接`nil`アドレスにジャンプする代わりに`runtime·nilfunc`が実行されます。`runtime·nilfunc`は意図的にNULLポインタ参照パニックを引き起こすため、Goランタイムがこのパニックを捕捉し、Windows上でも期待通りの`defer nil`のパニック挙動が実現されるようになりました。

## 関連リンク
*   Go Issue #8047: コミットメッセージで修正されたと記載されているIssue。直接的な詳細は見つかりませんでしたが、このコミットが解決した問題を示しています。
*   Go Issue #8109: `defer nil`の「遅延パニック」挙動に関する議論のIssue。このコミットの背景にある設計思想を理解する上で重要です。
*   CL 105140044: [https://golang.org/cl/105140044](https://golang.org/cl/105140044) - このコミットが修正している、Windowsでの問題を引き起こした元の変更。
*   CL 105120044: [https://golang.org/cl/105120044](https://golang.org/cl/105120044) - このコミット自体のGerritページ。

## 参考にした情報源リンク
*   [https://github.com/golang/go/commit/060a988011b34ded3e002e1a4cb138b7ed21b176](https://github.com/golang/go/commit/060a988011b34ded3e002e1a4cb138b7ed21b176) (Go GitHubリポジトリ上のコミットページ)
*   Google Web Search (golang CL 105140044, golang CL 105120044, Go issue 8109 などのキーワードで検索)