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

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

このコミットは、Goランタイムのtraceback_x86.cファイルにおけるビルドの問題を修正するものです。具体的には、x86アーキテクチャでのスタックトレース生成ロジックにおいて、関数が自身のスタックフレームをまだ作成していない状態を正しく判定するための条件を修正しています。

コミット

commit 80efeff20a88392b13f78338f5a17605fc55e460
Author: Russ Cox <rsc@golang.org>
Date:   Wed Jun 12 09:06:28 2013 -0400

    runtime: fix build
    
    TBR=dvyukov
    CC=golang-dev
    https://golang.org/cl/10227044

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

https://github.com/golang/go/commit/80efeff20a88392b13f78338f5a17605fc55e460

元コミット内容

このコミットは、Goランタイムのビルドプロセスにおける問題を解決することを目的としています。特に、traceback_x86.cファイル内のスタックトレース生成ロジックに焦点を当て、関数がスタックフレームをまだ設定していない場合の条件判定を修正しています。これにより、ビルドが正常に完了し、ランタイムの安定性が向上することが期待されます。

変更の背景

この変更の背景には、Goランタイムのスタックトレース生成メカニズムにおける潜在的なバグがありました。runtime·gentraceback関数は、プログラムの実行中にスタックトレースを生成するために使用されます。この関数は、各スタックフレームの情報を正確に解析し、ローカル変数の位置やサイズを特定する必要があります。

問題は、特定の条件下で、関数が呼び出された直後でまだ自身のスタックフレームを完全に設定していない場合に、その状態を正しく検出できないことにありました。従来のコードでは、フレームポインタ(fp)とスタックポインタ(sp)が等しい場合に、関数がフレームをまだ作成していないと判断していました。しかし、x86アーキテクチャの呼び出し規約では、関数が呼び出されるとリターンアドレスがスタックにプッシュされるため、spfpとは異なる位置(通常はfpからsizeof(uintptr)だけずれた位置)になります。この不一致が原因で、スタックトレースの生成が誤動作し、結果としてランタイムのビルドが失敗する、あるいは不正なスタックトレースが生成されるといった問題が発生していました。

このコミットは、この誤った条件判定を修正し、ビルドの安定性とスタックトレースの正確性を確保することを目的としています。

前提知識の解説

このコミットを理解するためには、以下の概念についての知識が不可欠です。

  1. スタック(Stack): プログラムの実行中に、関数呼び出し、ローカル変数の割り当て、レジスタの保存などに使用されるメモリ領域です。スタックは通常、メモリの高いアドレスから低いアドレスに向かって成長します(x86アーキテクチャの場合)。

  2. スタックポインタ(Stack Pointer, SP): 現在のスタックの最上位(または最下位、アーキテクチャによる)を指すレジスタです。新しいデータがスタックにプッシュされるとSPは減少し、ポップされると増加します。

  3. フレームポインタ(Frame Pointer, FP または Base Pointer, BP): 現在実行中の関数のスタックフレームの基底(または特定のオフセット)を指すレジスタです。FPは、関数内のローカル変数や引数にアクセスするための基準点として使用されます。多くのアーキテクチャでは、関数呼び出し時に以前のFPがスタックに保存され、新しいFPが設定されます。

  4. スタックフレーム(Stack Frame): 関数が呼び出されるたびにスタック上に作成される領域です。これには、関数の引数、リターンアドレス、ローカル変数、保存されたレジスタなどが含まれます。

  5. uintptr: Go言語におけるuintptr型は、ポインタを保持できる符号なし整数型です。そのサイズは、実行されているシステムのポインタサイズ(32ビットシステムでは4バイト、64ビットシステムでは8バイト)に依存します。sizeof(uintptr)は、このポインタのサイズをバイト単位で表します。

  6. トレースバック(Traceback)/スタックトレース(Stack Trace): プログラムがクラッシュしたり、特定のイベントが発生したりしたときに、現在実行中の関数の呼び出し履歴(コールスタック)を表示する機能です。デバッグやエラー解析に不可欠です。Goランタイムでは、runtime·gentracebackのような関数がこの処理を担当します。

  7. x86アーキテクチャの呼び出し規約: x86プロセッサにおける関数呼び出しの標準的なルールです。これには、引数の渡し方、レジスタの使用方法、スタックフレームの構築方法などが含まれます。一般的に、CALL命令はリターンアドレスをスタックにプッシュし、RET命令はスタックからリターンアドレスをポップして実行を再開します。

技術的詳細

