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

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

このコミットは、Goコンパイラ(cmd/gc)におけるインライン化された関数のアドレスが取られた変数に関するライブネス解析のバグを修正するものです。具体的には、インライン化時に変数の「アドレスが取られている」という情報が正しく伝播されず、結果として誤ったライブネス情報が生成される問題を解決します。

コミット

commit eb54079264896a8e7bd0ea79768ea4f46c47e30a
Author: Russ Cox <rsc@golang.org>
Date:   Mon Jun 2 21:26:32 2014 -0400

    cmd/gc: fix liveness for address-taken variables in inlined functions
    
    The 'address taken' bit in a function variable was not
    propagating into the inlined copies, causing incorrect
    liveness information.
    
    LGTM=dsymonds, bradfitz
    R=golang-codereviews, bradfitz
    CC=dsymonds, golang-codereviews, iant, khr, r
    https://golang.org/cl/96670046
---
 src/cmd/gc/inl.c |  1 +
 test/live.go     | 32 ++++++++++++++++++++++++++++++++\
 test/live2.go    | 39 +++++++++++++++++++++++++++++++++++++++
 3 files changed, 72 insertions(+)

diff --git a/src/cmd/gc/inl.c b/src/cmd/gc/inl.c
index 298a4c0d70..cf89b00902 100644
--- a/src/cmd/gc/inl.c
+++ b/src/cmd/gc/inl.c
@@ -802,6 +802,7 @@ inlvar(Node *var)
 	n->class = PAUTO;
 	n->used = 1;
 	n->curfn = curfn;   // the calling function, not the called one
+	n->addrtaken = var->addrtaken;
 
 	// esc pass wont run if we're inlining into a iface wrapper
 	// luckily, we can steal the results from the target func
