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

[インデックス 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型のsilong型のtに代入する際の符号拡張の挙動が、期待通りではなかったために発生していました。特に、負の値を扱う際に問題が生じていたと考えられます。

前提知識の解説

  • Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルな部分です。ガベージコレクション、スケジューリング、スタック管理、システムコールなどが含まれます。
  • ARMアーキテクチャ: スマートフォンや組み込みシステムで広く使われているCPUアーキテクチャです。x86とは異なる命令セットやレジスタ構成を持っています。
  • スタック (Stack): プログラムが関数呼び出しやローカル変数を格納するために使用するメモリ領域です。関数が呼び出されるたびにスタックフレームが積まれ、関数から戻ると解放されます。
  • スタック分割 (Stack Splitting): Goランタイムの重要な機能の一つで、関数が実行中にスタック領域が不足しそうになった場合、自動的に新しいより大きなスタック領域を割り当て、既存のスタックの内容を新しい領域にコピーする仕組みです。これにより、固定サイズのスタックを持つ他の言語で発生するスタックオーバーフローを防ぎ、効率的なメモリ利用を可能にします。
  • nosplit 関数: スタック分割が許可されない関数です。通常、Goランタイムの非常に低レベルな部分や、システムコールを直接呼び出す関数など、スタック分割が安全に行えない、あるいはオーバーヘッドが大きい場合に指定されます。#pragma textflag 7は、Goコンパイラにこの関数をnosplitとして扱うように指示します。
  • 符号拡張 (Sign Extension): 整数型をより広いビット幅の型に変換する際に、元の数値の符号(正負)を保持するために、新しい型の残りの上位ビットを元の数値の最上位ビット(符号ビット)で埋める操作です。例えば、8ビットの符号付き整数-111111111)を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;のように、明示的にsilongにキャストしています。この明示的なキャストにより、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点です。

  1. #pragma textflag 7 の追加: このプリプロセッサディレクティブは、Goコンパイラに対して、続く_si2v関数をnosplit関数として扱うように指示します。nosplit関数は、実行中にスタックを拡張するためのスタック分割を行いません。これは、_si2vのような低レベルなランタイム関数が、スタックの状態を厳密に制御する必要がある場合や、スタック分割のオーバーヘッドが許容されない場合に重要です。コミットメッセージにある「notetsleep: nosplit stack overflow」というエラーは、このフラグが正しく適用されていないか、あるいはフラグを適用しただけでは根本的な問題が解決しなかったことを示唆しています。この修正では、nosplitの指定と同時に、関数内部のロジックも修正することで、両方の問題を解決しています。

  2. 符号拡張の修正:

    • 変更前:

      long t;
      t = si;
      ret->lo = t;
      ret->hi = t >> 31;
      

      ここでは、int silong tに代入し、そのtを使ってret->loret->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のソースコードやコンパイラのドキュメントに詳細があります。

参考にした情報源リンク