[インデックス 17615] ファイルの概要
このコミットは、Go言語のARMアーキテクチャ向けランタイムにおける整数除算のゼロ除算時のトレースバックの挙動を改善し、よりデバッグしやすいようにするための変更です。特に、Goのツールチェイン5における除算の実装が持つ「魔法のような」挙動をトレースバックルーチンから隠蔽し、ソフトウェアによる除算ルーチンの結果を検証するための新しいテストを追加しています。
コミット
commit b2794a1c2ed8c74563cf28d9e4a9b3f1db43ef1f
Author: Russ Cox <rsc@golang.org>
Date: Mon Sep 16 14:04:45 2013 -0400
runtime: make ARM integer div-by-zero traceback-friendly
The implementation of division in the 5 toolchain is a bit too magical.
Hide the magic from the traceback routines.
Also add a test for the results of the software divide routine.
Fixes #5805.
R=golang-dev, minux.ma
CC=golang-dev
https://golang.org/cl/13239052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b2794a1c2ed8c74563cf28d9e4a9b3f1db43ef1f
元コミット内容
runtime: make ARM integer div-by-zero traceback-friendly
The implementation of division in the 5 toolchain is a bit too magical.
Hide the magic from the traceback routines.
Also add a test for the results of the software divide routine.
Fixes #5805.
変更の背景
このコミットの主な背景は、Go言語のARMアーキテクチャ向けランタイムにおける整数除算、特にゼロ除算が発生した際のトレースバック(スタックトレース)の可読性と正確性の問題にありました。当時のGoツールチェイン5(go tool 5
)は、ARMプロセッサ上での整数除算を効率的に行うために、アセンブリレベルで特定の最適化や「魔法のような」実装を行っていました。
この「魔法のような」実装とは、コンパイラやリンカが、Goコード中のDIV
やMOD
といった除算・剰余演算子を、実際にはランタイム内のソフトウェア除算ルーチンへの関数呼び出しに変換するプロセスを指します。この変換は、通常の関数呼び出しとは異なり、スタックフレームのレイアウトやレジスタの使用方法に特殊な処理が含まれていました。
問題は、ゼロ除算のような例外が発生し、ランタイムがパニック(Goにおける実行時エラー)を生成してトレースバックを出力しようとした際に、この特殊な実装が原因でトレースバックが正しく生成されない、あるいはデバッグに役立たない情報しか提供されないという点でした。具体的には、トレースバックルーチンが期待するスタックフレームの構造と、実際の除算ルーチン呼び出し時のスタックフレームの構造が異なっていたため、呼び出し元の正確な位置(PC: Program Counter)や、保存されたリターンアドレス(LR: Link Register)を特定するのが困難でした。
このコミットは、この問題を解決し、ゼロ除算が発生した際に開発者がより有用なデバッグ情報を得られるようにすることを目的としています。また、ソフトウェア除算ルーチン自体の正確性を保証するために、広範なテストケースを追加することも目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念が重要です。
- ARMアーキテクチャ: ARMは、モバイルデバイスや組み込みシステムで広く使用されているRISC(Reduced Instruction Set Computer)ベースのCPUアーキテクチャです。Go言語は、ARMを含む多くのアーキテクチャをサポートしており、それぞれのアーキテクチャに特化したランタイムコード(アセンブリ言語で記述されることが多い)を持っています。
- Goランタイム (runtime): Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ガベージコレクション、スケジューリング、メモリ管理、プリミティブな操作(例えば、整数除算のようなCPU命令に直接マッピングされない複雑な操作)など、プログラムの実行に必要な低レベルな機能を提供します。
- アセンブリ言語: CPUが直接実行できる機械語命令を人間が読める形式で記述したものです。Goランタイムのパフォーマンスが重要な部分や、特定のハードウェア機能にアクセスする必要がある部分は、アセンブリ言語で記述されることがあります。このコミットで変更されている
vlop_arm.s
ファイルは、ARMアセンブリ言語で書かれたGoランタイムの一部です。 - トレースバック (Traceback / Stack Trace): プログラムの実行中にエラーやパニックが発生した際に、その時点での関数呼び出しの履歴(コールスタック)を表示する機能です。これにより、エラーがどこで、どのような関数の流れで発生したかを特定し、デバッグを行うことができます。トレースバックは、各関数の呼び出し元のアドレス(PC)や、関数がリターンする際のアドレス(LR)などの情報に基づいて構築されます。
- ゼロ除算 (Division by Zero): 整数をゼロで割ろうとする操作です。数学的に未定義であり、プログラムでは通常、実行時エラー(パニックや例外)を引き起こします。
- Goツールチェイン5 (5 toolchain): Go言語の初期のバージョンで使用されていたコンパイラとリンカのセットを指します。Goは、異なるアーキテクチャやOS向けにクロスコンパイルを行うためのツールチェインを提供しています。当時のARM向けツールチェインは、整数除算の処理に特定のアプローチを採用していました。
- スタックフレーム: 関数が呼び出されるたびに、その関数のローカル変数、引数、リターンアドレスなどがメモリ上のスタックに割り当てられる領域です。トレースバックルーチンは、このスタックフレームの情報を解析して呼び出し履歴を再構築します。
- LR (Link Register): ARMアーキテクチャにおいて、関数呼び出し時にリターンアドレス(呼び出し元に戻るべきアドレス)を保持するために使用されるレジスタです。関数から戻る際には、このLRの値がプログラムカウンタ(PC)にロードされます。
技術的詳細
このコミットの技術的な核心は、ARMアーキテクチャにおけるGoランタイムのソフトウェア除算ルーチンudiv
(符号なし除算)およびidiv
(符号付き除算)が、ゼロ除算時にパニックを発生させる際のトレースバックの挙動を修正することにあります。
Goツールチェイン5では、Goのソースコード中のx / y
やx % y
といった除算・剰余演算は、直接ARMの除算命令に変換されるのではなく、ランタイム内のアセンブリで実装されたソフトウェア除算ルーチン(runtime·_divu
, runtime·_modu
, runtime·_div
, runtime·_mod
など)への呼び出しに変換されていました。この変換は、リンカによって行われる「擬似命令(pseudo-instruction)」の展開として記述されています。
問題は、この擬似命令が展開される際に、通常の関数呼び出しとは異なるスタックフレームのレイアウトが生成されることにありました。特に、トレースバックルーチンが期待する「スタックフレームの最下位ワードに保存されたLR(リターンアドレス)」が、この特殊なフレームレイアウトでは異なる位置に存在していました。そのため、ゼロ除算が発生してruntime·panicdivide
が呼び出された際に、トレースバックルーチンが正しい呼び出し元(擬似命令が展開された場所)を特定できず、デバッグが困難になっていました。
このコミットでは、udiv_by_0
という新しいラベルを追加し、ゼロ除算が発生した場合の処理を分離しています。このudiv_by_0
セクションでは、以下の重要な処理が行われます。
- スタックの巻き戻し:
MOVW 0(R13), LR
とADD $20, R13
によって、現在のスタックポインタ(R13)を、擬似命令が呼び出された時点のスタックフレームの開始位置まで巻き戻します。これは、リンカが擬似命令を展開する際に、スタックに2つの値をプッシュし、さらに16バイトのフレームと保存されたLRを確保するため、合計で20バイト(2つの値 + 16バイト)のオフセットが生じることを考慮しています。 - LRの再配置:
MOVW 8(R13), R1
で実際の保存されたLR(擬似命令の呼び出し元のアドレス)をR1レジスタにロードし、MOVW R1, 0(R13)
でそのLRを、トレースバックルーチンが期待するスタックフレームの最下位ワード(オフセット0)にコピーします。 panicdivide
へのジャンプ: 最後にB runtime·panicdivide(SB)
を使用して、runtime·panicdivide
関数にジャンプします。この時、LRが正しい位置に配置されているため、panicdivide
がトレースバックを生成する際に、あたかも擬似命令の呼び出し元から直接呼び出されたかのように見せかけることができます。
これにより、ゼロ除算によるパニック発生時でも、正確な呼び出し履歴がトレースバックに表示されるようになり、デバッグの効率が向上します。
また、このコミットではtest/divmod.go
という新しいテストファイルが追加されています。このテストは、Goの組み込みの除算・剰余演算子(/
と%
)が、Goランタイムのソフトウェア除算ルーチン(udiv
とidiv
)と同じ結果を返すことを検証します。特に、様々なビット数の整数型(uint
, uint64
, uint32
, uint16
, uint8
, int
, int64
, int32
, int16
, int8
)に対して、広範な入力値(特に境界値やビットパターンが特殊な値)を生成し、その結果を比較することで、ソフトウェア除算ルーチンの正確性を保証しています。ゼロ除算時のパニックも適切に発生するかどうかもテストしています。
コアとなるコードの変更箇所
src/pkg/runtime/vlop_arm.s
このファイルは、ARMアーキテクチャ向けのGoランタイムの低レベルな演算(vlop
は"variable length operations"の略か、あるいは"vector operations"の誤記の可能性もあるが、ここでは整数演算に関連する)を実装するアセンブリファイルです。
主な変更点は以下の通りです。
udiv_by_0_or_1
セクションの変更:- 以前は
MOVW.CS R(r), R(q)
、MOVW.CS $0, R(r)
、BL.CC runtime·panicdivide(SB)
という条件付き命令と直接呼び出しでゼロ除算を処理していました。 - 変更後、
BCC udiv_by_0
という条件付き分岐が追加され、ゼロ除算の場合の処理がudiv_by_0
という新しいセクションに分離されました。 d==1
(除数が1)の場合の処理(MOVW R(r), R(q)
、MOVW $0, R(r)
、RET
)が明確に分離されました。
- 以前は
udiv_by_0
セクションの追加:- この新しいセクションが追加され、ゼロ除算時のスタックフレームの調整と
runtime·panicdivide
へのジャンプロジックが実装されました。 MOVW 0(R13), LR
ADD $20, R13
MOVW 8(R13), R1
MOVW R1, 0(R13)
B runtime·panicdivide(SB)
- この新しいセクションが追加され、ゼロ除算時のスタックフレームの調整と
RET
命令の使用:- 以前はコメントで「
RET
は使えない」と書かれていた箇所(udiv
とudiv_by_large_d
の末尾)で、MOVW R14, R15
(LRをPCにコピーしてリターン)の代わりにRET
命令が使用されるようになりました。これは、リンカの挙動が改善されたか、あるいはこの変更によってRET
が安全に使用できるようになったことを示唆しています。
- 以前はコメントで「
fast_udiv_tab
の参照方法の変更:CLZ
命令後のADD R(a)>>25, PC, R(a)
というPC相対アドレス指定から、MOVW $fast_udiv_tab<>-64(SB), R(M)
とMOVBU.NE R(a)>>25(R(M)), R(a)
というグローバルシンボル相対アドレス指定に変更されました。これにより、コードの再配置に対する堅牢性が向上しています。
test/divmod.go
このファイルは、Goの整数除算と剰余演算の正確性を検証するための新しいテストスイートです。
main
関数: テストの実行エントリポイント。gen2
関数を呼び出して、様々な入力値の組み合わせを生成し、checkdiv1
関数に渡します。long
フラグによって、テストケースの数を調整できます。gen1
,gen
,gen2
関数: テストケースの入力値(uint64
)を生成するためのヘルパー関数。特に、特定のビット数だけがセットされた値や、その周辺の値を効率的に生成します。checkdiv1
,checkdiv2
,checkdiv3
関数: 生成された入力値に対して、様々な整数型(uint
,int
とその派生型)での除算・剰余演算の結果を検証します。checkuint
,checkuint64
, ...,checkint8
関数: 各整数型における除算・剰余演算の結果を、ソフトウェア除算ルーチン(udiv
,idiv
)の結果と比較し、不一致があればエラーメッセージを出力します。divzerouint
,divzerouint64
, ...,modzeroint8
関数: ゼロ除算・ゼロ剰余演算が適切にパニックを引き起こすことを検証するための関数。defer
とrecover
を使用してパニックの発生を捕捉します。udiv(x, y uint64) (q, r uint64)
: 符号なし整数除算と剰余を、シフトと減算のみで実装したリファレンス実装。idiv(x, y int64) (q, r int64)
: 符号付き整数除算と剰余を、udiv
を呼び出して結果の符号を調整することで実装したリファレンス実装。
コアとなるコードの解説
src/pkg/runtime/vlop_arm.s
の udiv_by_0
セクション
このコミットの最も重要な変更は、udiv_by_0
セクションの追加とそのロジックです。
udiv_by_0:
// The ARM toolchain expects it can emit references to DIV and MOD
// instructions. The linker rewrites each pseudo-instruction into
// a sequence that pushes two values onto the stack and then calls
// _divu, _modu, _div, or _mod (below), all of which have a 16-byte
// frame plus the saved LR. The traceback routine knows the expanded
// stack frame size at the pseudo-instruction call site, but it
// doesn't know that the frame has a non-standard layout. In particular,
// it expects to find a saved LR in the bottom word of the frame.
// Unwind the stack back to the pseudo-instruction call site, copy the
// saved LR where the traceback routine will look for it, and make it
// appear that panicdivide was called from that PC.
MOVW 0(R13), LR // スタックポインタR13が指すアドレスからLRを読み込む
ADD $20, R13 // スタックポインタR13を20バイト進める(スタックを巻き戻す)
MOVW 8(R13), R1 // 巻き戻されたスタックポインタから8バイトオフセットの位置にある実際のLRをR1に読み込む
MOVW R1, 0(R13) // R1に保存されたLRを、トレースバックルーチンが期待するスタックフレームの最下位ワード(オフセット0)に書き込む
B runtime·panicdivide(SB) // runtime·panicdivide関数へ無条件分岐(ジャンプ)
このアセンブリコードは、Goのコンパイラとリンカが生成する特殊なコードパスを理解し、それに対応するためのものです。
MOVW 0(R13), LR
: これは、現在のスタックポインタR13
が指すアドレス(スタックの最上位)から値を読み込み、それをLR
レジスタに格納しています。これは、panicdivide
が呼び出される直前のスタックの状態を調整する準備です。ADD $20, R13
: ここが最も重要な部分です。R13
(スタックポインタ)に20を加算しています。これは、スタックを20バイト分「巻き戻す」ことを意味します。コメントにもあるように、リンカが擬似命令を展開する際に、スタックに2つの値(おそらく引数)をプッシュし、さらに16バイトのスタックフレーム(ローカル変数やレジスタ保存用)と保存されたLRを確保します。この合計20バイト(2*4バイト + 16バイト)のオフセットを考慮して、スタックポインタを擬似命令が呼び出された時点のスタックフレームの開始位置に戻しています。MOVW 8(R13), R1
: スタックポインタが巻き戻された後、その位置から8バイトのオフセットにあるメモリの内容をR1
レジスタに読み込んでいます。この8バイトオフセットの位置に、本来の呼び出し元(Goコード中の/
や%
演算子があった場所)のリターンアドレス(LR)が保存されています。MOVW R1, 0(R13)
:R1
に読み込んだ実際のLRを、巻き戻されたスタックポインタR13
が指すアドレス(つまり、トレースバックルーチンが「保存されたLR」として期待するスタックフレームの最下位ワード)に書き込んでいます。これにより、トレースバックルーチンは、この位置から正しいリターンアドレスを読み取ることができるようになります。B runtime·panicdivide(SB)
: 最後に、runtime·panicdivide
関数に無条件でジャンプします。このジャンプが行われるときには、スタックフレームがトレースバックルーチンにとって「正常な」状態に調整されているため、panicdivide
は正しい呼び出し元を特定し、正確なトレースバックを生成できるようになります。
この一連の操作により、GoのARMランタイムにおける整数ゼロ除算のトレースバックが、開発者にとってより意味のあるものに改善されました。
test/divmod.go
の udiv
と idiv
test/divmod.go
に含まれる udiv
と idiv
関数は、Goの組み込みの除算・剰余演算子の動作を検証するためのリファレンス実装です。これらは、CPUの除算命令やランタイムのソフトウェア除算ルーチンに依存せず、ビットシフトと減算のみで除算と剰余を計算します。
// unsigned divide and mod using shift and subtract.
func udiv(x, y uint64) (q, r uint64) {
sh := 0
for y+y > y && y+y <= x { // yをxに近づけるために左シフトする回数を決定
sh++
y <<= 1
}
for ; sh >= 0; sh-- { // 決定したシフト回数から逆順に処理
q <<= 1 // 商を左シフト
if x >= y { // xがy以上であれば、商の現在のビットを1にし、xからyを引く
x -= y
q |= 1
}
y >>= 1 // yを右シフト
}
return q, x // 商と最終的なx(剰余)を返す
}
// signed divide and mod: do unsigned and adjust signs.
func idiv(x, y int64) (q, r int64) {
// special case for minint / -1 = minint
if x-1 > x && y == -1 { // 最小の負の整数(MinInt)を-1で割る特殊ケース
return x, 0
}
ux := uint64(x)
uy := uint64(y)
if x < 0 { // xが負の場合、符号なしに変換
ux = -ux
}
if y < 0 { // yが負の場合、符号なしに変換
uy = -uy
}
uq, ur := udiv(ux, uy) // 符号なし除算を実行
q = int64(uq)
r = int64(ur)
if x < 0 { // 元のxが負の場合、剰余の符号を調整
r = -r
}
if (x < 0) != (y < 0) { // xとyの符号が異なる場合、商の符号を反転
q = -q
}
return q, r
}
これらの関数は、Goのコンパイラが生成する除算コードやランタイムのソフトウェア除算ルーチンが、これらの「純粋な」実装と同じ結果を返すことを確認するための基準として機能します。これにより、Goの整数除算の正確性が、様々なエッジケースや境界値において保証されます。
関連リンク
- Go Issue #5805:
runtime: make ARM integer div-by-zero traceback-friendly
- このコミットが修正した問題のトラッキングイシュー。 - Go Code Review CL 13239052:
runtime: make ARM integer div-by-zero traceback-friendly
- このコミットのコードレビューページ。
参考にした情報源リンク
- Go言語の公式ドキュメント (Go runtime, ARM architectureに関する情報)
- ARMアーキテクチャのリファレンスマニュアル (特にレジスタとスタックフレームの規約について)
- Go言語のソースコード (特に
src/pkg/runtime/
以下のARM関連のアセンブリファイル) - Go言語のIssueトラッカーとCode Reviewシステム (関連する議論や背景情報の把握のため)
- 一般的なコンピュータアーキテクチャとコンパイラの原理に関する知識