[インデックス 16910] ファイルの概要
このコミットは、GoランタイムにおけるARMアーキテクチャでのビルド問題を修正するものです。具体的には、_si2v
関数における符号拡張の挙動が原因で発生していたスタック関連のエラーに対処しています。
コミット
commit 14e3540430adf614047328043e70a3184ce287da
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Jul 30 00:08:30 2013 +0400
runtime: fix arm build
The current failure is:
fatal error: runtime: stack split during syscall
goroutine 2 [stack split]:
_si2v(0xb6ebaebc, 0x3b9aca00)
/usr/local/go/src/pkg/runtime/vlrt_arm.c:628 fp=0xb6ebae9c
runtime.timediv(0xf8475800, 0xd, 0x3b9aca00, 0xb6ebaef4)
/usr/local/go/src/pkg/runtime/runtime.c:424 +0x1c fp=0xb6ebaed4
Just adding textflag 7 causes the following error:
notetsleep: nosplit stack overflow
128 assumed on entry to notetsleep
96 after notetsleep uses 32
60 after runtime.futexsleep uses 36
4 after runtime.timediv uses 56
-4 after _si2v uses 8
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/12001045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/14e3540430adf614047328043e70a318ce287da
元コミット内容
このコミットは、src/pkg/runtime/vlrt_arm.c
ファイルの _si2v
関数における変更です。
変更前:
void
_si2v(Vlong *ret, int si)
{
long t;
t = si;
ret->lo = t;
ret->hi = t >> 31;
}
変更後:
#pragma textflag 7
void
_si2v(Vlong *ret, int si)
{
ret->lo = (long)si;
ret->hi = (long)si >> 31;
}
変更の背景
このコミットは、GoランタイムをARMアーキテクチャでビルドする際に発生していた致命的なエラー「fatal error: runtime: stack split during syscall
」を修正するために導入されました。このエラーは、システムコール中にスタックが正しく分割されないことに起因していました。
エラーメッセージのスタックトレースを見ると、_si2v
関数が関与していることがわかります。この関数は、32ビットの符号付き整数(int si
)を64ビットのVlong
構造体(ret
)に変換する役割を担っています。
また、コミットメッセージには「Just adding textflag 7 causes the following error: notetsleep: nosplit stack overflow
」という記述があります。これは、_si2v
関数に#pragma textflag 7
を追加するだけでは、別のスタックオーバーフローエラーが発生することを示しています。textflag 7
は、Goコンパイラに対して、その関数がスタックを分割しない(nosplit
)ことを指示するフラグです。通常、Goの関数は必要に応じてスタックを拡張するためにスタック分割を行いますが、一部の低レベルなランタイム関数では、スタック分割が許可されない場合があります。
この問題は、_si2v
関数内でint
型のsi
をlong
型のt
に代入する際の符号拡張の挙動が、期待通りではなかったために発生していました。特に、負の値を扱う際に問題が生じていたと考えられます。
前提知識の解説
- Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルな部分です。ガベージコレクション、スケジューリング、スタック管理、システムコールなどが含まれます。
- ARMアーキテクチャ: スマートフォンや組み込みシステムで広く使われているCPUアーキテクチャです。x86とは異なる命令セットやレジスタ構成を持っています。
- スタック (Stack): プログラムが関数呼び出しやローカル変数を格納するために使用するメモリ領域です。関数が呼び出されるたびにスタックフレームが積まれ、関数から戻ると解放されます。
- スタック分割 (Stack Splitting): Goランタイムの重要な機能の一つで、関数が実行中にスタック領域が不足しそうになった場合、自動的に新しいより大きなスタック領域を割り当て、既存のスタックの内容を新しい領域にコピーする仕組みです。これにより、固定サイズのスタックを持つ他の言語で発生するスタックオーバーフローを防ぎ、効率的なメモリ利用を可能にします。
nosplit
関数: スタック分割が許可されない関数です。通常、Goランタイムの非常に低レベルな部分や、システムコールを直接呼び出す関数など、スタック分割が安全に行えない、あるいはオーバーヘッドが大きい場合に指定されます。#pragma textflag 7
は、Goコンパイラにこの関数をnosplit
として扱うように指示します。- 符号拡張 (Sign Extension): 整数型をより広いビット幅の型に変換する際に、元の数値の符号(正負)を保持するために、新しい型の残りの上位ビットを元の数値の最上位ビット(符号ビット)で埋める操作です。例えば、8ビットの符号付き整数
-1
(11111111
)を16ビットに拡張すると、1111111111111111
となります。もし符号拡張が行われないと、0000000011111111
となり、値が変わってしまいます。 Vlong
構造体: Goランタイム内部で64ビット整数を表現するために使用される可能性のある構造体です。通常、lo
(下位32ビット)とhi
(上位32ビット)の2つの32ビットフィールドで構成されます。
技術的詳細
このコミットの核心は、_si2v
関数における符号拡張の修正です。
元のコードでは、int si
(32ビット符号付き整数)をlong t
(通常、ARMでは32ビットまたは64ビットですが、この文脈では32ビットと仮定されます)に代入し、その後t
の値をret->lo
に、t >> 31
の結果をret->hi
に格納していました。
問題は、C言語の標準において、int
からlong
への代入時の符号拡張の挙動が、コンパイラやアーキテクチャによって微妙に異なる場合があることです。特に、負のint
値をlong
に代入し、そのlong
値をビットシフトして上位ビットを取得する際に、期待通りの符号拡張が行われない可能性がありました。
例えば、si
が負の値の場合、t = si;
の代入時にt
が正しく符号拡張されないと、t >> 31
の結果が期待と異なる可能性があります。>>
演算子は、符号付き整数に対しては算術右シフト(符号ビットを保持)を行うのが一般的ですが、それでも元の代入が正しくない場合、結果は誤りとなります。
修正後のコードでは、ret->lo = (long)si;
とret->hi = (long)si >> 31;
のように、明示的にsi
をlong
にキャストしています。この明示的なキャストにより、si
の値がlong
型に変換される際に、C言語の標準に従って正しい符号拡張が保証されます。その後、この正しく符号拡張されたlong
値に対してビットシフトを行うことで、ret->hi
に適切な上位ビット(符号ビット)が設定されるようになります。
この修正により、_si2v
関数が負の整数を正しく64ビットのVlong
に変換できるようになり、その結果、スタック分割やシステムコールに関連するランタイムエラーが解消されたと考えられます。
また、#pragma textflag 7
が追加されているのは、_si2v
関数がGoランタイムの非常に低レベルな部分であり、スタック分割を許可しない(nosplit
)必要があるためです。この関数は、スタックの管理やシステムコールに関連する処理の中で呼び出される可能性があり、その際にスタック分割が発生すると、競合状態やデッドロックなどの問題を引き起こす可能性があるためです。
コアとなるコードの変更箇所
src/pkg/runtime/vlrt_arm.c
ファイルの _si2v
関数。
--- a/src/pkg/runtime/vlrt_arm.c
+++ b/src/pkg/runtime/vlrt_arm.c
@@ -624,14 +624,12 @@ _ul2v(Vlong *ret, ulong ul)
ret->hi = 0;
}
+#pragma textflag 7
void
_si2v(Vlong *ret, int si)
{
- long t;
-
- t = si;
- ret->lo = t;
- ret->hi = t >> 31;
+ ret->lo = (long)si;
+ ret->hi = (long)si >> 31;
}
void
コアとなるコードの解説
変更のポイントは以下の2点です。
-
#pragma textflag 7
の追加: このプリプロセッサディレクティブは、Goコンパイラに対して、続く_si2v
関数をnosplit
関数として扱うように指示します。nosplit
関数は、実行中にスタックを拡張するためのスタック分割を行いません。これは、_si2v
のような低レベルなランタイム関数が、スタックの状態を厳密に制御する必要がある場合や、スタック分割のオーバーヘッドが許容されない場合に重要です。コミットメッセージにある「notetsleep: nosplit stack overflow
」というエラーは、このフラグが正しく適用されていないか、あるいはフラグを適用しただけでは根本的な問題が解決しなかったことを示唆しています。この修正では、nosplit
の指定と同時に、関数内部のロジックも修正することで、両方の問題を解決しています。 -
符号拡張の修正:
-
変更前:
long t; t = si; ret->lo = t; ret->hi = t >> 31;
ここでは、
int si
をlong t
に代入し、そのt
を使ってret->lo
とret->hi
を設定しています。問題は、int
からlong
への暗黙的な型変換(代入)において、特に負の値を扱う際に、コンパイラやアーキテクチャによっては期待通りの符号拡張が行われない可能性があったことです。もしt
が正しく符号拡張されず、上位ビットが0で埋められてしまった場合、t >> 31
の結果は負の数に対して0
となり、ret->hi
が誤った値を持つことになります。 -
変更後:
ret->lo = (long)si; ret->hi = (long)si >> 31;
この修正では、
si
を明示的に(long)
にキャストしています。C言語の標準では、より小さい整数型からより大きい整数型への変換(特に符号付き型の場合)は、符号拡張を伴うことが保証されています。したがって、(long)si
とすることで、si
が負の値であっても、long
型に変換された際に上位ビットが正しく符号ビットで埋められ、ret->lo
にはsi
の64ビット表現の下位32ビットが、ret->hi
にはsi
の64ビット表現の上位32ビット(つまり、符号ビットが拡張された結果)が正しく設定されるようになります。これにより、負の整数がVlong
構造体に正しく変換されることが保証され、スタック関連のエラーが解消されました。
-
この修正は、Goランタイムが異なるアーキテクチャ(特にARM)上で正しく動作するための、低レベルかつ重要なバグ修正です。
関連リンク
- Goのスタック管理に関する一般的な情報: https://go.dev/doc/articles/go_mem.html (Goのメモリ管理全般についてですが、スタックについても触れられています)
- Goの
textflag
に関する情報: Goのソースコードやコンパイラのドキュメントに詳細があります。
参考にした情報源リンク
- Goのコミット履歴: https://github.com/golang/go/commits/master
- C言語の型変換と符号拡張に関する一般的な知識
- ARMアーキテクチャの特性に関する一般的な知識