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

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

コミット

このコミットは、GoランタイムにおけるARMアーキテクチャでのビルド問題を修正するものです。具体的には、システムコール中に発生する「stack split during syscall」という致命的なエラーに対処しています。このエラーは、_addv 関数内でスタック分割が適切に行われなかったことに起因しているようです。

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

https://github.com/golang/go/commit/64db2ec915f7d3a7ea515ac3050afd4063cd7438

元コミット内容

commit 64db2ec915f7d3a7ea515ac3050afd4063cd7438
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Jul 29 23:41:12 2013 +0400

    runtime: fix arm build
    
    The current failure is:
    fatal error: runtime: stack split during syscall
    goroutine 2 [stack split]:
    _addv(0xb6fa0f28, 0xd0a5112e, 0x13156d6e, 0xf8475800, 0xd)
            /usr/local/go/src/pkg/runtime/vlrt_arm.c:66 fp=0xb6fa0ef8
    notetsleep(0xb6fa0f9c, 0xf8475800, 0xd, 0x0, 0x0, ...)\n            /usr/local/go/src/pkg/runtime/lock_futex.c:156 +0xd0 fp=0xb6fa0f18
    runtime.notetsleepg(0xb6fa0f9c, 0xf8475800, 0xd)
            /usr/local/go/src/pkg/runtime/lock_futex.c:197 +0x74 fp=0xb6fa0f3c
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/12052043

変更の背景

このコミットは、GoランタイムのARMビルドにおける特定のバグを修正するために行われました。報告されたエラーメッセージ「fatal error: runtime: stack split during syscall」は、Goのランタイムがシステムコール中にスタックを適切に拡張または切り替えることができなかったことを示しています。これは通常、スタックの制約、アセンブリコードの誤ったスタックポインタ操作、または特定の関数呼び出し規約の不一致によって発生します。

スタックトレースから、問題が_addv関数(vlrt_arm.cの66行目)で発生していることがわかります。この関数は、Goランタイムが内部的に使用する多倍長整数演算の一部である可能性が高いです。notetsleepruntime.notetsleepgといった関数もスタックトレースに現れており、これはランタイムのスケジューラや同期プリミティブに関連する部分で問題が顕在化したことを示唆しています。

ARMアーキテクチャは、レジスタの使用方法や関数呼び出し規約が他のアーキテクチャ(x86-64など)と異なるため、低レベルのアセンブリコードやCコードで記述されたランタイム部分では、アーキテクチャ固有の調整が必要となることがよくあります。この問題も、ARM固有のスタック管理や関数呼び出し規約の誤解釈、または最適化の副作用によって引き起こされたと考えられます。

前提知識の解説

  • Goランタイム: Goプログラムの実行を管理する低レベルのシステムです。ガベージコレクション、ゴルーチン管理、スケジューリング、システムコールインターフェースなどを担当します。Goの「並行性」と「シンプルさ」の多くは、このランタイムによって実現されています。
  • スタック分割 (Stack Splitting): Goのゴルーチンは、非常に小さなスタック(通常は数KB)で開始されます。関数呼び出しによってスタックが不足しそうになると、Goランタイムは自動的にスタックを拡張(より大きなスタックにコピー)します。これをスタック分割と呼びます。これにより、Goは数百万のゴルーチンを効率的に実行できます。
  • システムコール (Syscall): プログラムがオペレーティングシステム(OS)のサービス(ファイルI/O、ネットワーク通信、メモリ管理など)を要求するためのメカニズムです。システムコール中は、ユーザーモードからカーネルモードへの切り替えが発生し、スタックの扱いが通常とは異なる場合があります。
  • ARMアーキテクチャ: モバイルデバイスや組み込みシステムで広く使用されているRISC(Reduced Instruction Set Computer)ベースのCPUアーキテクチャです。x86とは異なる命令セット、レジスタセット、関数呼び出し規約を持ちます。
  • TEXTディレクティブ (Goアセンブリ): Goのアセンブリ言語(Plan 9アセンブラ構文)で使用されるディレクティブで、関数の定義を開始します。TEXT symbol(SB), flags, $framesize の形式を取り、flagsは関数の特性(例: スタック分割を無効にするなど)を定義し、$framesizeは関数のスタックフレームサイズを指定します。
  • #pragma textflag (Go Cコード): GoのCコード(gcコンパイラでコンパイルされるCコード)で使用されるディレクティブで、続く関数のGoアセンブリリンケージフラグを設定します。これは、GoアセンブリのTEXTディレクティブのflags引数に相当します。

