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

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

このコミットは、GoランタイムにおけるPlan 9オペレーティングシステム向けのシグナルハンドラが、runtime·memmove関数を使用する際に発生していた回帰バグを修正するものです。具体的には、runtime·memmoveが特定の条件下でSSE (Streaming SIMD Extensions) のMOVOU命令を使用するよう変更された結果、Plan 9カーネルのシグナルハンドラ(note handler)内で浮動小数点演算が禁止されているという制約に抵触し、問題を引き起こしていました。このコミットでは、runtime·memmoveの代わりにシンプルなバイトコピーループを使用することで、この問題を回避しています。

コミット

commit 274a8e3f56358dd8ab93aad1bbd750bcb2750296
Author: Anthony Martin <ality@pbrane.org>
Date:   Mon Dec 9 18:41:48 2013 -0500

    runtime: do not use memmove in the Plan 9 signal handler
    
    Fixes a regression introduced in revision 4cb93e2900d0.
    
    That revision changed runtime·memmove to use SSE MOVOU
    instructions for sizes between 17 and 256 bytes. We were
    using memmove to save a copy of the note string during
    the note handler. The Plan 9 kernel does not allow the
    use of floating point in note handlers (which includes
    MOVOU since it touches the XMM registers).
    
    Arguably, runtime·memmove should not be using MOVOU when
    GO386=387 but that wouldn't help us on amd64. It's very
    important that we guard against any future changes so we
    use a simple copy loop instead.
    
    This change is extracted from CL 9796043 (since that CL
    is still being ironed out).
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/34640045

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

https://github.com/golang/go/commit/274a8e3f56358dd8ab93aad1bbd750bcb2750296

元コミット内容

このコミットは、Goランタイムのmemmove関数が、特定のデータサイズ(17バイトから256バイト)のコピーにおいて、SSE (Streaming SIMD Extensions) のMOVOU命令を使用するように変更されたことによって引き起こされた回帰バグを修正するものです。

変更の背景

この変更の背景には、以前のコミット(リビジョン4cb93e2900d0)によって導入された回帰バグがあります。この回帰は、runtime·memmoveの実装が変更され、17バイトから256バイトの範囲のデータコピーにSSEのMOVOU命令を使用するようになったことに起因します。

問題は、GoランタイムがPlan 9オペレーティングシステム上でシグナル(Plan 9では「note」と呼ばれる)を処理する際に使用する「note handler」内にありました。Plan 9カーネルは、note handler内での浮動小数点演算の使用を厳しく制限しています。MOVOU命令はXMMレジスタ(SSE命令が使用するレジスタ)にアクセスするため、カーネルはこれを浮動小数点演算と見なし、note handler内での実行を許可しませんでした。

結果として、シグナルハンドラ内でruntime·memmoveを使用してエラー文字列(note string)をコピーしようとすると、不正な命令実行として扱われ、プログラムがクラッシュする可能性がありました。このコミットは、この特定の環境(Plan 9のシグナルハンドラ)において、memmoveがSSE命令を使用しないようにすることで、この問題を解決することを目的としています。

コミットメッセージには、「runtime·memmoveGO386=387の場合にMOVOUを使用すべきではないという議論もあるが、それはamd64には役立たない」と述べられており、将来の変更に対しても堅牢であるように、よりシンプルなコピーループを採用する重要性が強調されています。

前提知識の解説

Plan 9オペレーティングシステム

Plan 9 from Bell Labsは、ベル研究所で開発された分散オペレーティングシステムです。Unixの概念をさらに推し進め、すべてのリソース(ファイル、デバイス、ネットワーク接続など)をファイルシステムとして表現するという思想を特徴としています。シグナル処理に関しても、Unixのシグナルとは異なる「note」というメカニズムを使用します。

Plan 9のNote Handler

Plan 9における「note」は、Unixのシグナルに相当する非同期イベント通知メカニズムです。プロセスがnoteを受け取ると、カーネルはnote handlerと呼ばれる特定の関数を実行します。このnote handlerは、非常に制約の厳しい環境で実行されます。特に重要なのは、note handler内では浮動小数点演算(FPU: Floating-Point Unit)の使用が厳しく制限されるという点です。これは、カーネルがnote handlerの実行中にFPUの状態を保存・復元するオーバーヘッドを避けるため、またはFPUの状態が予測不能になることを防ぐためと考えられます。

