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

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

このコミットは、GoランタイムのWindows/amd64アーキテクチャにおけるスタック管理に関する修正です。具体的には、src/pkg/runtime/sys_windows_amd64.s ファイル内のruntime·badcallback関数のスタック管理方法が変更されています。このファイルは、Goランタイムの低レベルなシステムコールやアセンブリコードを定義しており、Windows上のAMD64プロセッサに特化した処理を記述しています。

コミット

commit b2a9079e54dc4e1e97551b8c60f2077888a544dc
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Thu Mar 15 02:24:49 2012 +0800

    runtime: manage stack by ourselves for badcallback on windows/amd64
    This function uses 48-byte of precious non-split stack for every callback
    function, and without this CL, it can easily overflow the non-split stack.
    I encountered this when trying to enable misc/cgo/test on windows/amd64.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5784075

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

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

元コミット内容

runtime: manage stack by ourselves for badcallback on windows/amd64
This function uses 48-byte of precious non-split stack for every callback
function, and without this CL, it can easily overflow the non-split stack.
I encountered this when trying to enable misc/cgo/test on windows/amd64.

R=rsc
CC=golang-dev
https://golang.org/cl/5784075

変更の背景

この変更は、GoランタイムがWindows/amd64環境でbadcallback関数を処理する際に発生するスタックオーバーフローの問題を解決するために行われました。

badcallback関数は、何らかの異常なコールバックが発生した際に呼び出されるGoランタイム内部の関数であると推測されます。コミットメッセージによると、この関数はコールバックごとに48バイトの「貴重な非分割スタック (precious non-split stack)」を使用しており、このコミットが適用される前は、このスタックが容易にオーバーフローする可能性がありました。

具体的な問題は、Windows/amd64上でmisc/cgo/testを有効にしようとした際に顕在化しました。cgoはGoプログラムからC言語のコードを呼び出すためのメカニズムであり、C言語のコールバックがGoに渡される際にbadcallbackのようなランタイム関数が関与することが考えられます。多数のコールバックが発生するシナリオにおいて、badcallbackが消費するスタック領域が蓄積され、最終的にスタックオーバーフローを引き起こしていたと推測されます。

この問題は、Goの設計思想である軽量なゴルーチンと効率的なスタック管理に反するものであり、安定した動作を保証するために修正が必要でした。

前提知識の解説

Goランタイムとスタック管理

Go言語は、軽量な並行処理の単位である「ゴルーチン (goroutine)」を特徴としています。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数百万ものゴルーチンを同時に実行することが可能です。これを可能にしているのが、Goランタイムの高度なスタック管理メカニズムです。

  • 動的なスタックサイズ: Goのゴルーチンは、非常に小さなスタックサイズ(現代のGoバージョンでは通常2KB)で開始します。関数呼び出しによってスタックが必要になると、Goランタイムは自動的にスタックを拡張します。逆に、スタックが不要になると縮小することもあります。これにより、メモリ使用量を効率的に抑えつつ、スタックオーバーフローのリスクを軽減しています。
  • 連続スタック (Contiguous Stacks): Go 1.4以降、ランタイムは「連続スタック」戦略を採用しています。これは、スタックが不足した場合に、より大きな新しいメモリブロックを割り当て、古いスタックの内容を新しい場所にコピーし、関連するすべてのポインタを更新することでスタックを拡張する方式です。これにより、スタックがメモリ上で連続していることが保証され、ポインタの扱いが単純化されます。
  • スタックチェック (Stack Checks): Goコンパイラは、各関数の冒頭に「スタックチェック」を挿入します。これにより、関数を実行するのに十分なスタック空間があるかどうかが判断されます。もし不足していれば、ランタイムのmorestack関数が呼び出され、スタック拡張メカニズムがトリガーされます。
  • 非分割スタック (Non-split Stack): 通常のGo関数は、動的に拡張・縮小する「分割スタック (split stack)」を使用します。しかし、ランタイムの非常に低レベルな部分や、特定のシステムコール、Cgoコールバックなど、スタックの拡張・縮小処理自体が困難または不適切な状況では、「非分割スタック」が使用されることがあります。非分割スタックは固定サイズであり、動的な拡張が行われないため、そのサイズを超えるとスタックオーバーフローが発生しやすくなります。コミットメッセージの「precious non-split stack」という表現は、この固定された限られたスタック領域の重要性を示唆しています。

Windows/amd64アーキテクチャ

Windowsオペレーティングシステム上で動作するAMD64(x86-64)アーキテクチャは、64ビットのレジスタと命令セットを持ちます。関数呼び出し規約(calling convention)は、スタックフレームの構築、引数の渡し方、戻り値の扱いなどを定義します。Windows/amd64では、Microsoft x64 calling conventionが一般的であり、最初の4つの整数またはポインタ引数はRCX, RDX, R8, R9レジスタで渡され、それ以降の引数はスタックにプッシュされます。また、関数呼び出し時には、呼び出し元が呼び出し先のレジスタ使用のためにシャドウスペース(shadow space)と呼ばれる領域をスタック上に確保することがあります。

badcallback関数

Goランタイムにおけるbadcallback関数は、通常、予期せぬ、または不正なコールバックが発生した場合に呼び出されるエラーハンドリングまたはデバッグ目的の関数であると推測されます。例えば、Cgoを介してGoに渡されたコールバックが、Goランタイムが想定しない状態であったり、無効なポインタを渡したりした場合に、この関数が呼び出される可能性があります。このような関数は、システムの安定性を保つために、エラー情報を記録したり、プログラムを安全に終了させたりする役割を担います。

