[インデックス 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·memmove
がGO386=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·memmove
がMOVOU
命令を使用すると、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.c
と src/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->notesig
へlen
バイトのデータをコピーしていました。この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
の呼び出しが削除され、代わりに手動のバイトコピーループが導入されています。
- コメントの追加: 変更の理由が明確に説明されています。「
runtime·memmove
が大きなコピーでSSE命令を使用するため、Plan 9カーネルのnote handlerでは浮動小数点演算が許可されていないため、これを使用できない」という点が強調されています。また、Plan 9カーネルが修正されたらmemmove
に戻すというTODO
コメントも追加されています。 - 長さの調整:
if(len >= ERRMAX) len = ERRMAX-1;
という行が追加されています。これは、コピーする文字列の長さがERRMAX
(エラーメッセージの最大長)以上の場合に、ERRMAX-1
に制限し、終端のヌル文字(\0
)のためのスペースを確保するためです。これにより、バッファオーバーフローを防ぎ、文字列が常にヌル終端されることを保証します。 - バイトコピーループ:
for(i = 0; i < len; i++) m->notesig[i] = s[i];
というループが追加されました。これは、s
からm->notesig
へ、1バイトずつデータをコピーするシンプルなループです。この方法ではSSE命令やXMMレジスタは使用されないため、Plan 9カーネルの浮動小数点演算の制限に抵触することはありません。 - ヌル終端:
m->notesig[i] = '\0';
という行が追加され、コピーされた文字列が確実にヌル終端されるようにしています。これはC言語の文字列処理において非常に重要です。
この変更により、Plan 9のシグナルハンドラ内でのメモリコピーが、カーネルの制約に違反することなく安全に実行されるようになりました。
関連リンク
- Go CL 34640045: https://golang.org/cl/34640045
- 回帰を導入したコミット(リビジョン 4cb93e2900d0): このコミットメッセージには直接リンクがありませんが、GoのGitリポジトリでハッシュを検索することで見つけることができます。
参考にした情報源リンク
- Plan 9 from Bell Labs: https://9p.io/plan9/
- Plan 9 Notes: https://9p.io/plan9/man/man2/notify.html (Plan 9の
notify
システムコールに関するマニュアルページ) - SSE (Streaming SIMD Extensions) - Wikipedia: https://ja.wikipedia.org/wiki/Streaming_SIMD_Extensions
memmove
- cppreference.com: https://ja.cppreference.com/w/c/string/byte/memmove- XMM registers - Wikipedia: https://en.wikipedia.org/wiki/XMM_register
- Intel® 64 and IA-32 Architectures Software Developer’s Manuals (SSE命令の詳細については、これらのマニュアルを参照する必要があります)