memmove関数

memmoveは、C言語の標準ライブラリ関数の一つで、メモリブロックをコピーするために使用されます。memcpyと異なり、memmoveはコピー元とコピー先のメモリ領域が重なっていても正しく動作することを保証します。内部的には、効率を最大化するために、CPUの特定の命令セット(例: SSE)を利用して高速化されることがあります。

SSE (Streaming SIMD Extensions)

SSEは、Intelが開発したSIMD (Single Instruction, Multiple Data) 命令セットの拡張機能です。これにより、単一の命令で複数のデータ要素(例: 浮動小数点数や整数)に対して同じ演算を並行して実行できます。SSE命令は、通常、XMMレジスタと呼ばれる専用の128ビットレジスタを使用します。

MOVOU命令

MOVOUはSSE命令の一つで、「Move Unaligned Oword」の略です。これは、メモリからXMMレジスタへ、またはXMMレジスタからメモリへ、アラインメントされていない(メモリ境界に揃っていない)128ビット(16バイト)のデータを移動するために使用されます。MOVOU命令はXMMレジスタを操作するため、カーネルによっては浮動小数点演算の一部と見なされることがあります。

浮動小数点演算とXMMレジスタ

現代のCPUでは、浮動小数点演算はFPUによって処理され、その状態は専用のレジスタ(x87 FPUレジスタやSSEのXMMレジスタなど)に保存されます。OSのカーネルは、コンテキストスイッチ(プロセスやスレッドの切り替え)の際に、これらのレジスタの状態を保存・復元する必要があります。シグナルハンドラのような非同期処理のコンテキストでは、FPUの状態管理が複雑になるため、一部のOS(Plan 9など)では、ハンドラ内でのFPUの使用を制限することで、この複雑さを回避しています。MOVOU命令がXMMレジスタに触れるため、Plan 9カーネルはこれをFPU関連の操作と判断し、note handler内での実行を禁止したと考えられます。

技術的詳細

この問題の核心は、Goランタイムのruntime·memmove関数が、特定のサイズのメモリコピーを最適化するためにSSEのMOVOU命令を使用するようになったことです。この最適化自体は一般的なシステムではパフォーマンス向上に寄与しますが、Plan 9のnote handlerという特殊な実行コンテキストでは問題を引き起こしました。

Plan 9カーネルは、note handlerが実行される際に、FPUの状態を保存・復元するオーバーヘッドを避けるため、あるいはFPUの状態がハンドラ内で変更されることによる予測不能な挙動を防ぐために、note handler内での浮動小数点演算を禁止しています。MOVOU命令はXMMレジスタを操作するため、カーネルはこれを浮動小数点演算の一部と見なします。

したがって、runtime·memmoveMOVOU命令を使用すると、Plan 9のnote handler内で「浮動小数点演算の禁止」というカーネルの制約に違反することになり、結果として不正な命令実行やクラッシュが発生していました。

このコミットは、この特定のシナリオ(Plan 9のシグナルハンドラ内でのエラー文字列のコピー)において、runtime·memmoveの使用を避け、代わりにシンプルなバイト単位のコピーループを実装することで、この問題を回避しています。これにより、XMMレジスタへのアクセスがなくなるため、Plan 9カーネルの制約に抵触することなく、安全にメモリコピーを実行できるようになります。

コミットメッセージにある「TODO(ality): revert back to memmove when the kernel is fixed.」というコメントは、この修正が一時的な回避策であり、将来的にはPlan 9カーネルがnote handler内でのSSE命令の使用を許可するように修正されることを期待していることを示唆しています。

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

変更は、src/pkg/runtime/os_plan9_386.csrc/pkg/runtime/os_plan9_amd64.c の両ファイルで行われています。これらのファイルは、それぞれ32ビット(x86)と64ビット(amd64)アーキテクチャ向けのPlan 9固有のランタイムコードを含んでいます。

具体的な変更箇所は、runtime·sighandler関数(Plan 9のnote handlerに相当)内でエラー文字列をコピーしている部分です。