このコミットの核心は、runtime·gentraceback関数内のスタックフレームの解析ロジックの修正にあります。この関数は、スタックを遡って各関数の呼び出し情報を収集し、スタックトレースを構築します。

変更前のコードでは、以下の条件を使用して、関数がまだ自身のスタックフレームを完全に設定していない状態を検出していました。

if(frame.fp == frame.sp) {
    // Function has not created a frame for itself yet.
    frame.varp = nil;
    frame.varlen = 0;
}

この条件は、関数が呼び出された直後で、fpspが同じアドレスを指していると仮定していました。しかし、x86アーキテクチャの標準的な呼び出し規約では、CALL命令が実行されると、呼び出し元の次の命令のアドレス(リターンアドレス)がスタックにプッシュされます。このプッシュ操作により、spはリターンアドレスの分だけ減少します。したがって、関数が自身のスタックフレームをまだ設定していない状態であっても、fpspは等しくなりません。具体的には、spfpよりもsizeof(uintptr)バイトだけ小さいアドレスを指すことになります(スタックが下方に成長する場合)。

この不一致を修正するため、コミットは条件を以下のように変更しました。

if(frame.fp == frame.sp + sizeof(uintptr)) {
    // Function has not created a frame for itself yet.
    frame.varp = nil;
    frame.varlen = 0;
}

この新しい条件は、frame.fpframe.spよりもsizeof(uintptr)バイトだけ大きいアドレスを指している場合に真となります。これは、spがリターンアドレスを指しており、fpがそのリターンアドレスの直前の位置(または、スタックフレームの基底として使用されるべき位置)を指しているという、x86アーキテクチャにおける関数呼び出し直後の正しいスタックの状態を反映しています。

この修正により、runtime·gentraceback関数は、関数がスタックフレームをまだ設定していない状態を正確に識別できるようになり、スタックトレースの生成がより堅牢になります。これにより、ビルド時の問題が解消され、ランタイムの安定性が向上します。

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

変更はsrc/pkg/runtime/traceback_x86.cファイルの一箇所のみです。

--- a/src/pkg/runtime/traceback_x86.c
+++ b/src/pkg/runtime/traceback_x86.c
@@ -112,7 +112,7 @@ runtime·gentraceback(uintptr pc0, uintptr sp0, uintptr lr0, G *gp, int32 skip,
 		}
 
 		// Derive location and size of local variables.
-		if(frame.fp == frame.sp) {
+		if(frame.fp == frame.sp + sizeof(uintptr)) {
 			// Function has not created a frame for itself yet.
 			frame.varp = nil;
 			frame.varlen = 0;

コアとなるコードの解説

変更された行は、runtime·gentraceback関数内でスタックフレームの情報を解析する部分にあります。

元のコード: if(frame.fp == frame.sp)

この条件は、フレームポインタ(frame.fp)とスタックポインタ(frame.sp)が同じアドレスを指しているかどうかをチェックしていました。これは、関数が呼び出された直後で、まだローカル変数のためのスタック領域を確保したり、フレームポインタを適切に設定したりしていない状態を検出するための試みでした。しかし、前述の通り、x86の呼び出し規約ではリターンアドレスがスタックにプッシュされるため、frame.fpframe.spは通常等しくありません。

修正後のコード: if(frame.fp == frame.sp + sizeof(uintptr))

この新しい条件は、frame.fpframe.spよりもsizeof(uintptr)バイトだけ大きいアドレスを指しているかどうかをチェックします。

  • frame.spは、現在のスタックの最上位、つまり直前にプッシュされたリターンアドレスを指しています。
  • sizeof(uintptr)は、リターンアドレスのサイズ(ポインタのサイズ)です。
  • したがって、frame.sp + sizeof(uintptr)は、リターンアドレスがプッシュされる前のスタックポインタの位置、つまり、関数が呼び出される直前のスタックの最上位を指します。
  • この位置は、多くの場合、呼び出された関数のフレームポインタが指すべき位置と一致します。

この修正により、runtime·gentracebackは、関数がまだ自身のスタックフレームを完全に設定していない、つまりリターンアドレスがスタックにプッシュされた直後の状態を正確に識別できるようになります。これにより、スタックトレースの生成ロジックがより堅牢になり、ビルド時の問題が解消されます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • x86アーキテクチャの呼び出し規約に関する一般的な情報(例: System V ABI for x86-64)
  • スタック、スタックポインタ、フレームポインタに関するコンピュータアーキテクチャの基本概念