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

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

このコミットは、Goコンパイラ(cmd/gc)におけるゼロ除算エラーの修正に関するものです。具体的には、配列やスライスの型チェックを行う際に発生する可能性のある、型の幅(type->width)がゼロである場合の除算エラーを解消しています。この修正は、Go言語のコンパイラの堅牢性を高め、特定の状況下でのコンパイル時パニックを防ぐことを目的としています。

コミット

commit 51266761fdbe1b22fc354d7536123492a51769cf
Author: Russ Cox <rsc@golang.org>
Date:   Mon Sep 16 14:22:37 2013 -0400

    cmd/gc: fix divide by zero error in compiler
    
    Fixes #6399.
    
    R=ken2
    CC=golang-dev
    https://golang.org/cl/13253055

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

https://github.com/golang/go/commit/51266761fdbe1b22fc354d7536123492a51769cf

元コミット内容

Goコンパイラにおけるゼロ除算エラーを修正します。 Issue #6399を修正します。

変更の背景

このコミットは、Goコンパイラが特定の状況下でゼロ除算エラーを引き起こし、コンパイルが失敗するバグ(Issue #6399)に対応するために行われました。このバグは、Goの型システムにおいて、型のサイズ(幅)がゼロである場合に、その幅を用いた計算(特に配列のインデックス計算やメモリ割り当てに関連する部分)でゼロ除算が発生するというものでした。

具体的には、Goのコンパイラは、配列やスライスのような複合型の要素のサイズを計算する際に、要素の型の「幅」(type->width)を使用します。例えば、struct{}のような空の構造体や、要素を持たないインターフェース型など、一部の型はメモリ上でのサイズがゼロとして扱われます。このようなゼロ幅の型が配列の要素として使用された場合、コンパイラが内部的に行うサイズ計算でゼロ除算が発生し、コンパイル時パニックを引き起こす可能性がありました。

この問題は、特にインターフェース型を扱う際に顕在化しました。Goのインターフェースは、メソッドセットを定義する型であり、具体的な実装は実行時に決定されます。interface{}(空インターフェース)のようにメソッドを持たないインターフェースは、そのインスタンスが保持する値の型情報と値自体へのポインタの2つのワードで構成されますが、その「要素」としての概念的な幅はゼロとみなされることがあります。このゼロ幅の型が配列やスライスの要素として扱われる際に、コンパイラの内部ロジックでゼロ除算が発生し、コンパイルエラーとなることが報告されました。

この修正は、コンパイラが型の幅を考慮して計算を行う際に、ゼロ除算が発生しないようにするための防御的なチェックを追加することで、この問題を解決しています。

前提知識の解説

Goコンパイラ (cmd/gc)

Go言語の公式コンパイラは、gc(Go Compiler)と呼ばれ、Goツールチェーンの一部として提供されています。cmd/gcは、Goのソースコードを機械語に変換する主要なコンポーネントです。コンパイルプロセスは、字句解析、構文解析、型チェック、中間表現(IR)の生成、最適化、コード生成など、複数のフェーズに分かれています。

walk.c

src/cmd/gc/walk.cは、Goコンパイラのバックエンドの一部であり、抽象構文木(AST)を走査("walk")し、型チェック、定数畳み込み、一部の最適化、そして最終的なコード生成のための準備を行う役割を担っています。このファイルには、Goの様々な式やステートメントのセマンティクスを処理するためのロジックが含まれています。特に、配列やスライスの操作、型変換、関数呼び出しなど、Go言語の多くの機能がこのフェーズで処理されます。

型の幅 (type->width)

Goコンパイラ内部では、各データ型には「幅」(width)という属性が関連付けられています。これは、その型のインスタンスがメモリ上で占めるバイト数を表します。例えば、int32型は4バイト、int64型は8バイトの幅を持ちます。構造体や配列のような複合型の場合、その幅はメンバーの幅の合計や、アライメントの要件に基づいて計算されます。

しかし、Goには幅がゼロとして扱われる特殊な型が存在します。

  • 空の構造体 (struct{}): メモリを占有しないため、幅は0です。これは、セマフォやイベント通知など、値自体ではなくその存在が意味を持つ場合に利用されます。
  • 空のインターフェース (interface{}): インターフェース型自体は、内部的に値の型情報と値へのポインタを保持しますが、その「要素」としての概念的な幅はゼロとみなされることがあります。特に、インターフェースの配列やスライスを扱う際に、要素の型がゼロ幅であるとコンパイラが判断する場合があります。

ゼロ除算エラー

プログラミングにおいて、ゼロ除算(division by zero)は、数値をゼロで割ろうとすると発生するエラーです。これは数学的に未定義の操作であり、多くのプログラミング言語やCPUアーキテクチャでは、ゼロ除算が発生するとプログラムがクラッシュしたり、例外がスローされたりします。コンパイラのようなシステムプログラムでは、このようなエラーはコンパイルの失敗や予期せぬ動作につながるため、厳密に回避する必要があります。

Issue #6399

GoのIssue #6399は、「make([]Foo, N) where Foo is an interface type causes a compiler panic」というタイトルで報告されたバグです。このバグは、インターフェース型のスライスを作成する際に、コンパイラがパニックを起こすというものでした。具体的には、make([]Foo, 20)のようにインターフェース型のスライスを初期化しようとすると、コンパイラ内部でゼロ除算エラーが発生し、コンパイルが中断するという問題でした。これは、インターフェース型の「幅」がゼロとして扱われることと、その幅を用いた計算が原因で発生していました。

技術的詳細

このコミットの技術的詳細は、Goコンパイラのwalk.cファイル内のwalkexpr関数における型幅の計算ロジックの修正に集約されます。

元のコードでは、配列の型チェックを行う際に、以下の条件式がありました。

&& mpgetfix(r->val.u.xval) < (1ULL<<16) / t->type->width)