--- a/src/pkg/runtime/os_plan9_386.c
+++ b/src/pkg/runtime/os_plan9_386.c
@@ -59,12 +59,18 @@ runtime·sighandler(void *v, int8 *s, G *gp)
  		if(gp == nil || m->notesig == 0)
  			goto Throw;
 
-		// Save error string from sigtramp's stack,
-		// into gsignal->sigcode0, so we can reliably
-		// access it from the panic routines.
-		if(len > ERRMAX)
-			len = ERRMAX;
-		runtime·memmove((void*)m->notesig, (void*)s, len);
+		// Copy the error string from sigtramp's stack into m->notesig so
+		// we can reliably access it from the panic routines. We can't use
+		// runtime·memmove here since it will use SSE instructions for big
+		// copies. The Plan 9 kernel doesn't allow floating point in note
+		// handlers.
+		//
+		// TODO(ality): revert back to memmove when the kernel is fixed.
+		if(len >= ERRMAX)
+			len = ERRMAX-1;
+		for(i = 0; i < len; i++)
+			m->notesig[i] = s[i];
+		m->notesig[i] = '\0';
 
  		gp->sig = i;
  		gp->sigpc = ureg->pc;

src/pkg/runtime/os_plan9_amd64.c も同様の変更が適用されています。

コアとなるコードの解説

変更されたコードは、runtime·sighandler関数内で、シグナル(note)によって渡されたエラー文字列(s)を、ランタイム内部の構造体(m->notesig)にコピーする部分です。

変更前:

		if(len > ERRMAX)
			len = ERRMAX;
		runtime·memmove((void*)m->notesig, (void*)s, len);

変更前は、runtime·memmove関数を使用して、sからm->notesiglenバイトのデータをコピーしていました。このruntime·memmoveがSSE命令を使用するようになったことが問題の原因でした。

変更後:

		// Copy the error string from sigtramp's stack into m->notesig so
		// we can reliably access it from the panic routines. We can't use
		// runtime·memmove here since it will use SSE instructions for big
		// copies. The Plan 9 kernel doesn't allow floating point in note
		// handlers.
		//
		// TODO(ality): revert back to memmove when the kernel is fixed.
		if(len >= ERRMAX)
			len = ERRMAX-1;
		for(i = 0; i < len; i++)
			m->notesig[i] = s[i];
		m->notesig[i] = '\0';

変更後では、runtime·memmoveの呼び出しが削除され、代わりに手動のバイトコピーループが導入されています。

  1. コメントの追加: 変更の理由が明確に説明されています。「runtime·memmoveが大きなコピーでSSE命令を使用するため、Plan 9カーネルのnote handlerでは浮動小数点演算が許可されていないため、これを使用できない」という点が強調されています。また、Plan 9カーネルが修正されたらmemmoveに戻すというTODOコメントも追加されています。
  2. 長さの調整: if(len >= ERRMAX) len = ERRMAX-1; という行が追加されています。これは、コピーする文字列の長さがERRMAX(エラーメッセージの最大長)以上の場合に、ERRMAX-1に制限し、終端のヌル文字(\0)のためのスペースを確保するためです。これにより、バッファオーバーフローを防ぎ、文字列が常にヌル終端されることを保証します。
  3. バイトコピーループ: for(i = 0; i < len; i++) m->notesig[i] = s[i]; というループが追加されました。これは、sからm->notesigへ、1バイトずつデータをコピーするシンプルなループです。この方法ではSSE命令やXMMレジスタは使用されないため、Plan 9カーネルの浮動小数点演算の制限に抵触することはありません。
  4. ヌル終端: m->notesig[i] = '\0'; という行が追加され、コピーされた文字列が確実にヌル終端されるようにしています。これはC言語の文字列処理において非常に重要です。

この変更により、Plan 9のシグナルハンドラ内でのメモリコピーが、カーネルの制約に違反することなく安全に実行されるようになりました。

関連リンク

  • Go CL 34640045: https://golang.org/cl/34640045
  • 回帰を導入したコミット(リビジョン 4cb93e2900d0): このコミットメッセージには直接リンクがありませんが、GoのGitリポジトリでハッシュを検索することで見つけることができます。

参考にした情報源リンク