[インデックス 17924] ファイルの概要
このコミットは、GoランタイムのARMアーキテクチャ向けアセンブリコード vlop_arm.s
における、関数内へのクロスファンクションジャンプを削除するものです。これは、Go 1.3で導入される新しいリンカの制約に対応するための変更であり、リンカのクリーンアップ作業の前提条件となっています。
コミット
commit 7c17982f72704464f74766b9ef957e3d69db92d7
Author: Russ Cox <rsc@golang.org>
Date: Sun Dec 8 22:52:08 2013 -0500
runtime: remove cross-function jump in vlop_arm.s
The new linker will disallow this on arm
(it is already disallowed on amd64 and 386)
in order to be able to lay out each function
separately.
The restriction is only for jumps into the middle
of a function; jumps to the beginning of a function
remain fine.
Prereq for linker cleanup (golang.org/s/go13linker).
R=iant, r, minux.ma
CC=golang-dev
https://golang.org/cl/35800043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7c17982f72704464f74766b9ef957e3d69db92d7
元コミット内容
runtime: remove cross-function jump in vlop_arm.s
The new linker will disallow this on arm
(it is already disallowed on amd64 and 386)
in order to be able to lay out each function
separately.
The restriction is only for jumps into the middle
of a function; jumps to the beginning of a function
remain fine.
Prereq for linker cleanup (golang.org/s/go13linker).
R=iant, r, minux.ma
CC=golang-dev
https://golang.org/cl/35800043
変更の背景
この変更の主な背景は、Go 1.3で導入される新しいリンカの設計と制約にあります。コミットメッセージに「The new linker will disallow this on arm (it is already disallowed on amd64 and 386) in order to be able to lay out each function separately.」とあるように、新しいリンカは各関数を独立して配置できるように設計されており、そのために特定の種類のジャンプを禁止します。
具体的には、関数の中間地点へのクロスファンクションジャンプ(ある関数から別の関数の途中へ直接ジャンプすること)が禁止されます。これは、リンカが関数をメモリ上で自由に再配置したり、最適化したりする際に、関数内部への外部からの参照があると、その参照を追跡・調整する必要が生じ、リンカの複雑性を増すためです。関数の中間へのジャンプが許されると、リンカは関数全体を一つの連続したブロックとして扱うことが難しくなり、個々の関数の独立した配置や最適化が妨げられます。
この制約は、すでにamd64および386アーキテクチャでは適用されていましたが、ARMアーキテクチャにも拡張されることになりました。コミットメッセージの「Prereq for linker cleanup (golang.org/s/go13linker)」という記述は、この変更がGo 1.3のリンカのクリーンアップ(再設計と改善)作業の前提条件であることを示しています。Go 1.3のリンカは、よりシンプルで堅牢な設計を目指しており、このような特定のジャンプの禁止はその目標達成に不可欠な要素でした。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
-
Goランタイム (Go Runtime): Goプログラムは、Goランタイムと呼ばれる小さな実行時システムに依存しています。これは、ガベージコレクション、スケジューリング、チャネル操作、メモリ管理など、Go言語のコア機能の多くを実装しています。ランタイムの一部は、パフォーマンスが重要な部分や、OSとの低レベルなインタラクションが必要な部分で、アセンブリ言語で書かれています。
vlop_arm.s
は、ARMアーキテクチャ向けのGoランタイムの一部であり、主にベクタ演算や低レベルな算術演算に関連する処理をアセンブリで実装しているファイルと考えられます。 -
アセンブリ言語 (Assembly Language): アセンブリ言語は、コンピュータのプロセッサが直接実行できる機械語に非常に近い低レベルのプログラミング言語です。各アセンブリ命令は、通常、特定のCPU命令に一対一で対応します。このコミットではARMアセンブリが使用されており、
BL
(Branch with Link),B
(Branch),MOVW
(Move Word),RET
(Return) などの命令が登場します。BL
(Branch with Link): サブルーチンコールに使用されます。指定されたアドレスにジャンプし、現在のプログラムカウンタ(次の命令のアドレス)をリンクレジスタ(通常はLR
またはR14
)に保存します。これにより、サブルーチンから戻る際に元の実行フローに戻ることができます。B
(Branch): 無条件ジャンプ命令です。指定されたアドレスにプログラムの実行フローを移します。BL
と異なり、戻りアドレスは保存されません。MOVW
(Move Word): レジスタ間またはレジスタと即値間でワード(32ビット)データを移動する命令です。RET
(Return): サブルーチンから呼び出し元に戻る命令です。通常、リンクレジスタに保存されたアドレスにジャンプします。
-
リンカ (Linker): リンカは、コンパイラによって生成された複数のオブジェクトファイル(コンパイルされたソースコード)と、必要なライブラリを結合して、実行可能なプログラムを生成するツールです。リンカの主な役割は以下の通りです。
- シンボル解決: オブジェクトファイル間で参照される関数や変数のアドレスを解決します。
- 再配置 (Relocation): コード内のアドレス参照を、最終的なメモリ配置に合わせて調整します。
- メモリレイアウト: 実行可能ファイル内のコード、データ、スタックなどのセクションをメモリ上にどのように配置するかを決定します。
-
クロスファンクションジャンプ (Cross-function Jump): ある関数(
funcA
)のコードから、別の関数(funcB
)のコードへ直接ジャンプするプログラミング手法です。通常、関数呼び出しはCALL
やBL
のような命令で行われ、呼び出された関数が終了すると呼び出し元に戻ります。しかし、クロスファンクションジャンプは、戻りアドレスを保存せずに直接別の場所へ実行フローを移すものです。 このコミットで問題となっているのは、特に「関数の中間地点へのジャンプ」です。関数の先頭へのジャンプは、その関数を通常のサブルーチンとして呼び出すことと等価であり、リンカが関数を独立して扱う上で問題になりにくいですが、関数の中間へのジャンプは、リンカが関数の構造を解析し、最適化を行う上で複雑性を増します。 -
Go 1.3 リンカの変更 (golang.org/s/go13linker): Go 1.3では、リンカの大規模な再設計が行われました。この再設計の目的は、リンカのコードベースを簡素化し、保守性を高め、将来的な最適化(例えば、より効率的なデッドコード削除や、より高度なコードレイアウト最適化)を可能にすることでした。この再設計の一環として、リンカは各関数をより独立した単位として扱うようになり、そのために特定の低レベルなジャンプパターンが禁止されることになりました。
技術的詳細
このコミットが対処している技術的な問題は、GoランタイムのARMアセンブリコード vlop_arm.s
内に存在していた「クロスファンクションジャンプ」です。具体的には、_div
関数内の B out
命令が、_mod
関数内の out
ラベル(実際には _mod
関数の内部にあるが、_div
関数から直接ジャンプしているように見える)にジャンプしていました。
新しいGoリンカ(Go 1.3で導入)は、実行可能ファイルの生成時に、各関数をメモリ上で独立したブロックとして配置する能力を向上させることを目指しています。この「各関数を独立して配置する」という設計目標を達成するためには、ある関数から別の関数の「途中」へ直接ジャンプするようなコードパターンが問題となります。なぜなら、リンカが関数を再配置する際に、その関数の中間地点への外部からのジャンプがあると、リンカはそのジャンプ先のアドレスを正確に追跡し、調整する必要が生じるためです。これはリンカの複雑性を増大させ、最適化の機会を制限します。
コミットメッセージにあるように、この制約はすでにamd64および386アーキテクチャでは適用されていましたが、ARMアーキテクチャにも拡張されることになりました。ただし、この制約は「関数の中間へのジャンプ」に限定され、関数の「先頭」へのジャンプは引き続き許可されます。関数の先頭へのジャンプは、実質的に通常の関数呼び出し(ただし、戻りアドレスをスタックにプッシュしない場合もある)と見なすことができ、リンカが関数を独立して扱う上で大きな問題にはなりません。
このコミットでは、_div
関数が _mod
関数内の out
ラベルにジャンプする代わりに、_div
関数自身の内部に out1
という新しいラベルと、それに続く必要な処理(レジスタの値をスタックから復元し、RET
で戻る)を定義することで、このクロスファンクションジャンプの問題を解決しています。これにより、_div
関数は独立した実行フローを持つようになり、新しいリンカの制約に適合します。
コアとなるコードの変更箇所
変更は src/pkg/runtime/vlop_arm.s
ファイルにあります。
--- a/src/pkg/runtime/vlop_arm.s
+++ b/src/pkg/runtime/vlop_arm.s
@@ -255,7 +255,7 @@ TEXT _div(SB),NOSPLIT,$16
d0:
BL udiv<>(SB) /* none/both neg */
MOVW R(q), R(TMP)
- B out
+ B out1
d1:
CMP $0, R(q)
BGE d0
@@ -263,7 +263,12 @@ d1:
d2:
BL udiv<>(SB) /* one neg */
RSB $0, R(q), R(TMP)
- B out
+out1:
+ MOVW 4(R13), R(q)
+ MOVW 8(R13), R(r)
+ MOVW 12(R13), R(s)
+ MOVW 16(R13), R(M)
+ RET
TEXT _mod(SB),NOSPLIT,$16
MOVW R(q), 4(R13)
コアとなるコードの解説
この変更は、_div
関数内のジャンプ先を変更し、_div
関数自身の終了処理を独立させることで、クロスファンクションジャンプを解消しています。
変更前:
TEXT _div(SB),NOSPLIT,$16
...
d0:
BL udiv<>(SB) /* none/both neg */
MOVW R(q), R(TMP)
- B out // ここで 'out' ラベルにジャンプ
...
d2:
BL udiv<>(SB) /* one neg */
RSB $0, R(q), R(TMP)
- B out // ここでも 'out' ラベルにジャンプ
...
// 'out' ラベルは _mod(SB) 関数の近くに定義されていたと推測される
// 例えば、_mod(SB) 関数の内部か、その直前/直後に共通の終了処理として存在していた可能性
変更前は、_div
関数内の複数の箇所から out
というラベルにジャンプしていました。この out
ラベルは、おそらく _mod
関数(TEXT _mod(SB),NOSPLIT,$16
の行の下に続くコード)の近く、またはその内部に定義されており、_div
関数が _mod
関数の途中にあるコードブロックに直接ジャンプする形になっていました。これが「クロスファンクションジャンプ」であり、新しいリンカが禁止するパターンです。
変更後:
TEXT _div(SB),NOSPLIT,$16
...
d0:
BL udiv<>(SB) /* none/both neg */
MOVW R(q), R(TMP)
- B out1 // 新しい 'out1' ラベルにジャンプ
...
d2:
BL udiv<>(SB) /* one neg */
RSB $0, R(q), R(TMP)
- B out1 // ここでも 'out1' ラベルにジャンプ
+out1: // _div 関数内に新しいラベル 'out1' を定義
+ MOVW 4(R13), R(q) // R13 (SP) からオフセット4の値を R(q) にロード
+ MOVW 8(R13), R(r) // R13 (SP) からオフセット8の値を R(r) にロード
+ MOVW 12(R13), R(s) // R13 (SP) からオフセット12の値を R(s) にロード
+ MOVW 16(R13), R(M) // R13 (SP) からオフセット16の値を R(M) にロード
+ RET // 関数から戻る
変更後では、_div
関数内のジャンプ命令 B out
が B out1
に変更されました。そして、_div
関数の内部に out1
という新しいラベルが定義されました。
out1:
ラベル以下の命令は、_div
関数の終了処理の一部です。
MOVW 4(R13), R(q)
MOVW 8(R13), R(r)
MOVW 12(R13), R(s)
MOVW 16(R13), R(M)
これらの命令は、スタックポインタR13
(ARMでは通常SP
レジスタ) を基準としたオフセットから、レジスタR(q)
,R(r)
,R(s)
,R(M)
に値をロードしています。これは、関数呼び出し規約に従って、呼び出し元がスタックに保存したレジスタの値を復元しているものと考えられます。Goのアセンブリでは、R(q)
のような表記は、Goのコンパイラが内部的に使用するレジスタのエイリアスであり、実際のARMレジスタ(例:R0
,R1
など)にマッピングされます。
最後に RET
命令が実行され、_div
関数から呼び出し元に戻ります。
この変更により、_div
関数は他の関数の中間地点にジャンプすることなく、自身の内部で終了処理を完結させ、独立した関数として振る舞うようになりました。これにより、新しいGoリンカが各関数を独立して配置・最適化できるようになり、リンカのクリーンアップ作業が進行可能になります。
関連リンク
- Go CL 35800043: https://golang.org/cl/35800043
参考にした情報源リンク
- Go 1.3 Linker Cleanup Proposal: https://golang.org/s/go13linker
- Go 1.3 Release Notes (Linker section): https://go.dev/doc/go1.3#linker
- ARM Assembly Language Programming (General concepts)
- Go Assembly Language (General concepts)
- Go Runtime Source Code (General understanding of
src/pkg/runtime/
)