この式は、配列のサイズ(r->val.u.xval)が、特定の定数((1ULL<<16))を要素の型の幅(t->type->width)で割った値よりも小さいかどうかをチェックしています。このチェックは、配列のサイズが大きすぎないか、または特定の内部的な制限を超えていないかを確認するために行われます。

問題は、t->type->widthがゼロになる可能性がある点でした。前述の通り、struct{}のようなゼロ幅の型や、特定のインターフェース型が配列の要素として使用された場合、t->type->widthがゼロになります。この状態で除算を行うと、ゼロ除算エラーが発生し、コンパイラがパニックに陥ります。

修正後のコードは、このゼロ除算を回避するために、除算を行う前にt->type->widthがゼロでないことを確認する条件を追加しています。

&& (t->type->width == 0 || mpgetfix(r->val.u.xval) < (1ULL<<16) / t->type->width))

この変更により、t->type->widthがゼロの場合、t->type->width == 0という条件が真となり、論理OR演算子(||)のショートサーキット評価によって、ゼロ除算を含む右側の式は評価されなくなります。これにより、ゼロ除算エラーが回避され、コンパイラはパニックを起こすことなく処理を続行できるようになります。

この修正は、Goコンパイラの堅牢性を高める上で非常に重要です。コンパイラは、ユーザーが記述したコードの正当性を検証し、実行可能なバイナリを生成する役割を担っています。コンパイラ自体が特定の有効なGoコード(この場合はインターフェース型のスライス作成)によってパニックを起こすことは、開発体験を著しく損ない、信頼性を低下させます。この修正は、このようなエッジケースを適切に処理することで、コンパイラの安定性を向上させています。

また、test/fixedbugs/issue6399.goという新しいテストファイルが追加されています。このテストは、問題が修正されたことを検証するために、インターフェース型のスライスを作成し、その要素に値を代入するコードを含んでいます。これにより、将来的に同様の回帰バグが発生しないように保証されます。

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

src/cmd/gc/walk.c

