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

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

このコミットは、Goランタイムのsrc/pkg/runtime/lock_futex.cファイルに対する変更です。このファイルは、Linuxシステムにおけるミューテックス(相互排他ロック)の実装、特にfutex(Fast Userspace Mutex)システムコールを利用した同期プリミティブに関連するコードを含んでいます。futexは、ユーザー空間でロックを効率的に実装するためのLinuxカーネルの機能です。

コミット

commit d91219e458823a26a447c57c616f8ddf5adf4c9a
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Jul 29 22:59:30 2013 +0400

    runtime: fix linux/arm build
    notetsleep: nosplit stack overflow
            128     assumed on entry to notetsleep
            80      after notetsleep uses 48
            44      after runtime.futexsleep uses 36
            -12     after runtime.timediv uses 56
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/12049043

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

https://github.com/golang/go/commit/d91219e458823a26a447c57c616f8ddf5adf4c9a

元コミット内容

diff --git a/src/pkg/runtime/lock_futex.c b/src/pkg/runtime/lock_futex.c
index 4fabc76944..3f8d632363 100644
--- a/src/pkg/runtime/lock_futex.c
+++ b/src/pkg/runtime/lock_futex.c
@@ -138,9 +138,11 @@ runtime·notesleep(Note *n)
 
 #pragma textflag 7
 static bool