diff --git a/test/live.go b/test/live.go
index 286fcc3064..b4cced47e3 100644
--- a/test/live.go
+++ b/test/live.go
@@ -4,6 +4,9 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+// liveness tests with inlining disabled.
+// see also live2.go.
+
 package main
 
 func f1() {
@@ -590,3 +593,32 @@ func f39c() (x [10]*int) {\n 	println() // ERROR "live at call to printnl: x"\n 	return\n }\n+\n+// issue 8142: lost 'addrtaken' bit on inlined variables.\n+// no inlining in this test, so just checking that non-inlined works.\n+\n+type T40 struct {\n+\tm map[int]int\n+}\n+\n+func newT40() *T40 {\n+\tret := T40{ // ERROR "live at call to makemap: &ret"\n+\t\tmake(map[int]int), \n+\t}\n+\treturn &ret\n+}\n+\n+func bad40() {\n+\tt := newT40()\n+\tprintln()\n+\t_ = t\n+}\n+\n+func good40() {\n+\tret := T40{ // ERROR "live at call to makemap: ret"\n+\t\tmake(map[int]int),\n+\t}\n+\tt := &ret\n+\tprintln() // ERROR "live at call to printnl: ret"\n+\t_ = t\n+}\ndiff --git a/test/live2.go b/test/live2.go
new file mode 100644
index 0000000000..1e32794026
--- /dev/null
+++ b/test/live2.go
@@ -0,0 +1,39 @@
+// errorcheck -0 -live
+\n+// Copyright 2014 The Go Authors.  All rights reserved.\n+// Use of this source code is governed by a BSD-style\n+// license that can be found in the LICENSE file.\n+\n+// liveness tests with inlining ENABLED\n+// see also live.go.\n+\n+package main\n+\n+// issue 8142: lost 'addrtaken' bit on inlined variables.\n+// no inlining in this test, so just checking that non-inlined works.\n+\n+type T40 struct {\n+\tm map[int]int\n+}\n+\n+func newT40() *T40 {\n+\tret := T40{ // ERROR "live at call to makemap: &ret"\n+\t\tmake(map[int]int),\n+\t}\n+\treturn &ret\n+}\n+\n+func bad40() {\n+\tt := newT40() // ERROR "live at call to makemap: ret"\n+\tprintln()     // ERROR "live at call to printnl: ret"\n+\t_ = t\n+}\n+\n+func good40() {\n+\tret := T40{ // ERROR "live at call to makemap: ret"\n+\t\tmake(map[int]int),\n+\t}\n+\tt := &ret\n+\tprintln() // ERROR "live at call to printnl: ret"\n+\t_ = t\n+}\n```

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

[https://github.com/golang/go/commit/eb54079264896a8e7bd0ea79768ea4f46c47e30a](https://github.com/golang/go/commit/eb54079264896a8e7bd0ea79768ea4f46c47e30a)

## 元コミット内容

cmd/gc: fix liveness for address-taken variables in inlined functions

The 'address taken' bit in a function variable was not propagating into the inlined copies, causing incorrect liveness information.

LGTM=dsymonds, bradfitz R=golang-codereviews, bradfitz CC=dsymonds, golang-codereviews, iant, khr, r https://golang.org/cl/96670046


## 変更の背景

このコミットは、Goコンパイラ(`cmd/gc`)における重要なバグを修正するために導入されました。問題は、関数がインライン化される際に、その関数内で定義された変数、特に「アドレスが取られた(address-taken)」変数の情報が正しく引き継がれないことにありました。

具体的には、変数のメモリアドレスがポインタとして使用される場合、その変数は「アドレスが取られた」とマークされます。この情報は、ガベージコレクタが変数のライフタイム(ライブネス)を正確に判断するために不可欠です。しかし、Goコンパイラが関数をインライン化する際、インライン化されたコード内の変数のコピーに対して、元の変数が持っていた「アドレスが取られた」というフラグ(`addrtaken`ビット)が伝播されませんでした。

この情報の欠落により、コンパイラのライブネス解析が誤った結果を生成しました。ライブネス解析は、あるプログラムポイントで変数が将来的に使用される可能性があるかどうかを判断するプロセスです。もし変数が「アドレスが取られている」にもかかわらず、その情報が失われると、ライブネス解析は誤ってその変数を「デッド(死んでいる)」と判断してしまう可能性がありました。結果として、ガベージコレクタがまだライブであるべき変数を誤って回収してしまい、プログラムのクラッシュや予期せぬ動作を引き起こす可能性がありました。

このバグは、特に複雑なデータ構造(例:マップを含む構造体)を扱う際に顕在化し、Goプログラムの安定性と正確性に影響を与えていました。

## 前提知識の解説

このコミットの理解を深めるためには、以下のコンパイラ最適化とガベージコレクションに関する基本的な概念を理解しておく必要があります。

*   **インライン化 (Inlining)**:
    インライン化は、コンパイラ最適化の一種です。関数呼び出しのオーバーヘッド(スタックフレームのセットアップ、引数のプッシュ、戻り値の処理など)を削減するために、呼び出し元のコードに呼び出される関数の本体を直接埋め込む手法です。これにより、実行時のパフォーマンスが向上する可能性がありますが、コードサイズが増加する可能性もあります。Goコンパイラは、特定の条件(関数のサイズ、呼び出し回数など)に基づいて自動的にインライン化を決定します。

*   **アドレスが取られた変数 (Address-taken variables)**:
    Go言語では、変数のメモリアドレスを`&`演算子を使って取得し、ポインタとして扱うことができます。このようにしてアドレスが取得された変数は「アドレスが取られた変数」と呼ばれます。これらの変数は、そのアドレスがポインタとしてどこかに保存されている可能性があるため、ガベージコレクタがそのライフタイムを追跡する上で特別な注意を払う必要があります。アドレスが取られた変数は、そのポインタが参照され続ける限り、ガベージコレクションの対象とはなりません。

*   **ライブネス解析 (Liveness Analysis)**:
    ライブネス解析は、コンパイラのデータフロー解析の一種であり、プログラムの特定のポイントにおいて、変数が「ライブ(生きている)」であるか「デッド(死んでいる)」であるかを判断します。変数がライブであるとは、その変数の値が将来的にプログラムの実行に影響を与える可能性がある(つまり、後続の命令でその値が読み取られる可能性がある)ことを意味します。逆に、デッドであるとは、その変数の値がそれ以降使用されないことを意味します。
    ライブネス解析の結果は、主に以下の目的で利用されます。
    *   **レジスタ割り当て**: ライブな変数をレジスタに割り当てることで、メモリアクセスを減らしパフォーマンスを向上させます。
    *   **ガベージコレクション**: ガベージコレクタは、デッドな変数が占めるメモリ領域を安全に解放することができます。ライブな変数は、プログラムの実行に必要なため、回収されません。

*   **Goコンパイラ (gc)**:
    `gc`は、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、構文解析、意味解析、中間コード生成、最適化、コード生成など、様々な段階が含まれます。このコミットで修正されたバグは、最適化段階、特にインライン化とそれに続くライブネス解析の連携に関するものでした。

## 技術的詳細

このコミットの技術的な核心は、Goコンパイラのインライン化処理における変数のプロパティ伝播の不備にありました。

Goコンパイラのソースコードにおいて、`src/cmd/gc/inl.c`ファイルは、関数のインライン化に関連するロジックを実装しています。このファイル内の`inlvar`関数は、インライン化される関数のローカル変数を処理する際に呼び出されます。`inlvar`関数は、元の関数内の変数(`var`)に対応する新しいノード(`n`)を、呼び出し元の関数(インライン化のターゲット)のコンテキストで作成します。

問題は、この新しいノード`n`が作成される際に、元の変数`var`が持っていた`addrtaken`(アドレスが取られているかどうかのフラグ)の情報が、新しいノード`n`にコピーされていなかったことです。`addrtaken`フラグは、変数のメモリアドレスがポインタとして使用されていることを示し、ガベージコレクタがその変数をライブであると判断するための重要なヒントとなります。

`addrtaken`情報が欠落した状態でインライン化された変数は、ライブネス解析において誤って「アドレスが取られていない」と判断される可能性がありました。これにより、たとえその変数のアドレスが他の場所で参照され続けていたとしても、ライブネス解析はそれをデッドな変数と誤認し、ガベージコレクタがそのメモリを早期に解放してしまう危険性がありました。これは、プログラムの実行中に不正なメモリ参照を引き起こし、クラッシュやデータ破損につながる可能性のある深刻なバグでした。

このコミットは、`inlvar`関数にたった1行のコードを追加することで、この問題を解決しています。

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

```diff
--- a/src/cmd/gc/inl.c
+++ b/src/cmd/gc/inl.c
@@ -802,6 +802,7 @@ inlvar(Node *var)
 	n->class = PAUTO;
 	n->used = 1;
 	n->curfn = curfn;   // the calling function, not the called one
+	n->addrtaken = var->addrtaken;
 
 	// esc pass wont run if we're inlining into a iface wrapper
 	// luckily, we can steal the results from the target func

コアとなるコードの解説

変更はsrc/cmd/gc/inl.cファイルのinlvar関数内で行われました。追加された行は以下の通りです。

n->addrtaken = var->addrtaken;

この1行の追加が、バグの修正において極めて重要です。

  • var: これは、インライン化される元の関数内の変数を表すノードです。このノードは、その変数が「アドレスが取られている」かどうかを示すaddrtakenフラグを持っています。
  • n: これは、インライン化処理中に、呼び出し元の関数内で新しく作成された、元の変数varに対応するノードです。

この行が追加される前は、naddrtakenフラグは初期値のままであり、varaddrtaken情報がnに引き継がれていませんでした。この修正により、inlvar関数が新しい変数ノードnを作成する際に、元の変数ノードvaraddrtakenフラグの値をnに明示的にコピーするようになりました。

これにより、インライン化された関数内の変数のコピーも、元の変数がアドレスが取られていた場合は、正しく「アドレスが取られている」とマークされるようになります。この正確なaddrtaken情報は、その後のコンパイラのライブネス解析に正しく伝達され、ガベージコレクタが変数のライブネスを正確に判断できるようになります。結果として、まだ参照されているライブな変数が誤って回収されることを防ぎ、Goプログラムのメモリ安全性と正確性が保証されます。

このコミットには、修正を検証するための新しいテストケースも追加されています。test/live.goにはインライン化を無効にした状態でのテストが追加され、test/live2.goという新しいファイルではインライン化を有効にした状態でのテストが追加されています。これにより、インライン化の有無にかかわらず、addrtakenビットが正しく扱われることが確認されます。

関連リンク

参考にした情報源リンク