技術的詳細

このコミットの技術的な核心は、GoランタイムがARMアーキテクチャ上でシステムコール中にスタックを適切に管理できなかった問題の解決にあります。

スタックトレースに示されている「fatal error: runtime: stack split during syscall」は、Goのスタック分割メカニズムがシステムコール中に失敗したことを明確に示しています。通常、システムコール中はスタックの切り替えや拡張が制限されるか、特別な注意が必要です。Goランタイムは、システムコールに入る際にスタックを固定したり、特別なスタックに切り替えたりすることがあります。このプロセスがARM固有のコードで正しく処理されていなかったと考えられます。

変更点を見ると、src/pkg/runtime/vlop_arm.ssrc/pkg/runtime/vlrt_arm.cという2つのファイルが修正されています。これらのファイル名は、ARMアーキテクチャにおける「vlong」(おそらく64ビット整数またはそれ以上のサイズの整数)の演算(vlopは"vlong operations"、vlrtは"vlong runtime"の略か)に関連する低レベルのコードであることを示唆しています。

具体的な変更は以下の通りです。

  1. src/pkg/runtime/vlop_arm.s の変更:

    • TEXT _mulv(SB), $0TEXT _mulv(SB), 7, $0 に変更されました。
    • ここで追加された7というフラグは、GoアセンブリのTEXTディレクティブにおけるNOSPLITフラグ(値は1)とNOFRAMEフラグ(値は2)、そしてWRAPPERフラグ(値は4)の組み合わせである可能性があります。
      • NOSPLIT (1): この関数ではスタック分割を行わないことを示します。これは、関数が非常に短く、スタックを消費しない場合や、スタック分割が安全でないコンテキスト(例: システムコール中や、スタックポインタが厳密に管理されている低レベルコード)で使用されます。
      • NOFRAME (2): この関数がスタックフレームを持たないことを示します。これは、レジスタのみを使用し、ローカル変数や引数をスタックに保存しない非常に最適化されたアセンブリ関数で使われます。
      • WRAPPER (4): この関数が他の関数をラップするラッパー関数であることを示します。これは、プロファイリングやトレースなどの目的で使われることがあります。
    • _mulv関数は、多倍長整数の乗算を行うアセンブリ関数であると推測されます。この関数にNOSPLITフラグを追加することで、システムコール中にこの関数が呼び出された際に、予期せぬスタック分割が発生するのを防ぎ、スタックの整合性を保つことを目的としていると考えられます。
  2. src/pkg/runtime/vlrt_arm.c の変更:

    • _addv関数の定義の直前に #pragma textflag 7 が追加されました。
    • これは、Cコードで記述された_addv関数に対しても、GoアセンブリのTEXTディレクティブと同じフラグ7NOSPLIT | NOFRAME | WRAPPER)を適用することを意味します。
    • _addv関数は、多倍長整数の加算を行うC関数であると推測されます。スタックトレースで問題が発生したのがこの関数であったため、この関数もシステムコール中にスタック分割を避ける必要があると判断されたのでしょう。

これらの変更は、ARMアーキテクチャにおけるGoランタイムの低レベルなスタック管理と関数呼び出し規約の厳密な要件に対応するためのものです。特に、システムコールのようなクリティカルなパスでは、スタックの安定性が非常に重要であり、NOSPLITフラグはこのような状況でのランタイムの堅牢性を高めるために使用されます。

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

src/pkg/runtime/vlop_arm.s

--- a/src/pkg/runtime/vlop_arm.s
+++ b/src/pkg/runtime/vlop_arm.s
@@ -27,7 +27,7 @@ arg=0
 
 /* replaced use of R10 by R11 because the former can be the data segment base register */
 
-TEXT _mulv(SB), $0
+TEXT _mulv(SB), 7, $0
  MOVW 0(FP), R0
  MOVW 4(FP), R2 /* l0 */
  MOVW 8(FP), R11 /* h0 */

src/pkg/runtime/vlrt_arm.c