-notetsleep(Note *n, int64 ns)
+notetsleep(Note *n, int64 ns, int64 deadline, int64 now)
 {
-\tint64 deadline, now;\n+\t// Conceptually, deadline and now are local variables.\n+\t// They are passed as arguments so that the space for them\n+\t// does not count against our nosplit stack sequence.\n \n \tif(ns < 0) {\n \t\twhile(runtime·atomicload((uint32*)&n->key) == 0)\n@@ -174,7 +176,7 @@ runtime·notetsleep(Note *n, int64 ns)\n 
 \tif(m->profilehz > 0)\n \t\truntime·setprof(false);\n-\tres = notetsleep(n, ns);\n+\tres = notetsleep(n, ns, 0, 0);\n \tif(m->profilehz > 0)\n \t\truntime·setprof(true);\n \treturn res;\n@@ -192,7 +194,7 @@ runtime·notetsleepg(Note *n, int64 ns)\n \t\truntime·throw(\"notetsleepg on g0\");\n \n \truntime·entersyscallblock();\n-\tres = notetsleep(n, ns);\n+\tres = notetsleep(n, ns, 0, 0);\n \truntime·exitsyscall();\n \treturn res;\n }\n```

## 変更の背景

このコミットは、Linux/ARMアーキテクチャでのGoランタイムのビルドにおけるバグ修正を目的としています。具体的には、`notetsleep`関数が`nosplit`関数としてマークされているにもかかわらず、その内部でローカル変数(`deadline`と`now`)を宣言していたために、スタックオーバーフローが発生するという問題がありました。

Goランタイムには、スタックの拡張を許可しない`nosplit`関数という特殊なカテゴリの関数が存在します。これらの関数は、スタックの境界チェックを行わないため、非常に限られたスタック空間しか使用できません。通常、これらの関数は、スタックの拡張処理自体や、スタックの拡張が不可能なコンテキスト(例えば、システムコールに入る直前や、アセンブリコードから呼び出される低レベルのランタイム関数)で使用されます。

`notetsleep`関数は、Goのスケジューラがゴルーチンをスリープさせる際に使用される低レベルの関数であり、`futex`システムコールを介して実装されています。この関数が`nosplit`としてマークされているにもかかわらず、内部でローカル変数を宣言すると、その変数がスタック上に割り当てられ、`nosplit`関数に許容されるスタックサイズを超過してしまう可能性がありました。コミットメッセージに記載されているスタック使用量の数値(`notetsleep`が48バイト、`runtime.futexsleep`が36バイト、`runtime.timediv`が56バイトを使用し、合計でスタックが負の値になる)は、この問題の深刻さを示しています。特にARMのような組み込みシステムでは、スタックサイズが厳しく制限されることが多いため、このような問題が顕在化しやすくなります。

## 前提知識の解説

### Goランタイムのスタック管理

Go言語のゴルーチンは、非常に軽量なスレッドであり、それぞれが独自のスタックを持っています。Goランタイムは、必要に応じてゴルーチンのスタックを動的に拡張するメカニズムを持っています。これは、スタックの先頭にガードページを配置し、スタックポインタがそのページに触れるとスタックオーバーフローを検知し、より大きなスタックを割り当てて内容をコピーすることで実現されます。

### `nosplit`関数

`nosplit`関数は、Goコンパイラによって生成される特殊な関数です。これらの関数は、スタックの拡張チェックを行わないようにコンパイラに指示する`//go:nosplit`ディレクティブ(または`#pragma textflag 7`のようなアセンブリレベルの指示)によってマークされます。`nosplit`関数は、スタックの拡張処理自体や、スタックの拡張が不可能な低レベルのコンテキスト(例えば、システムコールに入る前や、アセンブリコードから呼び出される関数)で使用されます。これらの関数は、非常に小さなスタック空間しか使用できないという制約があります。もし`nosplit`関数が許容されるスタックサイズを超えてローカル変数を割り当てたり、他の関数を呼び出したりすると、スタックオーバーフローが発生し、プログラムがクラッシュする可能性があります。

### `futex` (Fast Userspace Mutex)

`futex`はLinuxカーネルが提供するシステムコールで、ユーザー空間で効率的な同期プリミティブ(ミューテックス、セマフォなど)を実装するために使用されます。`futex`は、競合がない場合にはカーネルへの移行(システムコール)を避け、ユーザー空間で処理を完結させることでオーバーヘッドを削減します。競合が発生した場合にのみカーネルに処理を委ね、スリープやウェイクアップの操作を行います。Goランタイムのロック機構は、この`futex`を内部的に利用して、ゴルーチンのスリープやウェイクアップを効率的に行っています。

### `Note`構造体

Goランタイムにおける`Note`構造体は、ゴルーチンをスリープさせたりウェイクアップさせたりするための低レベルの同期プリミティブです。これは、`futex`システムコールをラップしたもので、Goの`sync.Mutex`や`sync.WaitGroup`などの高レベルな同期プリミティブの基盤として使用されます。`runtime·notesleep`関数は、この`Note`を使ってゴルーチンを指定された期間スリープさせる役割を担います。

## 技術的詳細

この問題の核心は、`notetsleep`関数が`nosplit`関数として定義されているにもかかわらず、その内部で`int64 deadline, now;`というローカル変数を宣言していた点にあります。`nosplit`関数は、スタックの拡張を許可しないため、関数内で使用されるすべてのスタック空間(ローカル変数、引数、呼び出し先の関数のスタックフレームなど)が、コンパイラが事前に決定した非常に小さな固定サイズに収まる必要があります。

コミットメッセージのスタック使用量の内訳は、この問題を明確に示しています。
- `notetsleep`へのエントリで仮定されるスタック使用量: 128バイト
- `notetsleep`が使用するスタック量: 48バイト(ローカル変数`deadline`と`now`の分)
- `runtime.futexsleep`が使用するスタック量: 36バイト
- `runtime.timediv`が使用するスタック量: 56バイト

これらの数値は、`notetsleep`が呼び出す他の関数(`runtime.futexsleep`や`runtime.timediv`)のスタック使用量と合算されると、`nosplit`関数に許容されるスタックサイズを超過し、結果としてスタックオーバーフローを引き起こすことを示唆しています。特に、`runtime.timediv`が56バイトという比較的大きなスタックを使用していることが問題の一因となっています。

この問題を解決するために、コミットでは`deadline`と`now`というローカル変数を`notetsleep`関数の引数として渡すように変更しました。

```c
static bool
notetsleep(Note *n, int64 ns, int64 deadline, int64 now)
{
	// Conceptually, deadline and now are local variables.
	// They are passed as arguments so that the space for them
	// does not count against our nosplit stack sequence.
	// ...
}

Goの呼び出し規約では、関数の引数は呼び出し元のスタックフレームに配置されます。したがって、deadlinenowを引数として渡すことで、これらの変数のためのスタック空間がnotetsleep自身のスタックフレームではなく、notetsleepを呼び出すruntime·notesleepruntime·notetsleepgのスタックフレームに割り当てられるようになります。これにより、notetsleep関数自身のスタック使用量が減少し、nosplitの制約を満たすことができるようになります。

引数として渡されるdeadlinenowの値は、呼び出し元では常に0に設定されています。これは、これらの変数がnotetsleep関数内で計算される一時的な値であり、呼び出し元から初期値を受け取る必要がないためです。この変更の目的は、あくまでスタック割り当ての場所を移動させることであり、変数の意味論的な役割を変更するものではありません。

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

変更はsrc/pkg/runtime/lock_futex.cファイル内のnotetsleep関数の定義と、その関数を呼び出しているruntime·notesleepおよびruntime·notetsleepg関数の部分です。

  1. notetsleep関数のシグネチャ変更:

    -notetsleep(Note *n, int64 ns)
    +notetsleep(Note *n, int64 ns, int64 deadline, int64 now)
    

    int64 deadlineint64 nowが新たな引数として追加されました。

  2. notetsleep関数内部のローカル変数宣言の削除:

    -\tint64 deadline, now;
    +\t// Conceptually, deadline and now are local variables.
    +\t// They are passed as arguments so that the space for them
    +\t// does not count against our nosplit stack sequence.
    

    以前ローカル変数として宣言されていたdeadlinenowの行が削除され、代わりにコメントが追加されています。

  3. runtime·notesleepからのnotetsleep呼び出しの変更:

    -\tres = notetsleep(n, ns);
    +\tres = notetsleep(n, ns, 0, 0);
    

    notetsleepを呼び出す際に、新しい引数0, 0が追加されました。

  4. runtime·notetsleepgからのnotetsleep呼び出しの変更:

    -\tres = notetsleep(n, ns);
    +\tres = notetsleep(n, ns, 0, 0);
    

    同様に、notetsleepを呼び出す際に、新しい引数0, 0が追加されました。

コアとなるコードの解説

この変更の目的は、notetsleep関数がnosplit関数であるという制約を遵守し、スタックオーバーフローを防ぐことです。

notetsleep関数は、#pragma textflag 7によってnosplit関数としてマークされています。これは、この関数がスタックの拡張をトリガーしないことを意味します。したがって、この関数内で使用されるスタック空間は非常に限られています。

変更前は、notetsleep関数内でint64 deadline, now;というローカル変数が宣言されていました。これらの変数は、関数が呼び出された際にその関数のスタックフレーム内に割り当てられます。nosplit関数では、このようなローカル変数の割り当てが、許容されるスタックサイズを超過する原因となる可能性がありました。特に、notetsleepがさらに他の関数(runtime.futexsleepruntime.timediv)を呼び出す場合、それらの関数のスタック使用量もnotetsleepのスタックフレームに加算されるため、問題が顕在化しやすくなります。

変更後は、deadlinenownotetsleep関数の引数として渡されるようになりました。Goの呼び出し規約では、関数の引数は呼び出し元のスタックフレームに配置されます。したがって、runtime·notesleepruntime·notetsleepgnotetsleepを呼び出す際に、これらの引数のためのスタック空間がruntime·notesleepruntime·notetsleepgのスタックフレームに割り当てられます。これにより、notetsleep関数自身のスタック使用量が減少し、nosplitの制約を満たすことができるようになります。

引数として渡される0, 0という値は、これらの変数がnotetsleep関数内で計算される一時的な値であり、呼び出し元から初期値を受け取る必要がないことを示しています。この変更は、変数の論理的な役割を変えることなく、スタック割り当てのメカニズムを変更することで、Linux/ARM環境でのスタックオーバーフローの問題を解決しています。

関連リンク

  • Go言語のスタック管理に関する公式ドキュメントやブログ記事
  • Linux futexシステムコールに関するドキュメント
  • Goランタイムのnosplit関数に関する議論や実装の詳細

参考にした情報源リンク

  • Go言語のソースコード (src/pkg/runtime/lock_futex.c)
  • Goのコミット履歴と関連するコードレビュー (https://golang.org/cl/12049043)
  • Goのスタック管理に関する一般的な知識
  • Linux futexの概念に関する一般的な知識
  • Goのnosplit関数の概念に関する一般的な知識