[インデックス 18379] ファイルの概要
このコミットは、Goコンパイラの一部である cmd/8g (x86-64アーキテクチャ向けのアセンブラ) において、Prog 構造体の u.branch フィールドが nil である場合に発生するクラッシュを防ぐための修正です。特に、アセンブリコードをダンプする -S オプションを使用した際にクラッシュが発生していました。
コミット
commit 289d46399b794a15be12f57a8162fa514bd8f306
Author: Rob Pike <r@golang.org>
Date: Wed Jan 29 16:14:45 2014 -0800
cmd/8g: don't crash if Prog->u.branch is nil
The code is copied from cmd/6g.
Empirically, all branch targets are nil in this code so
something is still wrong, but at least this stops 8g -S
from crashing.
Update #7178
LGTM=dave, iant
R=iant, dave
CC=golang-codereviews
https://golang.org/cl/58400043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/289d46399b794a15be12f57a8162fa514bd8f306
元コミット内容
cmd/8g: Prog->u.branch が nil の場合にクラッシュしないようにする。
このコードは cmd/6g からコピーされたものです。
経験的に、このコードではすべての分岐ターゲットが nil であるため、何らかの問題がまだ残っていますが、少なくともこれにより 8g -S がクラッシュするのを防ぎます。
Issue #7178 を更新。
変更の背景
このコミットの背景には、Goコンパイラの cmd/8g ツールが特定の条件下でクラッシュするというバグが存在していました。具体的には、8g コマンドに -S フラグ(生成されたアセンブリコードを表示するためのフラグ)を付けて実行すると、プログラムが異常終了していました。
クラッシュの原因は、Prog 構造体(Goコンパイラが中間表現として使用する命令を表す構造体)の u.branch フィールドが nil であるにもかかわらず、そのフィールドが参照される可能性があったためです。u.branch は通常、分岐命令のターゲットとなる Prog へのポインタを保持しますが、何らかの理由で nil になっている場合に、nil ポインタデリファレンスが発生し、プログラムがクラッシュしていました。
コミットメッセージには「経験的に、このコードではすべての分岐ターゲットが nil であるため、何らかの問題がまだ残っています」とあり、これは u.branch が nil になる根本原因は解決されていないものの、少なくともクラッシュを防ぐための緊急的な修正であることを示唆しています。この修正は、同様の問題を抱えていた cmd/6g (x86-32アーキテクチャ向けのアセンブラ) のコードから移植されたものです。
前提知識の解説
- Goコンパイラ (
cmd/): Go言語のソースコードを機械語に変換するツール群です。cmd/ディレクトリ以下に、各アーキテクチャ向けのアセンブラやリンカなどが含まれています。cmd/8g: x86-64 (64ビットIntel/AMD) アーキテクチャ向けのGoアセンブラです。Goのソースコードをコンパイルする際に、最終的な機械語を生成する過程で利用されます。cmd/6g: x86-32 (32ビットIntel/AMD) アーキテクチャ向けのGoアセンブラです。
Prog構造体: Goコンパイラの内部で、アセンブリ命令や中間表現の命令を表すために使用されるデータ構造です。各Progオブジェクトは、オペコード、オペランド、そして次の命令へのポインタなど、命令に関する情報を保持します。u.branchフィールド:Prog構造体内のフィールドの一つで、主に分岐命令(JMP,CALLなど)のターゲットとなる別のProgオブジェクトへのポインタを保持します。uはユニオン(共用体)のようなもので、命令の種類によって異なるデータが格納されることを示唆しています。nil: Go言語におけるゼロ値の一つで、ポインタ、スライス、マップ、チャネル、関数、インターフェースなどの参照型が何も指していない状態を表します。C言語のNULLに相当します。nilポインタをデリファレンス(nilが指す先のメモリにアクセスしようとすること)すると、プログラムはランタイムエラー(パニック)を起こし、クラッシュします。8g -S:8gコマンドに-Sオプションを付けて実行すると、コンパイルされたGoプログラムの生成されたアセンブリコードが標準出力にダンプされます。これはデバッグやパフォーマンス分析に役立ちます。Dconv関数:src/cmd/8g/list.cに存在する関数で、Prog構造体の内容を文字列に変換(フォーマット)して表示するために使用されます。特に、アセンブリコードのダンプ (-Sオプション) の際に、Progオブジェクトの各フィールドを人間が読める形式で出力する役割を担っています。
技術的詳細
このコミットは、src/cmd/8g/list.c ファイル内の Dconv 関数における D_BRANCH ケースの処理を修正しています。Dconv 関数は、Prog 構造体の内容を整形して出力する際に呼び出されます。D_BRANCH は、現在の Prog オブジェクトが分岐命令を表している場合に処理されるケースです。
修正前のコードでは、D_BRANCH の場合、a->u.branch->loc という形で直接 u.branch フィールドが指す Prog オブジェクトの loc フィールドにアクセスしていました。ここで a は現在の Prog オブジェクトを指します。
case D_BRANCH:
snprint(str, sizeof(str), "%d", a->u.branch->loc);
break;
問題は、何らかの理由で a->u.branch が nil になっている場合、a->u.branch->loc にアクセスしようとすると nil ポインタデリファレンスが発生し、プログラムがクラッシュすることでした。これは特に 8g -S のようなアセンブリダンプのシナリオで顕在化しました。
修正は非常にシンプルで、a->u.branch が nil であるかどうかをチェックする条件分岐を追加しました。
case D_BRANCH:
if(a->u.branch == nil)
snprint(str, sizeof(str), "<nil>");
else
snprint(str, sizeof(str), "%d", a->u.branch->loc);
break;
この変更により、u.branch が nil の場合は、クラッシュする代わりに <nil> という文字列が出力されるようになります。これにより、nil ポインタデリファレンスによるクラッシュは回避されます。
コミットメッセージにある「経験的に、このコードではすべての分岐ターゲットが nil であるため、何らかの問題がまだ残っています」という記述は重要です。これは、u.branch が nil になること自体が予期せぬ状態であり、その根本原因(コンパイラの他の部分でのバグや、特定のコードパターンに対する不適切な Prog 生成)はまだ解決されていないことを示しています。このコミットは、根本原因を解決するのではなく、その結果として発生するクラッシュを回避するための防御的なプログラミング(nilチェック)を導入したものです。
コアとなるコードの変更箇所
--- a/src/cmd/8g/list.c
+++ b/src/cmd/8g/list.c
@@ -107,7 +107,10 @@ Dconv(Fmt *fp)
break;
case D_BRANCH:
- snprint(str, sizeof(str), "%d", a->u.branch->loc);
+ if(a->u.branch == nil)
+ snprint(str, sizeof(str), "<nil>");
+ else
+ snprint(str, sizeof(str), "%d", a->u.branch->loc);
break;
case D_EXTERN:
コアとなるコードの解説
変更は src/cmd/8g/list.c ファイル内の Dconv 関数にあります。この関数は、Goコンパイラの内部表現である Prog 構造体を人間が読める形式の文字列に変換する役割を担っています。
特に、case D_BRANCH: の部分が修正されました。これは、処理中の Prog オブジェクトが分岐命令(例: JMP, CALL)を表している場合に実行されるコードブロックです。
修正前は以下のようになっていました。
snprint(str, sizeof(str), "%d", a->u.branch->loc);
ここで a は現在の Prog オブジェクトへのポインタです。a->u.branch は分岐ターゲットの Prog オブジェクトへのポインタを保持しています。この行は、分岐ターゲットの Prog オブジェクトの loc フィールド(おそらく命令のアドレスやオフセットを示す)を整数として文字列 str にフォーマットしようとしていました。
修正後は以下のようになりました。
if(a->u.branch == nil)
snprint(str, sizeof(str), "<nil>");
else
snprint(str, sizeof(str), "%d", a->u.branch->loc);
この変更により、a->u.branch が nil であるかどうかのチェックが追加されました。
- もし
a->u.branchがnilであれば、つまり分岐ターゲットが設定されていないか、無効な状態である場合、snprint関数はstrに<nil>という文字列を書き込みます。これにより、nilポインタデリファレンスが回避され、クラッシュが防がれます。 - もし
a->u.branchがnilでなければ、以前と同様にa->u.branch->locの値を文字列にフォーマットします。
この修正は、nil ポインタデリファレンスという特定のランタイムエラーを直接的に解決するものであり、プログラムの堅牢性を向上させます。ただし、u.branch が nil になる根本的な原因については、このコミットでは触れられていません。
関連リンク
- Go CL 58400043: https://golang.org/cl/58400043
- Go Issue 7178: https://golang.org/issue/7178
参考にした情報源リンク
- Go言語のソースコード (特に
src/cmd/8g/list.cおよび関連するコンパイラコード) - Go言語のIssueトラッカー (Issue 7178)
- Go言語のコードレビューシステム (CL 58400043)
- Goコンパイラの内部構造に関する一般的な知識 (Prog構造体、アセンブラの動作など)
- C言語における
nilポインタ(NULLポインタ)の概念とデリファレンスによるクラッシュ