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

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

このコミットは、Goコンパイラ(cmd/gc)におけるnilインターフェースおよびnilスライスの誤った使用に関するバグ修正です。具体的には、nil定数として扱われるインターフェースやスライスが、コンパイラによって不正なコード生成を引き起こす問題に対処しています。この修正により、nilインターフェースやnilスライスが使用された際に、適切にパニックを発生させるようなコードが生成されるようになります。

コミット

commit 05ac300830ab89c46a259b3bb9d57a4a5a080a15
Author: Russ Cox <rsc@golang.org>
Date:   Sat Sep 22 20:42:11 2012 -0400

    cmd/gc: fix use of nil interface, slice
    
    Fixes #3670.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/6542058

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

https://github.com/golang/go/commit/05ac300830ab89c46a259b3bb9d57a4a5a080a15

元コミット内容

このコミットは、Goコンパイラがnilインターフェースやnilスライスを誤って処理し、結果として不正なコードを生成するバグを修正します。この問題は、特にreflect.TypeOf(T(nil))のような操作や、[]byte(nil)[0]のようにnilスライスにアクセスしようとした場合に顕在化していました。コンパイラはこれらのnil値をアドレス可能(addressable)なものとして扱おうとし、load-effective-address命令(LEAQALEAL)を不適切に生成していました。

変更の背景

Go言語では、nilは特定の型のゼロ値を表します。インターフェース型やスライス型もnilになり得ます。しかし、コンパイラがこれらのnil値を処理する際に、その内部表現やメモリレイアウトに関する誤解釈が生じることがありました。特に、nilインターフェースやnilスライスに対してアドレスを取得しようとするような状況で、コンパイラがクラッシュしたり、予期せぬ動作を引き起こすコードを生成したりするバグが存在していました。

この問題は、Goの内部バグトラッカーで「Issue 3670」として報告されていました。このコミットは、そのIssue 3670を修正することを目的としています。nilインターフェースやnilスライスへのアクセスは、通常、実行時パニック(runtime panic)を引き起こすべき動作です。しかし、バグのあるコンパイラは、パニックを発生させる代わりに、不正なメモリ参照を行うコードを生成してしまう可能性がありました。

test/fixedbugs/bug444.goというテストファイルがこのバグを再現するために追加されており、reflect.TypeOf(T(nil))が以前はコンパイルエラーまたは誤った動作を引き起こしていたこと、そして[]byte(nil)[0]のようなnilスライスへのアクセスが誤ったコンパイルを招いていたことが示されています。

前提知識の解説

  • Goコンパイラ (cmd/gc): Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担います。5g, 6g, 8gはそれぞれ、Goがサポートしていた異なるアーキテクチャ(5gはARM、6gはx86-64、8gはx86-32)向けのコンパイラバックエンドを指します。
  • nil: Goにおけるゼロ値の一つで、ポインタ、インターフェース、スライス、マップ、チャネル、関数などの参照型が何も指していない状態を表します。
  • インターフェース (Interface): Goのインターフェースは、メソッドのシグネチャの集合を定義します。インターフェース型の変数は、そのインターフェースが定義するすべてのメソッドを実装する任意の型の値を保持できます。nilインターフェースは、基底となる値も型も持たない状態です。
  • スライス (Slice): Goのスライスは、配列の一部を参照する動的なデータ構造です。内部的には、ポインタ、長さ、容量の3つの要素で構成されます。nilスライスは、これらの要素がすべてゼロ値である状態です。
  • isconst(n, CTNIL): コンパイラ内部の関数で、与えられたノードnnil定数であるかどうかを判定します。CTNILnil定数を表すフラグです。
  • widthptr: ポインタのサイズ(バイト単位)を表す定数。通常、32ビットシステムでは4バイト、64ビットシステムでは8バイトです。
  • agen関数: コンパイラのコード生成フェーズで使用される関数の一つで、ノードnのアドレスを計算し、結果をresに格納します。
  • gins関数: アセンブリ命令を生成する関数。ALEAQ (Load Effective Address Quadword) や ALEAL (Load Effective Address Longword) は、メモリのアドレスをレジスタにロードする命令です。これらは通常、ポインタ演算や構造体フィールドへのアクセスなどで使用されます。
  • tempname: 一時変数を生成するコンパイラ内部の関数。
  • clearfat: 構造体やスライスなどの「fat」なデータ構造(ポインタだけでなく、長さや容量などの追加情報を持つもの)をゼロクリアする関数。
  • regalloc / regfree: レジスタの割り当てと解放を行う関数。
  • パニック (Panic): Goのランタイムエラーメカニズム。通常、プログラムの回復不可能なエラーが発生した場合に、現在のゴルーチンを停止させ、スタックをアンワインドします。nilスライスへのインデックスアクセスなど、不正な操作はパニックを引き起こします。

