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

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

このコミットは、GoランタイムのWindows/AMD64アーキテクチャにおけるusleep2関数、具体的にはsrc/pkg/runtime/sys_windows_amd64.sファイル内のアセンブリコードに対する修正です。この修正は、Windows API関数を呼び出す前にスタックを適切にアラインメントすることで、潜在的なクラッシュや未定義の動作を防ぐことを目的としています。

コミット

commit 418b39d436ceda146bbbced0bd716bff2f8371e2
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Thu Jul 10 14:23:50 2014 +1000

    runtime: align stack before calling windows in usleep2
    
    Fixes #8174.
    
    LGTM=minux
    R=golang-codereviews, minux
    CC=golang-codereviews
    https://golang.org/cl/102360043

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

https://github.com/golang/go/commit/418b39d436ceda146bbbced0bd716bff2f8371e2

元コミット内容

このコミットは、Goランタイムのusleep2関数において、Windows APIを呼び出す直前にスタックのアラインメントを修正するものです。これにより、Windowsの呼び出し規約に準拠し、NtWaitForSingleObjectのようなシステムコールが正しく実行されるようにします。

変更の背景

この変更は、GoのIssue #8174「not aligned stack before call into NtWaitForSingleObject in usleep2」を修正するために行われました。Windows x64の呼び出し規約では、CALL命令を実行する前にスタックポインタ(RSP)が16バイト境界にアラインされている必要があります。Goランタイムのusleep2関数は、内部的にWindowsの低レベルAPIであるNtWaitForSingleObjectを呼び出してスリープ処理を実現しています。しかし、このAPI呼び出しの前にスタックが適切にアラインされていない場合、未定義の動作、クラッシュ、またはパフォーマンスの低下を引き起こす可能性がありました。

特に、SSE命令など128ビットレジスタを使用する操作では、アラインされたメモリアクセス(movdqaなど)がアラインされていないアクセス(movdquなど)よりもはるかに効率的です。スタックが適切にアラインされていないと、これらの最適化が利用できず、パフォーマンスに悪影響を与えるだけでなく、データ破損やプログラムの不安定性を招くこともあります。このコミットは、この重要な規約違反を修正し、GoプログラムがWindows上で安定して動作するようにするためのものです。

前提知識の解説

  • Windows x64 呼び出し規約 (x64 Calling Convention): Windows 64ビット環境における関数呼び出しの標準的な規約です。これには、引数の渡し方(最初の4つの整数/ポインタ引数はRCX, RDX, R8, R9レジスタで渡され、浮動小数点引数はXMM0-XMM3で渡される)、スタックの使用方法、レジスタの保存/復元ルールなどが含まれます。最も重要な点の一つは、CALL命令の直前にはスタックポインタ(RSP)が16バイト境界にアラインされている必要があるという要件です。関数が呼び出されると、リターンアドレス(8バイト)がスタックにプッシュされるため、関数エントリ時点ではRSPは16バイトアラインメントから8バイトずれた状態になります。
  • スタックアラインメント (Stack Alignment): メモリ上のデータが特定のバイト境界に配置されることを指します。CPUは、アラインされたデータにアクセスする方が、アラインされていないデータにアクセスするよりも効率的です。特にSIMD(Single Instruction, Multiple Data)命令(例: SSE)は、データが特定の境界にアラインされていることを前提とすることが多く、アラインされていないデータに対してはパフォーマンスが低下したり、ハードウェア例外が発生したりすることがあります。
  • usleep2関数: Goランタイムの内部関数で、Windows環境における低レベルのスリープ処理を担当します。ユーザーがGoのtime.Sleep()を呼び出すと、最終的にこのusleep2のようなOS固有の関数が呼び出され、指定された期間だけゴルーチンを一時停止させます。
  • NtWaitForSingleObject: Windows Native APIの一部であり、ntdll.dllによってエクスポートされる低レベルのシステムコールです。この関数は、指定されたカーネルオブジェクト(イベント、ミューテックス、プロセス、スレッドなど)がシグナル状態になるか、タイムアウト期間が経過するまで、現在のスレッドの実行を一時停止するために使用されます。これは、Windowsにおけるスレッド同期とプロセス間通信の基本的なメカニズムです。通常、アプリケーション開発者はより高レベルのWin32 APIであるWaitForSingleObjectを使用しますが、これは内部的にNtWaitForSingleObjectを呼び出します。
  • Goアセンブリ (TEXT, NOSPLIT, $framesize):
    • TEXT: Goアセンブリで関数を定義するためのディレクティブです。
    • NOSPLIT: このフラグは、Goアセンブリ関数にスタックオーバーフローチェック(スタック分割プリアンブル)を挿入しないようにコンパイラに指示します。これは、スタックの使用量が非常に少なく、固定されていることが保証される低レベルのランタイムコードで主に使用されます。NOSPLIT関数は、スタックチェックのオーバーヘッドを回避し、ゴルーチンが関数実行中にプリエンプトされないようにするために使われます。
    • $framesize: TEXTディレクティブの引数で、関数のスタックフレームサイズ(ローカル変数や保存されたレジスタのために確保されるスタック領域)を指定します。

技術的詳細

このコミットの技術的詳細の中心は、src/pkg/runtime/sys_windows_amd64.sファイル内のruntime·usleep2アセンブリ関数の修正です。