--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -1295,7 +1295,7 @@ walkexpr(Node **np, NodeList **init)\
 		t = n->type;\
 		if(n->esc == EscNone\
 			&& smallintconst(l) && smallintconst(r)\
-			&& mpgetfix(r->val.u.xval) < (1ULL<<16) / t->type->width) {\
+			&& (t->type->width == 0 || mpgetfix(r->val.u.xval) < (1ULL<<16) / t->type->width)) {\
 			// var arr [r]T\
 			// n = arr[:l]\
 			t = aindex(r, t->type); // [r]T

test/fixedbugs/issue6399.go

--- /dev/null
+++ b/test/fixedbugs/issue6399.go
@@ -0,0 +1,27 @@
+// compile
+
+package main
+
+type Foo interface {
+	Print()
+}
+
+type Bar struct{}
+
+func (b Bar) Print() {}
+
+func main() {
+	b := make([]Bar, 20)
+	f := make([]Foo, 20)
+	for i := range f {
+		f[i] = b[i]
+	}
+	T(f)
+	_ = make([]struct{}, 1)
+}
+
+func T(f []Foo) {
+	for i := range f {
+		f[i].Print()
+	}
+}

コアとなるコードの解説

src/cmd/gc/walk.c の変更

変更はwalkexpr関数内の一行にあります。この関数は、Goの式を走査し、型チェックや最適化を行います。

元のコード: && mpgetfix(r->val.u.xval) < (1ULL<<16) / t->type->width)

修正後のコード: && (t->type->width == 0 || mpgetfix(r->val.u.xval) < (1ULL<<16) / t->type->width))

この変更の目的は、t->type->widthがゼロである場合に発生するゼロ除算エラーを回避することです。

  • t->type->width == 0: この条件が追加されました。もし型の幅がゼロであれば、論理OR演算子(||)のショートサーキット評価により、右側の式(ゼロ除算を含む部分)は評価されません。
  • mpgetfix(r->val.u.xval) < (1ULL<<16) / t->type->width: これは元の条件式です。r->val.u.xvalは配列のサイズ(要素数)を表し、(1ULL<<16)は特定の定数(おそらく内部的なバッファサイズや制限に関連する値)です。この式は、配列の合計サイズが特定の閾値を超えていないかをチェックしています。

この修正により、struct{}や特定のインターフェース型のように、メモリ上でのサイズがゼロとして扱われる型が配列の要素として使用された場合でも、コンパイラがゼロ除算でパニックを起こすことなく、正しく処理を続行できるようになります。

test/fixedbugs/issue6399.go の追加

このファイルは、Issue #6399で報告されたバグを再現し、修正が正しく適用されたことを検証するためのテストケースです。

package main

type Foo interface {
	Print()
}

type Bar struct{}

func (b Bar) Print() {}

func main() {
	b := make([]Bar, 20)
	f := make([]Foo, 20) // ここで問題が発生していた
	for i := range f {
		f[i] = b[i]
	}
	T(f)
	_ = make([]struct{}, 1) // ゼロ幅のstruct{}のスライスもテスト
}

func T(f []Foo) {
	for i := range f {
		f[i].Print()
	}
}
  • type Foo interface { Print() }: Print()メソッドを持つインターフェースFooを定義します。
  • type Bar struct{}func (b Bar) Print() {}: Fooインターフェースを実装する空の構造体Barを定義します。
  • f := make([]Foo, 20): この行が、元のバグを引き起こしていた主要な原因です。インターフェース型のスライスを作成しようとすると、コンパイラがFoo型の幅を計算する際にゼロ除算エラーが発生していました。
  • _ = make([]struct{}, 1): これは、ゼロ幅のstruct{}のスライスを作成するケースもテストしており、同様のゼロ除算問題が発生しないことを確認しています。

このテストファイルは、コンパイルが成功すること(// compileディレクティブ)を期待しており、これにより修正が正しく機能していることが確認されます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Goコンパイラのソースコード
  • Go Issue Tracker
  • Go言語に関する技術ブログやフォーラム(一般的なGoの型システム、コンパイラの仕組みに関する情報)
  • ゼロ除算に関する一般的なプログラミングの知識
  • Goのインターフェースの内部表現に関する情報
  • Goのmake関数の動作に関する情報
  • Goのstruct{}の特性に関する情報
  • Goのコンパイラ開発に関する議論やメーリングリストのアーカイブ