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

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

このコミットは、Goランタイムにおける特定の関数(_lshv)に対して、コンパイラディレクティブである#pragma textflag 7を適用することで、関数がスタックを分割しないようにマークする変更です。これは主にARMアーキテクチャでのビルド問題を解決するために行われましたが、一貫性のために386アーキテクチャの対応するコードにも適用されました。

コミット

commit f4f2cf16b0d7dd2f1841122c15e9e0388470fca7
Author: Russ Cox <rsc@golang.org>
Date:   Thu Aug 1 00:23:30 2013 -0400

    runtime: mark arm _lsvh nosplit (may fix arm build)
    
    Mark the 386 one too for consistency,
    although most of that code is no longer used.
    
    TBR=dvyukov
    CC=golang-dev
    https://golang.org/cl/12227043

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

https://github.com/golang/go/commit/f4f2cf16b0d7dd2f1841122c15e9e0388470fca7

元コミット内容

runtime: mark arm _lsvh nosplit (may fix arm build)

Mark the 386 one too for consistency,
although most of that code is no longer used.

TBR=dvyukov
CC=golang-dev
https://golang.org/cl/12227043

変更の背景

このコミットの主な背景は、ARMアーキテクチャにおけるGoのビルドプロセスで発生していた潜在的な問題に対処することです。Goランタイムの特定の低レベル関数、特にスタック管理やアセンブリコードと密接に関連する関数は、通常のGo関数とは異なる特別な扱いを必要とすることがあります。

Goの関数は通常、必要に応じてスタックを動的に拡張する「スタック分割(stack splitting)」というメカニズムを使用します。これにより、小さなスタックで開始し、必要に応じて成長させることでメモリ効率を高めます。しかし、ランタイムの非常に低レベルな部分、特にアセンブリで書かれた関数や、スタックの成長を安全に処理できないようなクリティカルなパスにある関数では、このスタック分割が問題を引き起こす可能性があります。スタック分割の処理自体がスタックを必要とし、デッドロックや無限ループ、あるいは予期せぬクラッシュにつながる可能性があるためです。

ARMアーキテクチャでは、特定のランタイム関数がスタック分割のルールに違反したり、予期せぬ動作を引き起こしたりする可能性があったため、ビルドが不安定になることが考えられました。このコミットは、_lshvのような関数がスタック分割を行わないように明示的にマークすることで、これらの問題を回避し、ARMでのGoの安定性を向上させることを目的としています。386アーキテクチャのコードにも同様の変更が適用されたのは、コードベース全体の一貫性を保つためであり、将来的な互換性やメンテナンス性を考慮したものです。

前提知識の解説

このコミットを理解するためには、以下の概念が重要です。

  1. Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルな部分です。ガベージコレクション、スケジューリング、スタック管理、システムコールなど、Go言語の並行性モデルとメモリ管理を支える基盤を提供します。ランタイムの多くはGoで書かれていますが、パフォーマンスやOSとのインタラクションのためにCやアセンブリで書かれた部分も存在します。

  2. スタック分割 (Stack Splitting): Goの関数呼び出しにおいて、関数が実行に必要なスタック領域が不足した場合に、自動的にスタックを拡張するメカニズムです。これにより、各goroutineが最初から大きなスタックを確保する必要がなくなり、メモリ使用量を効率化できます。スタックの拡張は、関数プロローグでスタックガードページをチェックし、必要であればランタイムのスタック拡張関数を呼び出すことで行われます。

  3. nosplit (No Split): Goコンパイラに対するディレクティブの一つで、特定の関数がスタック分割を行わないように指示します。nosplit関数は、呼び出し時にスタックガードチェックを行わず、スタックを拡張しようとしません。これは、スタック分割ロジック自体がスタックを必要とするため、スタック分割が不可能な低レベルのランタイム関数や、非常に短いクリティカルなパスの関数に適用されます。例えば、スタックを拡張する関数自体がスタック分割を試みると、無限再帰やデッドロックに陥る可能性があります。

  4. #pragma textflag 7: これはGoコンパイラ(具体的にはcmd/compile)が認識する特殊な#pragmaディレクティブです。textflagは、生成されるアセンブリコードのセクションフラグを制御するために使用されます。textflag 7は、Goのソースコードでは//go:nosplitディレクティブに相当し、コンパイラに対してその関数がスタック分割を行わないことを指示します。これは、C言語で書かれたランタイムファイル(.cファイル)内でGoのコンパイラディレクティブを適用するための方法です。

  5. vlrt_386.c および vlrt_arm.c: これらのファイルは、Goランタイムの一部であり、それぞれ386(x86 32-bit)およびARMアーキテクチャ向けの低レベルな算術演算やユーティリティ関数を含んでいます。ファイル名のvlrtは "V-long runtime" の略である可能性があり、Vlongのような64ビット整数型(Goのint64に相当)に対する操作(シフト演算など)をC言語で実装していることが多いです。これらの関数は、Goの組み込み型が特定のアーキテクチャで直接サポートされていない場合や、アセンブリレベルでの最適化が必要な場合に利用されます。

  6. _lshv 関数: このコミットで変更された関数名です。_lshvは "left shift Vlong" の略であると推測され、おそらく64ビット整数(Vlong)の左シフト演算を実装している関数です。このような低レベルのビット演算関数は、ランタイムの他の部分で頻繁に使用されるため、その安定性と効率性は非常に重要です。