技術的詳細

このコミットの核心は、Goコンパイラのコード生成部分、特にagen関数におけるnilインターフェースおよびnilスライスの特殊なハンドリングにあります。

以前のコンパイラでは、nilインターフェースやnilスライスがagen関数に渡された際、それらがアドレス可能であるかのように扱われ、ALEAQALEALといったアドレスをロードする命令が生成されていました。しかし、nilインターフェースやnilスライスは、有効なメモリ領域を指していないため、そのアドレスを取得しようとすると問題が発生します。特に、これらの型はwidthptr(ポインタのサイズ)よりも大きいサイズを持つことがあり、その場合、単なるポインタとして扱われると情報が失われたり、不正なメモリ参照を引き起こしたりする可能性がありました。

この修正では、agen関数に以下のロジックが追加されました。

  1. isconst(n, CTNIL) && n->type->width > widthptr のチェック:

    • isconst(n, CTNIL): 現在処理しているノードnnil定数であるかを確認します。
    • n->type->width > widthptr: そのnil定数の型が、ポインタのサイズ(widthptr)よりも大きいサイズを持つかを確認します。これは、インターフェース(型情報と値ポインタを持つ)やスライス(ポインタ、長さ、容量を持つ)のような「fat」な型がnilである場合に該当します。
    • この条件が真の場合、それはnilインターフェースまたはnilスライスの使用を意味します。
  2. 一時変数の生成とアドレスの取得:

    • tempname(&n1, n->type): nilインターフェース/スライスと同じ型を持つ一時変数n1を生成します。
    • clearfat(&n1): その一時変数をゼロクリアします。これにより、nilの状態が表現されます。
    • regalloc(&n2, types[tptr], res): ポインタ型の一時レジスタn2を割り当てます。
    • gins(ALEAQ, &n1, &n2) (または ALEAL for 8g): 生成した一時変数n1のアドレスをレジスタn2にロードするアセンブリ命令を生成します。
    • gmove(&n2, res): n2の内容を結果ノードresに移動します。
    • regfree(&n2): 一時レジスタn2を解放します。
    • goto ret; (または return; for 8g): 処理を終了します。

この変更の意図は、「nilインターフェースやnilスライスが使用された場合、生成されるコードは単にパニックを引き起こすだけでよい」というものです。コンパイラは、これらの不正な使用に対して効率的なコードを生成する必要はなく、むしろ実行時に確実にパニックを発生させるようにすべきです。一時変数を作成し、そのアドレスを取得するこの方法は、実際にnil値がメモリ上に存在するかのように見せかけ、その後の不正なアクセスがランタイムパニックを引き起こすことを保証します。

また、gsubr.c内のgins関数にも変更が加えられています。ALEAQ(またはALEAL)命令を生成する際に、ソースオペランドfnil定数である場合、fatalエラーを発生させるチェックが追加されました。これは、コンパイラがnil定数に対して直接LEAQ命令を生成しようとするような、より根本的な誤りを防ぐための防御的なチェックです。

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

主に以下のファイルが変更されています。

  • src/cmd/5g/cgen.c
  • src/cmd/6g/cgen.c
  • src/cmd/8g/cgen.c
    • これらのファイルは、それぞれARM、x86-64、x86-32アーキテクチャ向けのGoコンパイラのコード生成部分(agen関数)です。agen関数内に、nilインターフェース/スライスの特殊なハンドリングロジックが追加されています。
  • src/cmd/6g/gsubr.c
  • src/cmd/8g/gsubr.c
    • これらのファイルは、それぞれx86-64、x86-32アーキテクチャ向けのGoコンパイラの共通サブルーチンです。gins関数内に、ALEAQ/ALEAL命令がnil定数に対して生成されようとした場合のfatalエラーチェックが追加されています。
  • test/fixedbugs/bug444.go
    • このファイルは、修正されたバグを再現し、修正が正しく機能することを確認するためのテストケースです。

コアとなるコードの解説

src/cmd/{5,6,8}g/cgen.c の変更点

 // ... (既存のコード) ...

 if(isconst(n, CTNIL) && n->type->width > widthptr) {
  // Use of a nil interface or nil slice.
  // Create a temporary we can take the address of and read.
  // The generated code is just going to panic, so it need not
  // be terribly efficient. See issue 3670.
  tempname(&n1, n->type);
  clearfat(&n1);
  regalloc(&n2, types[tptr], res);
  gins(ALEAQ, &n1, &n2); // 8gではALEAL
  gmove(&n2, res);
  regfree(&n2);
  goto ret; // 8gではreturn;
 }

 // ... (既存のコード) ...

