[インデックス 17082] ファイルの概要
このコミットは、Goランタイムにおけるjmpdefer
関数の引数サイズに関する修正です。具体的には、jmpdefer
が呼び出される際のスタックフレームのサイズを正確に記録することで、バグ6055を修正しています。この修正は、386、AMD64、およびARMアーキテクチャのランタイムアセンブリコードに影響を与え、関連するテストケースが追加されています。
コミット
commit a97a91de06b3f071a08314c7cb54eac57c4a624a
Author: Keith Randall <khr@golang.org>
Date: Wed Aug 7 14:03:50 2013 -0700
runtime: Record jmpdefer's argument size.
Fixes bug 6055.
R=golang-dev, bradfitz, dvyukov, khr
CC=golang-dev
https://golang.org/cl/12536045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a97a91de06b3f071a08314c7cb54eac57c4a624a
元コミット内容
runtime: Record jmpdefer's argument size.
Fixes bug 6055.
R=golang-dev, bradfitz, dvyukov, khr
CC=golang-dev
https://golang.org/cl/12536045
変更の背景
このコミットは、Goランタイムにおける特定のバグ、すなわち「バグ6055」を修正するために行われました。コミットメッセージからは具体的なバグの内容は読み取れませんが、jmpdefer
関数の引数サイズが正しく記録されていなかったことが問題であったと推測されます。Goのdefer
ステートメントは、関数の実行が終了する直前に指定された関数を呼び出すための強力な機能です。このdefer
の内部実装では、ランタイムがスタックフレームを操作し、遅延実行される関数の呼び出しを管理します。
jmpdefer
は、おそらくこのdefer
メカニズムの一部として、特定の状況下で実行フローをジャンプさせるために使用されるランタイム関数であると考えられます。引数サイズの不正確な記録は、スタックの破損、不正なメモリアクセス、または予期せぬパニックを引き起こす可能性があり、プログラムの安定性と正確性に重大な影響を与えます。特に、インターフェース型のメソッド呼び出しがdefer
された場合に問題が発生した可能性があります。追加されたテストケース(test/fixedbugs/issue6055.go
)がnil
インターフェースのClose()
メソッドをdefer
呼び出し、それがパニックを引き起こすことを期待していることから、この問題がnil
インターフェースの遅延呼び出しに関連していたことが示唆されます。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
-
Goの
defer
ステートメント:defer
は、Go言語のキーワードで、その後の関数呼び出しを、囲む関数がreturnする直前(またはパニックが発生する前)に実行するようにスケジュールします。これはリソースの解放(ファイルクローズ、ロック解除など)によく使用されます。defer
された関数はLIFO(後入れ先出し)順で実行されます。 -
Goランタイム: Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ガベージコレクション、ゴルーチンのスケジューリング、チャネル通信、メモリ管理など、Go言語の多くの低レベルな側面を処理します。
-
アセンブリ言語 (x86, AMD64, ARM): Goランタイムの低レベルな部分は、パフォーマンスとシステムとの直接的な対話のためにアセンブリ言語で記述されています。
- x86 (386): 32ビットIntel/AMDプロセッサアーキテクチャ。
- AMD64: 64ビットIntel/AMDプロセッサアーキテクチャ(x86-64とも呼ばれる)。
- ARM: モバイルデバイスや組み込みシステムで広く使用されるRISCベースのプロセッサアーキテクチャ。 これらのアーキテクチャでは、関数呼び出し規約、スタックフレームのレイアウト、レジスタの使用方法が異なります。
-
スタックフレームとスタックポインタ (SP): 関数が呼び出されると、その関数のローカル変数、引数、リターンアドレスなどを格納するためのメモリ領域がスタック上に確保されます。これをスタックフレームと呼びます。スタックポインタ(SP)レジスタは、現在のスタックの最上位(または最下位、アーキテクチャによる)を指します。関数呼び出しやスタック操作では、SPが適切に調整される必要があります。
-
TEXT
ディレクティブとNOSPLIT
、$
記号: Goのアセンブリでは、TEXT
ディレクティブは関数の定義を開始します。TEXT runtime·jmpdefer(SB), NOSPLIT, $0-8
のような形式は、Goのアセンブリ構文です。runtime·jmpdefer(SB)
:runtime
パッケージのjmpdefer
関数を指します。SB
は静的ベースポインタで、グローバルシンボルを参照するために使用されます。NOSPLIT
: この関数がスタックを分割しないことを示します。つまり、スタックの拡張チェックを行いません。これは、非常に低レベルで重要なランタイム関数でよく見られます。$0-8
: ここがこのコミットの核心です。これは、関数のスタックフレームサイズと引数サイズを示します。- 最初の数値(
$0
)は、この関数自身のスタックフレームサイズ(ローカル変数など)を示します。この場合は0バイトです。 - ハイフンの後の数値(
-8
)は、この関数が期待する引数の合計サイズを示します。この場合、8バイトの引数を期待していることを意味します。この値が正しくないと、スタック上の引数を誤って読み取ったり、スタックが破損したりする可能性があります。
- 最初の数値(
技術的詳細
このコミットの技術的な核心は、Goランタイムのアセンブリコードにおけるjmpdefer
関数の定義にあります。jmpdefer
は、defer
された関数が実際に呼び出される際に、実行フローをその関数にジャンプさせるための内部的なメカニズムです。
Goの関数呼び出し規約では、引数はスタックにプッシュされるか、レジスタを介して渡されます。jmpdefer
のようなランタイム関数は、通常のGo関数とは異なる特殊な呼び出し規約を持つことがよくあります。特に、defer
メカニズムは、遅延実行される関数の情報(関数ポインタ、レシーバ、引数など)をスタック上に保存し、適切なタイミングでそれらを復元して呼び出す必要があります。
以前のコードでは、jmpdefer
関数の定義がTEXT runtime·jmpdefer(SB), NOSPLIT, $0
となっていました。これは、この関数が自身のスタックフレームを0バイト使用し、引数も0バイトであると宣言していました。しかし、jmpdefer
は実際には引数(例えば、ジャンプ先の関数ポインタや関連データ)を受け取るため、この$0
という宣言は不正確でした。
この不正確さが、特定のシナリオ(特にnil
インターフェースのメソッドをdefer
した場合)でバグを引き起こしていました。jmpdefer
が引数を受け取るにもかかわらず、ランタイムがその引数サイズを認識していなかったため、スタックポインタの調整や引数の読み取りが正しく行われず、結果としてパニックやクラッシュが発生したと考えられます。
修正は、各アーキテクチャ(386、AMD64、ARM)のjmpdefer
関数の定義において、引数サイズを正確な値に更新することです。
- 386アーキテクチャ:
$0
から$0-8
へ変更。これは、32ビットシステムで8バイトの引数(例えば、2つの32ビットポインタ)を期待していることを示唆します。 - AMD64アーキテクチャ:
$0
から$0-16
へ変更。これは、64ビットシステムで16バイトの引数(例えば、2つの64ビットポインタ)を期待していることを示唆します。 - ARMアーキテクチャ:
$0
から$0-8
へ変更。これは、ARMアーキテクチャで8バイトの引数を期待していることを示唆します。
これらの変更により、Goランタイムはjmpdefer
が呼び出されたときにスタック上の引数を正しく認識し、適切に処理できるようになります。これにより、バグ6055が修正され、defer
ステートメントの堅牢性が向上しました。
追加されたテストケースtest/fixedbugs/issue6055.go
は、このバグを再現し、修正が正しく機能することを確認するためのものです。このテストは、nil
インターフェースのメソッド(Close()
)をdefer
呼び出し、それがパニックを引き起こすことを期待しています。修正前は、このシナリオで予期せぬ動作やクラッシュが発生していた可能性がありますが、修正後は期待通りにパニックが発生し、ランタイムがエラーを適切に処理できるようになったことを示します。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、以下の3つのアセンブリファイルにおけるjmpdefer
関数の定義行です。
-
src/pkg/runtime/asm_386.s
--- a/src/pkg/runtime/asm_386.s +++ b/src/pkg/runtime/asm_386.s @@ -537,7 +537,7 @@ TEXT runtime·atomicstore64(SB), NOSPLIT, $0-12 // 1. pop the caller // 2. sub 5 bytes from the callers return // 3. jmp to the argument -TEXT runtime·jmpdefer(SB), NOSPLIT, $0 +TEXT runtime·jmpdefer(SB), NOSPLIT, $0-8 MOVL 4(SP), DX // fn MOVL 8(SP), BX // caller sp LEAL -4(BX), SP // caller sp after CALL
-
src/pkg/runtime/asm_amd64.s
--- a/src/pkg/runtime/asm_amd64.s +++ b/src/pkg/runtime/asm_amd64.s @@ -577,7 +577,7 @@ TEXT runtime·atomicstore64(SB), NOSPLIT, $0-16 // 1. pop the caller // 2. sub 5 bytes from the callers return // 3. jmp to the argument -TEXT runtime·jmpdefer(SB), NOSPLIT, $0 +TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16 MOVQ 8(SP), DX // fn MOVQ 16(SP), BX // caller sp LEAQ -8(BX), SP // caller sp after CALL
-
src/pkg/runtime/asm_arm.s
--- a/src/pkg/runtime/asm_arm.s +++ b/src/pkg/runtime/asm_arm.s @@ -367,7 +367,7 @@ TEXT runtime·lessstack(SB), NOSPLIT, $-4-0 // 1. grab stored LR for caller // 2. sub 4 bytes to get back to BL deferreturn // 3. B to fn -TEXT runtime·jmpdefer(SB), NOSPLIT, $0 +TEXT runtime·jmpdefer(SB), NOSPLIT, $0-8 MOVW 0(SP), LR MOVW $-4(LR), LR // BL deferreturn MOVW fn+0(FP), R7
また、バグ修正を検証するための新しいテストファイルが追加されています。
test/fixedbugs/issue6055.go
// run // Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main type Closer interface { Close() } func nilInterfaceDeferCall() { var x Closer defer x.Close() } func shouldPanic(f func()) { defer func() { if recover() == nil { panic("did not panic") } }() f() } func main() { shouldPanic(nilInterfaceDeferCall) }
コアとなるコードの解説
各アセンブリファイルにおける変更は、jmpdefer
関数のTEXT
ディレクティブの引数サイズ指定を修正することに集約されます。
-
TEXT runtime·jmpdefer(SB), NOSPLIT, $0
から$0-8
または$0-16
への変更:- 以前の
$0
は、この関数が自身のスタックフレームを0バイト使用し、かつ引数も0バイトであると宣言していました。 - 修正後の
$0-8
(386, ARM)や$0-16
(AMD64)は、この関数が自身のスタックフレームを0バイト使用するが、8バイトまたは16バイトの引数を期待していることをランタイムに正確に伝えます。 - この引数サイズは、
jmpdefer
がジャンプ先の関数ポインタや、defer
された関数を呼び出すために必要なその他のコンテキスト情報を受け取るために必要なスタック上の領域に対応します。 - この修正により、ランタイムは
jmpdefer
が呼び出された際にスタックポインタを正しく調整し、引数を適切に読み取ることができるようになります。これにより、スタックの破損や不正なメモリアクセスが防止され、バグ6055が解決されます。
- 以前の
-
test/fixedbugs/issue6055.go
の追加:- このテストファイルは、
nil
インターフェースのメソッド(x.Close()
)をdefer
呼び出しするシナリオを再現します。 nilInterfaceDeferCall
関数内でvar x Closer
と宣言されたインターフェース変数x
は初期化されていないため、nil
です。defer x.Close()
は、nil
インターフェースのメソッドを呼び出そうとするため、Goの仕様によりパニックが発生するはずです。shouldPanic
ヘルパー関数は、引数として渡された関数がパニックを引き起こすことを検証します。もしパニックが発生しなければ、それ自体がパニックを引き起こします。main
関数でshouldPanic(nilInterfaceDeferCall)
を呼び出すことで、nil
インターフェースの遅延呼び出しが期待通りにパニックを引き起こすことを確認します。- このテストの追加は、修正が特定のバグシナリオ(
nil
インターフェースのdefer
呼び出し)を正しく処理できるようになったことを保証します。
- このテストファイルは、
これらの変更は、Goランタイムの低レベルなスタック管理とdefer
メカニズムの正確性を向上させ、Goプログラムの安定性を高める上で重要な役割を果たしています。
関連リンク
- Go言語の
defer
ステートメントに関する公式ドキュメントやチュートリアル - Goランタイムの内部構造に関する技術記事やドキュメント
- Goのアセンブリ言語(Plan 9アセンブラ)の構文と使用法に関する情報
参考にした情報源リンク
- Go言語の公式ドキュメント: https://golang.org/doc/
- Goのソースコードリポジトリ: https://github.com/golang/go
- GoのIssue Tracker (バグ6055の具体的な詳細については、当時のGoのIssue Trackerで検索する必要があるかもしれません。ただし、古いバグはアーカイブされている可能性があります。)
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージに記載されている
https://golang.org/cl/12536045
は、このGerritの変更リストへのリンクです。)