技術的詳細

このコミットの技術的な核心は、runtime·badcallback関数が、Goランタイムの通常のスタック管理メカニズム(動的なスタック拡張)の恩恵を受けられない「非分割スタック」上で実行されるという点にあります。

コミットメッセージによると、badcallback関数は、その処理のために48バイトのスタック領域を必要としていました。しかし、この48バイトは、関数が呼び出されるたびに非分割スタック上に確保されるため、多数のコールバックが発生すると、この固定されたスタック領域がすぐに枯渇し、スタックオーバーフローを引き起こしていました。

この問題は、Goのmisc/cgo/testをWindows/amd64で有効にしようとした際に発見されました。cgoのテストスイートは、GoとCの間の相互運用性を広範にテストするため、多数のコールバックや異なるスタックコンテキストでの実行を伴う可能性があります。これにより、badcallbackが頻繁に呼び出され、スタックオーバーフローが再現されたと考えられます。

解決策として、Goランタイムはbadcallback関数自身のスタック管理を「自己管理 (manage stack by ourselves)」することを選択しました。これは、Goコンパイラが自動的に挿入するスタックチェックや動的なスタック拡張に頼るのではなく、アセンブリコード内で明示的にスタックポインタ(SPレジスタ)を操作して、必要なスタック領域を確保・解放するというアプローチです。

これにより、badcallback関数は、その実行に必要な48バイトのスタック領域を、非分割スタックの制約を受けずに、自身で適切に管理できるようになり、スタックオーバーフローが回避されます。

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

変更はsrc/pkg/runtime/sys_windows_amd64.sファイルにあります。

--- a/src/pkg/runtime/sys_windows_amd64.s
+++ b/src/pkg/runtime/sys_windows_amd64.s
@@ -60,7 +60,11 @@ loadregs:
 
 	RET
 
-TEXT runtime·badcallback(SB),7,$48
+// This should be called on a system stack,
+// so we don't need to concern about split stack.
+TEXT runtime·badcallback(SB),7,$0
+	SUBQ	$48, SP
+
 	// stderr
 	MOVQ	$-12, CX // stderr
 	MOVQ	CX, 0(SP)
@@ -80,6 +84,7 @@ TEXT runtime·badcallback(SB),7,$48
 	MOVQ	runtime·WriteFile(SB), AX
 	CALL	AX
 	
+	ADDQ	$48, SP
 	RET
 
 TEXT runtime·badsignal(SB),7,$48

主な変更点は以下の通りです。

  1. TEXT runtime·badcallback(SB),7,$48TEXT runtime·badcallback(SB),7,$0 に変更されました。
    • これは、Goのアセンブリ言語における関数定義の構文です。$48は、この関数が呼び出し時に確保するスタックフレームのサイズを示していました。これを$0に変更することで、Goランタイムがこの関数に対して自動的にスタック領域を確保しないように指示しています。
  2. SUBQ $48, SP が追加されました。
    • これは、関数の冒頭でスタックポインタ(SPレジスタ)から48を減算する命令です。これにより、関数が自身で48バイトのスタック領域を確保します。SUBQは64ビット値の減算です。
  3. ADDQ $48, SP が追加されました。
    • これは、関数の末尾(RET命令の直前)でスタックポインタに48を加算する命令です。これにより、関数が確保した48バイトのスタック領域を解放し、スタックポインタを元の位置に戻します。

コアとなるコードの解説

この変更は、runtime·badcallback関数がGoランタイムの自動的なスタック管理に依存するのではなく、自身でスタックを管理するように修正されたことを示しています。

  • TEXT runtime·badcallback(SB),7,$48 から $0 への変更:

    • 元のコードでは、Goコンパイラ/アセンブラに対して、badcallback関数が呼び出された際に48バイトのスタックフレームを自動的に確保するように指示していました。しかし、この関数が「非分割スタック」上で実行されるため、この自動的な確保が問題を引き起こしていました。
    • $0に変更することで、Goランタイムはbadcallbackに対してスタックフレームを自動的に確保しなくなります。これにより、この関数はGoの通常のスタック拡張メカニズムの対象外となります。
  • SUBQ $48, SP の追加:

    • 関数が実行を開始すると、まずこの命令が実行されます。SPレジスタは現在のスタックのトップを指しています。SUBQ $48, SPは、スタックポインタを48バイト分「下げる」(アドレスを減らす)ことで、スタック上に48バイトの新しい領域を確保します。この領域は、badcallback関数がローカル変数や一時的なデータを格納するために使用できます。
  • ADDQ $48, SP の追加:

    • 関数が終了する直前、RET命令で呼び出し元に戻る前に、この命令が実行されます。ADDQ $48, SPは、スタックポインタを48バイト分「上げる」(アドレスを増やす)ことで、関数が冒頭で確保した48バイトのスタック領域を解放します。これにより、スタックは関数が呼び出される前の状態に戻り、スタックの整合性が保たれます。

この修正により、badcallback関数は、Goランタイムの通常のスタック管理とは独立して、必要なスタック領域を明示的に確保・解放するようになりました。これにより、非分割スタックの制約下でもスタックオーバーフローを回避し、Windows/amd64環境でのcgoの安定性が向上しました。

関連リンク

  • Go言語のスタック管理に関する公式ドキュメントやブログ記事は、Goのバージョンアップに伴い内容が変化している可能性があります。最新の情報はGoの公式ドキュメントを参照してください。
  • Goのランタイムソースコード: src/runtime/ ディレクトリには、Goランタイムのスタック管理に関する詳細な実装が含まれています。

参考にした情報源リンク