元のコードでは、runtime·usleep2関数のスタックフレームサイズは$8バイトと宣言されていました。これは、関数が自身のローカル変数や保存されたレジスタのために8バイトのスタック領域を使用することを意味します。しかし、Windows x64の呼び出し規約では、CALL命令の直前にスタックポインタが16バイト境界にアラインされている必要があります。

修正前は、usleep2NtWaitForSingleObjectを呼び出す際に、スタックが16バイトアラインメントを満たしていない可能性がありました。これは、usleep2が呼び出される前のスタックの状態に依存します。

修正では、以下の変更が加えられました。

  1. スタックフレームサイズの変更: TEXT runtime·usleep2(SB),NOSPLIT,$8TEXT runtime·usleep2(SB),NOSPLIT,$16に変更されました。これにより、usleep2関数は自身のスタックフレームとして16バイトを確保するようになります。
  2. スタックアラインメントの強制:
    • MOVQ SP, AX: 現在のスタックポインタ(SP)の値を一時的にAXレジスタに保存します。これは、アラインメント後に元のSPに戻すために必要です。
    • ANDQ $~15, SP: この命令がスタックアラインメントの核心です。$~15はビットマスクで、バイナリで...11110000となります(16の補数表現で-16)。SPとこのマスクのビットAND演算を行うことで、SPの下位4ビット(0-15)がクリアされ、SPが最も近い16の倍数に切り捨てられます。これにより、SPが16バイト境界に強制的にアラインされます。
    • MOVQ AX, 8(SP): 元のSPの値を、新しくアラインされたSPから8バイトオフセットした位置に保存します。これは、関数が終了する際に元のSPに戻すための準備です。
  3. スタックの復元: CALL AXNtWaitForSingleObjectの呼び出し)の後に、MOVQ 8(SP), SP命令が追加されました。これは、アラインメントのために一時的に変更されたSPを、関数エントリ時に保存しておいた元のSPの値に戻すためのものです。これにより、usleep2関数が終了する際に、呼び出し元のスタックフレームが正しく復元されます。

これらの変更により、NtWaitForSingleObjectが呼び出される直前には、スタックポインタが常に16バイト境界にアラインされることが保証されます。

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

--- a/src/pkg/runtime/sys_windows_amd64.s
+++ b/src/pkg/runtime/sys_windows_amd64.s
@@ -367,7 +367,10 @@ usleep1_ret:
 	RET

 // Runs on OS stack. duration (in 100ns units) is in BX.
-TEXT runtime·usleep2(SB),NOSPLIT,$8
+TEXT runtime·usleep2(SB),NOSPLIT,$16
+\tMOVQ\tSP, AX
+\tANDQ\t$~15, SP\t// alignment as per Windows requirement
+\tMOVQ\tAX, 8(SP)\
 	// Want negative 100ns units.
 	NEGQ\tBX
 	MOVQ\tSP, R8 // ptime
@@ -376,4 +379,5 @@ TEXT runtime·usleep2(SB),NOSPLIT,$8
 	MOVQ\t$0, DX // alertable
 	MOVQ\truntime·NtWaitForSingleObject(SB), AX
 	CALL\tAX
+\tMOVQ\t8(SP), SP\
 	RET

コアとなるコードの解説

変更されたruntime·usleep2関数は、GoランタイムがWindows上でスリープ処理を行うためのアセンブリコードです。

  1. TEXT runtime·usleep2(SB),NOSPLIT,$16:

    • TEXT: runtime·usleep2という名前の関数を定義します。SBはシンボルベースレジスタで、グローバルシンボルを参照するために使用されます。
    • NOSPLIT: この関数はスタックオーバーフローチェックを行いません。これは、この関数が非常に短く、スタック使用量が固定されており、ランタイムの低レベル部分で実行されるためです。
    • $16: この関数のスタックフレームサイズを16バイトに設定します。これは、以前の8バイトから増加しており、スタックアラインメントのための領域を確保するためです。
  2. MOVQ SP, AX:

    • 現在のスタックポインタ(SP)の値をAXレジスタに移動します。これは、スタックアラインメントのためにSPを変更する前に、元のSPの値を一時的に保存するためです。
  3. ANDQ $~15, SP:

    • SPレジスタの値を(~15)(つまり0xFFFFFFFFFFFFFFF0)とビットAND演算します。これにより、SPの下位4ビットがクリアされ、SPが16バイトの倍数に切り捨てられます。これは、Windows x64呼び出し規約で要求される16バイトスタックアラインメントを強制するための重要なステップです。
  4. MOVQ AX, 8(SP):

    • AXレジスタに保存されていた元のSPの値を、新しくアラインされたSPから8バイトオフセットしたメモリ位置に保存します。これは、NtWaitForSingleObject呼び出し後にスタックを元の状態に戻すために使用されます。
  5. CALL AX:

    • AXレジスタに格納されているアドレス(この場合はruntime·NtWaitForSingleObjectのアドレス)にある関数を呼び出します。このCALL命令の直前で、スタックは16バイト境界にアラインされていることが保証されます。
  6. MOVQ 8(SP), SP:

    • NtWaitForSingleObjectの呼び出しが完了した後、スタックを元の状態に戻します。8(SP)に保存されていた元のSPの値をSPレジスタに移動します。これにより、usleep2関数がリターンする際に、呼び出し元のスタックフレームが正しく復元されます。

これらの変更により、GoランタイムはWindows x64環境でNtWaitForSingleObjectのようなシステムコールを安全かつ効率的に呼び出すことができるようになり、スタックアラインメントに関連する潜在的なバグやクラッシュが解消されます。

関連リンク

参考にした情報源リンク