--- a/src/pkg/runtime/vlrt_arm.c
+++ b/src/pkg/runtime/vlrt_arm.c
@@ -62,6 +62,7 @@ struct  Vlong
 
 void    runtime·abort(void);
 
+#pragma textflag 7
 void
 _addv(Vlong *r, Vlong a, Vlong b)
 {

コアとなるコードの解説

src/pkg/runtime/vlop_arm.s の変更

TEXT _mulv(SB), 7, $0 の変更は、Goアセンブリで定義された_mulv関数に特定のフラグを設定しています。

  • _mulv: 多倍長整数の乗算を行うアセンブリ関数。

  • (SB): シンボルが静的セグメント(Static Base)に属することを示します。

  • 7: これはフラグのビットマスクです。Goのランタイムアセンブリにおけるフラグは以下の通りです(当時のGoのバージョンに基づく推測):

    • 1 (0x1): NOSPLIT - この関数はスタック分割を行いません。つまり、この関数が実行中にスタックが不足しても、ランタイムはスタックを拡張しようとしません。これは、スタックポインタが厳密に管理されている低レベルのコードや、システムコールのようなスタック切り替えが絡むコンテキストで重要です。
    • 2 (0x2): NOFRAME - この関数はスタックフレームを持ちません。これは、レジスタのみを使用し、ローカル変数や引数をスタックに保存しない、非常に最適化されたアセンブリ関数で使われます。
    • 4 (0x4): WRAPPER - この関数が他の関数をラップするラッパー関数であることを示します。プロファイリングやトレースなどの目的で使われることがあります。

    したがって、7NOSPLIT | NOFRAME | WRAPPERの組み合わせを意味します。この変更により、_mulv関数がシステムコール中に呼び出された際に、スタック分割による問題が発生するのを防ぎ、スタックの整合性を保証します。

  • $0: この関数のスタックフレームサイズが0バイトであることを示します。これはNOFRAMEフラグと一致し、関数がスタックを消費しないことを意味します。

src/pkg/runtime/vlrt_arm.c の変更

#pragma textflag 7 の追加は、C言語で記述された_addv関数に、GoアセンブリのTEXTディレクティブと同じフラグ7を適用するためのものです。

  • #pragma textflag: GoのCコンパイラ(gc)がGoアセンブリリンケージフラグを設定するために使用する非標準のプラグマです。
  • 7: vlop_arm.s_mulv関数と同様に、NOSPLIT | NOFRAME | WRAPPERの組み合わせを意味します。

_addv関数は、スタックトレースで問題の発生源として特定された関数です。この関数にNOSPLITフラグを適用することで、システムコール中にこの関数が呼び出された際に、スタック分割が試みられてエラーになることを防ぎます。これは、_addvがシステムコールパスの一部として、またはシステムコール中に呼び出される可能性のあるコードパスから呼び出される場合に特に重要です。

これらの変更は、ARMアーキテクチャにおけるGoランタイムのスタック管理の厳密な要件を満たすためのものであり、特にシステムコールのようなクリティカルなコンテキストでの安定性を向上させます。

関連リンク

参考にした情報源リンク

  • Goのスタック分割に関するドキュメントや議論 (一般的な情報源):
    • Goのスタック管理に関するブログ記事や公式ドキュメント
    • Goのソースコード内のruntimeパッケージのコメントやドキュメント
  • GoアセンブリのTEXTディレクティブに関する情報:
    • Goの公式ドキュメント: https://go.dev/doc/asm
    • Goのソースコード内のアセンブリファイルのコメント
  • ARMアーキテクチャの関数呼び出し規約に関する情報 (一般的な情報源):
    • ARM Architecture Reference Manual
    • ARMのABI (Application Binary Interface) ドキュメント
  • #pragma textflagに関する情報:
    • Goのソースコード内のCファイルやコンパイラのドキュメント
    • Goのコンパイラに関する技術的な議論やブログ記事
  • Goのfatal error: runtime: stack split during syscallに関する情報:
    • Goのissue trackerやメーリングリストでの過去の議論
    • Goのランタイムに関する技術ブログや記事
    • Stack OverflowなどのQ&Aサイトでの関連質問と回答

これらの情報源は、Goランタイムの内部動作、特にスタック管理、アセンブリ言語の規約、およびアーキテクチャ固有の考慮事項を理解する上で役立ちます。