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

[インデックス 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インターフェースの遅延呼び出しに関連していたことが示唆されます。

前提知識の解説

このコミットを理解するためには、以下の前提知識が必要です。

  1. Goのdeferステートメント: deferは、Go言語のキーワードで、その後の関数呼び出しを、囲む関数がreturnする直前(またはパニックが発生する前)に実行するようにスケジュールします。これはリソースの解放(ファイルクローズ、ロック解除など)によく使用されます。deferされた関数はLIFO(後入れ先出し)順で実行されます。

  2. Goランタイム: Goプログラムは、Goランタイムと呼ばれる軽量な実行環境上で動作します。ランタイムは、ガベージコレクション、ゴルーチンのスケジューリング、チャネル通信、メモリ管理など、Go言語の多くの低レベルな側面を処理します。

  3. アセンブリ言語 (x86, AMD64, ARM): Goランタイムの低レベルな部分は、パフォーマンスとシステムとの直接的な対話のためにアセンブリ言語で記述されています。

    • x86 (386): 32ビットIntel/AMDプロセッサアーキテクチャ。
    • AMD64: 64ビットIntel/AMDプロセッサアーキテクチャ(x86-64とも呼ばれる)。
    • ARM: モバイルデバイスや組み込みシステムで広く使用されるRISCベースのプロセッサアーキテクチャ。 これらのアーキテクチャでは、関数呼び出し規約、スタックフレームのレイアウト、レジスタの使用方法が異なります。
  4. スタックフレームとスタックポインタ (SP): 関数が呼び出されると、その関数のローカル変数、引数、リターンアドレスなどを格納するためのメモリ領域がスタック上に確保されます。これをスタックフレームと呼びます。スタックポインタ(SP)レジスタは、現在のスタックの最上位(または最下位、アーキテクチャによる)を指します。関数呼び出しやスタック操作では、SPが適切に調整される必要があります。

  5. 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関数の定義行です。

  1. 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
    
  2. 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
    
  3. 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
    

また、バグ修正を検証するための新しいテストファイルが追加されています。

  1. 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の変更リストへのリンクです。)