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

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

このコミットは、Goコンパイラのcmd/gcパッケージ内のwidstruct関数におけるnilポインタのデリファレンスを防ぐための修正です。具体的には、型情報がnilである場合に発生する可能性のあるコンパイラの停止(Unix環境ではセグメンテーション違反にはならないが、コンパイルが停止する)を回避し、より正確なエラー報告を可能にします。

コミット

commit 51e8fe5b1b6ae86976fb8e6d6333f14299b36b17
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date:   Sun Oct 7 14:11:59 2012 +0800

    cmd/gc: don't dereference a nil Type pointer in widstruct
    
    The nil dereference in the next few lines doesn't seem
    to cause a segmentation fault on Unix, but does seem
    to halt the Go compiler.
    
    The following is a test case:
    
    >>>
    package main
    
    func mine(int b) int {
            return b + 2
    }
    
    func main() {
            mine()
    
            c = mine()
    }
    <<<
    
    Without this change only the following is caught:
    
    typecheck.go:3: undefined: b
    typecheck.go:4: undefined: b
    
    with it, we catch all the errors:
    
    typecheck.go:3: undefined: b
    typecheck.go:4: undefined: b
    typecheck.go:10: undefined: c
    typecheck.go:10: cannot assign to c .
    
    R=rsc, minux.ma
    CC=golang-dev
    https://golang.org/cl/6542060

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

https://github.com/golang/go/commit/51e8fe5b1b6ae86976fb8e6d6333f14299b36b17

元コミット内容

cmd/gc: don't dereference a nil Type pointer in widstruct

The nil dereference in the next few lines doesn't seem
to cause a segmentation fault on Unix, but does seem
to halt the Go compiler.

The following is a test case:

>>>
package main

func mine(int b) int {
        return b + 2
}

func main() {
        mine()

        c = mine()
}
<<<

Without this change only the following is caught:

typecheck.go:3: undefined: b
typecheck.go:4: undefined: b

with it, we catch all the errors:

typecheck.go:3: undefined: b
typecheck.go:4: undefined: b
typecheck.go:10: undefined: c
typecheck.go:10: cannot assign to c .

R=rsc, minux.ma
CC=golang-dev
https://golang.org/cl/6542060

変更の背景

この変更の背景には、Goコンパイラが特定の不正なコードパターンに遭遇した際に、nilポインタのデリファレンスによってコンパイル処理が途中で停止してしまうという問題がありました。具体的には、cmd/gcパッケージ内のwidstruct関数において、構造体のフィールドの型情報(f->type)がnilであるにもかかわらず、そのポインタがデリファレンスされようとしていました。

コミットメッセージに示されているテストケースのように、関数定義の引数リストで型が未定義である場合(例: func mine(int b) intb)、コンパイラはbの型を特定できず、内部的にその型をnilとして扱う可能性がありました。このnilの型ポインタがwidstruct関数内で処理される際に、f->type->alignのようにnilポインタのメンバにアクセスしようとすると、デリファレンスが発生します。

Unix環境では、このようなnilデリファレンスが必ずしもセグメンテーション違反(プログラムの異常終了)を引き起こすとは限りませんでしたが、Goコンパイラの処理が停止し、それ以降のコンパイルエラーが報告されないという問題がありました。これは、開発者がコードのすべてのエラーを一度に把握できないため、デバッグ効率を著しく低下させる要因となっていました。

このコミットは、このようなコンパイラの停止を防ぎ、不正なコードに対するすべてのエラーを正確に報告できるようにするために導入されました。

前提知識の解説

Goコンパイラ (cmd/gc)

Go言語の公式コンパイラは、主にcmd/gcというディレクトリにそのソースコードが格納されています。gcは「Go Compiler」の略であり、Go言語のソースコードを機械語に変換する役割を担っています。コンパイルプロセスは、字句解析、構文解析、型チェック、中間コード生成、最適化、コード生成など、複数のフェーズに分かれています。

widstruct関数

widstruct関数は、Goコンパイラのsrc/cmd/gc/align.cファイルに存在する関数です。この関数は、構造体(struct)のメモリレイアウトを計算し、各フィールドのアライメント(メモリ上での配置規則)とオフセット(構造体の先頭からの相対位置)を決定する役割を担っています。Go言語では、メモリ効率とパフォーマンスのために、構造体のフィールドが特定のバイト境界に配置されるようにアライメントが行われます。widstructは、このアライメント要件を満たすために、各フィールドの型情報に基づいてそのサイズとアライメントを計算します。

Goの型システムとTypeポインタ