技術的詳細

このコミットの技術的な核心は、GoコンパイラがC言語のソースファイル内で認識する#pragma textflag 7ディレクティブの使用です。

Goのコンパイラは、Go言語で書かれたコードだけでなく、ランタイムの一部としてC言語で書かれたファイルもコンパイルします。これらのCファイルは、Goのランタイムと密接に連携するため、Goのコンパイラが提供する特別なディレクティブ(//go:で始まるもの)をCファイル内で直接使用することはできません。そこで、Cプリプロセッサの#pragma構文を利用して、Goコンパイラに特定の指示を与える方法が採用されています。

#pragma textflag 7は、Goコンパイラに対して、その後に続く関数がnosplit属性を持つことを伝えます。nosplit属性が設定された関数は、以下の特性を持ちます。

  • スタックガードチェックのスキップ: 関数プロローグで通常行われるスタックガードページのチェックをスキップします。これにより、スタックが不足している場合にスタック拡張ルーチンを呼び出すことを防ぎます。
  • スタック拡張の禁止: この関数内でスタックを拡張しようとする試みは行われません。
  • 呼び出し規約の変更: nosplit関数は、呼び出し元が十分なスタック空間を確保していることを前提とします。また、nosplit関数から呼び出される他の関数もnosplitである必要があります。

_lshvのようなビットシフト関数は、非常に短く、かつランタイムの他の部分から頻繁に呼び出される可能性があります。このような関数がスタック分割を試みると、スタック分割のオーバーヘッドがパフォーマンスに影響を与えたり、スタック分割ロジック自体がスタックを必要とするためにデッドロックや無限再帰を引き起こすリスクがあります。特に、スタック拡張ルーチン自体が_lshvのような低レベル関数を使用している場合、循環依存が発生し、システムがクラッシュする可能性があります。

ARMアーキテクチャでは、特定のコンテキストでスタックの制約が厳しかったり、アセンブリコードとの連携が複雑だったりするために、このnosplitの指定が特に重要になったと考えられます。386アーキテクチャのコードにも同様の変更が適用されたのは、コードの対称性を保ち、将来的なリファクタリングやクロスアーキテクチャ開発を容易にするためです。たとえ386のコードが現在あまり使用されていなくても、一貫したアプローチを取ることで、潜在的なバグの導入を防ぎ、コードベースの健全性を維持できます。

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

このコミットによる変更は、以下の2つのファイルにそれぞれ1行ずつ追加されています。

  1. src/pkg/runtime/vlrt_386.c
  2. src/pkg/runtime/vlrt_arm.c

それぞれのファイルで、_lshv関数の定義の直前に#pragma textflag 7が追加されています。

diff --git a/src/pkg/runtime/vlrt_386.c b/src/pkg/runtime/vlrt_386.c
index 1631dbe108..78e3f02a17 100644
--- a/src/pkg/runtime/vlrt_386.c
+++ b/src/pkg/runtime/vlrt_386.c
@@ -423,6 +423,7 @@ _rshlv(Vlong *r, Vlong a, int b)
 	r->lo = (t << (32-b)) | (a.lo >> b);
 }
 
+#pragma textflag 7
 void
 _lshv(Vlong *r, Vlong a, int b)
 {
diff --git a/src/pkg/runtime/vlrt_arm.c b/src/pkg/runtime/vlrt_arm.c
index 11813f91c4..b58c5fbc0e 100644
--- a/src/pkg/runtime/vlrt_arm.c
+++ b/src/pkg/runtime/vlrt_arm.c
@@ -421,6 +421,7 @@ _rshlv(Vlong *r, Vlong a, int b)
 	r->lo = (t << (32-b)) | (a.lo >> b);
 }
 
+#pragma textflag 7
 void
 _lshv(Vlong *r, Vlong a, int b)
 {

コアとなるコードの解説

追加された#pragma textflag 7は、Goコンパイラに対する指示であり、その直後に続く_lshv関数がスタック分割を行わない(nosplit)関数として扱われるべきであることを意味します。

  • src/pkg/runtime/vlrt_386.c: このファイルは、32ビットx86アーキテクチャ(386)向けのGoランタイムの低レベルなCコードを含んでいます。_lshv関数は、おそらく64ビット整数(Vlong)の左シフト演算を実装しています。この変更により、386環境で_lshvが呼び出される際にスタック分割が試みられなくなり、ランタイムの安定性が向上します。コミットメッセージにあるように、この386のコードは「ほとんど使われていない」とされていますが、一貫性のために変更が適用されました。

  • src/pkg/runtime/vlrt_arm.c: このファイルは、ARMアーキテクチャ向けのGoランタイムの低レベルなCコードを含んでいます。同様に_lshv関数に対して#pragma textflag 7が適用されています。これがこのコミットの主要な目的であり、ARMアーキテクチャでのGoのビルドが不安定になる可能性のある問題を解決するために導入されました。ARMのような組み込みシステムやモバイル環境では、スタックの管理が特に重要になることがあります。

この変更により、_lshv関数は、呼び出し時にスタックの拡張を試みることなく、現在のスタックフレーム内で完結して実行されることが保証されます。これは、ランタイムの非常にデリケートな部分において、予期せぬスタック関連の問題(スタックオーバーフロー、デッドロック、クラッシュなど)を防ぐために不可欠な措置です。

関連リンク

参考にした情報源リンク