このコードブロックは、agen関数内で、ノードnnil定数であり、かつその型がポインタサイズ(widthptr)よりも大きい場合に実行されます。これは、nilインターフェースやnilスライスを処理していることを意味します。

  1. tempname(&n1, n->type);: nと同じ型を持つ一時変数n1を宣言します。
  2. clearfat(&n1);: n1のメモリ領域をゼロで埋めます。これにより、nilインターフェースやnilスライスの内部表現(例えば、インターフェースの型情報や値ポインタ、スライスのポインタ、長さ、容量など)がすべてゼロに設定されます。
  3. regalloc(&n2, types[tptr], res);: ポインタを保持するための一時レジスタn2を割り当てます。
  4. gins(ALEAQ, &n1, &n2); (または ALEAL for 8g): n1のアドレスをn2にロードするアセンブリ命令を生成します。これにより、nilインターフェース/スライスがメモリ上のどこかに存在するかのように見せかけます。
  5. gmove(&n2, res);: n2の内容(n1のアドレス)を結果ノードresに移動します。
  6. regfree(&n2);: n2を解放します。
  7. goto ret; (または return; for 8g): agen関数の処理を終了します。

このロジックにより、nilインターフェースやnilスライスへのアクセスは、有効なアドレスを指さない一時変数へのアクセスとしてコンパイルされ、結果として実行時にパニックを引き起こすことが保証されます。

src/cmd/{6,8}g/gsubr.c の変更点

 // ... (既存のコード) ...

 case ALEAQ: // 8gではALEAL
  if(f != N && isconst(f, CTNIL)) {
   fatal("gins LEAQ nil %T", f->type);
  }
  break;

 // ... (既存のコード) ...

この変更は、gins関数内でALEAQ(またはALEAL)命令を生成する際の防御的なチェックです。

  • if(f != N && isconst(f, CTNIL)): 命令のソースオペランドfnil定数であるかどうかをチェックします。
  • fatal("gins LEAQ nil %T", f->type);: もしfnil定数であれば、コンパイラは致命的なエラーを発生させます。これは、nil定数に対して直接アドレスをロードしようとするのは、コンパイラの内部ロジックに誤りがあることを示唆するためです。このチェックは、agen関数での修正とは異なる、より低レベルでの誤ったコード生成を防ぐためのものです。

test/fixedbugs/bug444.go の変更点

 // ... (既存のコード) ...

 // The no-op conversion here used to confuse the compiler
 // into doing a load-effective-address of nil.
 // See issue 3670.

 package main

 import "reflect"

 type T interface {}

 var x bool

 func main() {
        reflect.TypeOf(nil)
        reflect.TypeOf(T(nil)) // used to miscompile
        shouldPanic()
 }

 func f() byte {
        return []byte(nil)[0] // used to miscompile
 }

 func shouldPanic() {
        defer func() {
                if recover() == nil {
                        panic("not panicking")
                }
        }()
        f()
 }

このテストファイルは、以下の2つの主要なケースをテストしています。

  1. reflect.TypeOf(T(nil)): nilインターフェースの型情報を取得しようとするケース。以前はこれが誤ってコンパイルされていました。
  2. []byte(nil)[0]: nilバイトスライスにインデックス0でアクセスしようとするケース。これは実行時にパニックを引き起こすべきですが、以前は誤ってコンパイルされていました。

shouldPanic関数は、f()がパニックを引き起こすことを検証するためのヘルパー関数です。f()がパニックしない場合、shouldPanic自体がパニックします。これにより、[]byte(nil)[0]が正しくパニックを引き起こすようになったことを確認できます。

関連リンク

参考にした情報源リンク

  • コミットハッシュ: 05ac300830ab89c46a259b3bb9d57a4a5a080a15
  • Goのコードレビューシステム (Gerrit) の変更リスト: https://golang.org/cl/6542058 (現在はGitHubに移行しているため、直接アクセスできない可能性がありますが、コミットメッセージに記載されています)
  • Go言語のソースコード (GitHub): https://github.com/golang/go
  • Go言語のIssue Tracker (GitHub): https://github.com/golang/go/issues (Issue 3670は古いIssueであり、直接リンクを見つけるのは難しい場合がありますが、このコミットが修正した問題の性質を理解する上で重要です。)