Go言語は静的型付け言語であり、すべての変数や式にはコンパイル時に型が関連付けられます。Goコンパイラの内部では、これらの型情報はType構造体(またはそれに相当するデータ構造)として表現されます。Typeポインタは、この型情報への参照を保持します。

nilポインタ

Goにおけるnilは、ポインタ、スライス、マップ、チャネル、関数、インターフェースなどのゼロ値を表します。ポインタの場合、nilは「何も指していない」状態を示します。nilポインタをデリファレンスしようとすると、通常はランタイムパニック(Goプログラムの異常終了)が発生します。しかし、コンパイラのような低レベルのC言語で書かれた部分では、nilポインタのデリファレンスが必ずしも即座にセグメンテーション違反を引き起こすとは限らず、未定義の動作や、今回のケースのようにコンパイラの停止を引き起こすことがあります。

T型(コンパイラ内部の特殊な型)

Goコンパイラの内部では、Tというシンボルが特殊な型を表すために使用されることがあります。これは、型が不明、無効、または存在しないことを示すプレースホルダーのような役割を果たすことがあります。今回の文脈では、f->type == Tというチェックは、フィールドの型が有効でない、または未定義の状態であることを検出するために使用されています。

技術的詳細

このコミットの技術的な核心は、src/cmd/gc/align.cファイル内のwidstruct関数に、nilポインタのデリファレンスを防ぐためのガード句が追加された点です。

元のコードでは、widstruct関数内で構造体の各フィールドをイテレートし、その型情報に基づいて処理を行っていました。

for(f=t->type; f!=T; f=f->down) {
    if(f->etype != TFIELD)
        fatal("widstruct: not TFIELD: %lT", f);
    dowidth(f->type); // ここで f->type が nil の場合に問題が発生する可能性
    if(f->type->align > maxalign) // ここでも f->type が nil の場合に問題が発生する可能性
        maxalign = f->type->align;
    ...
}

ここで、f->typeがnilであるにもかかわらず、dowidth(f->type)f->type->alignのようにそのメンバにアクセスしようとすると、nilポインタのデリファレンスが発生します。

このコミットでは、この問題に対処するために以下の行が追加されました。

if(f->type == T)
    break;

この変更により、ループ内で現在のフィールドfの型(f->type)がT(コンパイラ内部で「無効な型」や「未定義の型」を示す特殊な値)と等しい場合、それ以上の処理を行わずにループを中断するようになりました。これにより、f->typeが実質的にnilであるか、または無効な状態である場合に、その後のdowidth(f->type)f->type->alignといったデリファレンス操作が実行されるのを防ぎます。

結果として、コンパイラは不正な型情報に遭遇しても停止することなく、後続のコードの型チェックを続行し、より多くのエラーを報告できるようになります。コミットメッセージのテストケースで示されているように、この修正により、undefined: ccannot assign to cといったエラーも検出されるようになりました。これは、コンパイラが途中で停止しなくなったため、より完全なエラー報告が可能になったことを意味します。

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

--- a/src/cmd/gc/align.c
+++ b/src/cmd/gc/align.c
@@ -54,6 +54,8 @@ widstruct(Type *errtype, Type *t, vlong o, int flag)
 	for(f=t->type; f!=T; f=f->down) {
 	\tif(f->etype != TFIELD)
 	\t\tfatal(\"widstruct: not TFIELD: %lT\", f);\n+\t\tif(f->type == T)\n+\t\t\tbreak;\n \t\tdowidth(f->type);\n \t\tif(f->type->align > maxalign)\n \t\t\tmaxalign = f->type->align;

コアとなるコードの解説

変更はsrc/cmd/gc/align.cファイルのwidstruct関数内で行われています。

追加された2行は以下の通りです。

if(f->type == T)
    break;
  • fは、現在処理している構造体のフィールドを表すポインタです。
  • f->typeは、そのフィールドの型情報へのポインタです。
  • Tは、Goコンパイラ内部で「未定義」または「無効」な型を示すために使用される特殊な定数または値です。

このif文は、ループの各イテレーションで、現在のフィールドfの型ポインタf->typeT(つまり、型が未定義または無効な状態)であるかどうかをチェックします。もしf->typeTと等しい場合、それは型情報が不正であることを意味するため、break文が実行され、現在のforループが直ちに終了します。

このガード句が追加されることで、f->typeが不正な状態(実質的にnilポインタ)であるにもかかわらず、その後のdowidth(f->type)f->type->alignといった操作でnilポインタのデリファレンスが発生するのを未然に防ぐことができます。これにより、コンパイラが不正な型情報に遭遇してもクラッシュしたり停止したりすることなく、残りのコンパイルプロセスを続行し、より多くのエラーを報告できるようになります。

関連リンク

参考にした情報源リンク