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

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

このコミットは、Go言語のcmd/cgoツールにおけるバグ修正に関するものです。具体的には、Clangコンパイラを使用する際に、ポインタの配列のサイズが誤って計算される問題を修正しています。この問題は、Clangがポインタ型の「サイズ」フィールドをDWARF情報に記録しないことに起因し、cgoが配列のサイズを計算する際に、ネストされたポインタ型のサイズが正しく反映されないために発生していました。

コミット

commit 6be1cb8c7a8771310e0cc36c3d8fa783d48d0cf9
Author: Russ Cox <rsc@golang.org>
Date:   Thu Nov 7 15:24:51 2013 -0500

    cmd/cgo: fix handling of array of pointers when using clang
    
    Clang does not record the "size" field for pointer types,
    so we must insert the size ourselves. We were already
    doing this, but only for the case of pointer types.
    For an array of pointer types, the setting of the size for
    the nested pointer type was happening after the computation
    of the size of the array type, meaning that the array type
    was always computed as 0 bytes. Delay the size computation.
    
    This bug happens on all Clang systems, not just FreeBSD.
    Our test checked that cgo wrote something, not that it was correct.
    FreeBSD's default clang rejects array[0] as a C struct field,
    so it noticed the incorrect sizes. But the sizes were incorrect
    everywhere.
    
    Update testcdefs to check the output has the right semantics.
    
    Fixes #6292.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/22840043

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

https://github.com/golang/go/commit/6be1cb8c7a8771310e0cc36c3d8fa783d48d0cf9

元コミット内容

cmd/cgo: Clang使用時のポインタの配列のハンドリングを修正

Clangはポインタ型の「サイズ」フィールドを記録しないため、我々自身でサイズを挿入する必要がある。これは既にポインタ型の場合には行っていたが、ポインタの配列の場合、ネストされたポインタ型のサイズ設定が配列型のサイズ計算後に行われていたため、配列型が常に0バイトとして計算されていた。サイズ計算を遅延させる。

このバグはFreeBSDだけでなく、全てのClangシステムで発生する。我々のテストはcgoが何かを書き出すことをチェックしていただけで、それが正しいことをチェックしていなかった。FreeBSDのデフォルトのclangはC構造体フィールドとしてarray[0]を拒否するため、不正なサイズに気づいた。しかし、サイズはどこでも不正だった。

testcdefsを更新し、出力が正しいセマンティクスを持つことをチェックするようにした。

Fixes #6292.

変更の背景

Go言語のcgoツールは、GoプログラムからC言語のコードを呼び出す(またはその逆)ためのメカニズムを提供します。このツールは、Cの型定義をGoの型に変換する際に、コンパイラが生成するデバッグ情報(DWARF)を利用します。

問題は、Clangコンパイラがポインタ型のサイズ情報をDWARFエントリに記録しないという特定の挙動にありました。cgoは、Cの構造体や配列のサイズを正確に計算するために、これらの型情報に依存しています。

既存のcgoの実装では、ポインタ型自体のサイズ(例えば、64ビットシステムでは8バイト)は正しく挿入されていました。しかし、int8 *array3[20]; のような「ポインタの配列」の場合、cgoはまず配列全体のサイズを計算しようとします。この時、配列の要素であるポインタのサイズがまだ不明(ClangのDWARF情報にはないため)だと、ポインタのサイズが0として扱われ、結果として配列全体のサイズも0バイトと誤って計算されてしまっていました。

このバグは、FreeBSDのClang環境で特に顕在化しました。FreeBSDのClangは、C構造体フィールドとしてarray[0]のような不正なサイズを持つ配列を拒否する厳格なチェックを行うため、この誤ったサイズ計算がビルドエラーとして表面化したのです。しかし、コミットメッセージが示すように、この問題はFreeBSDに限定されず、Clangを使用する全てのシステムで潜在的に存在していました。既存のテストは、cgoが何らかの出力を生成することを確認するだけで、その出力のセマンティクス(特にサイズ情報)が正しいことを検証していなかったため、これまで見過ごされていました。

この修正の目的は、cgoがClang環境下でもポインタの配列のサイズを正確に計算できるようにし、クロスプラットフォームでのcgoの信頼性を向上させることです。

前提知識の解説

cgo

cgoはGo言語のツールチェーンの一部で、GoプログラムからC言語の関数を呼び出したり、C言語の型をGoの型として利用したりするためのものです。GoとCの間の相互運用性を提供し、既存のCライブラリをGoから利用する際に不可欠です。cgoは、Goのソースコード内の特別なコメント(import "C"ブロック)を解析し、Cのヘッダーファイルを読み込んで、GoとCの間でデータを変換するための接着コードを生成します。

DWARF (Debugging With Arbitrary Record Formats)

DWARFは、プログラムのソースコードとコンパイルされたバイナリコードの間のマッピングを記述するための標準的なデバッグ情報フォーマットです。コンパイラは、ソースコードをコンパイルする際に、変数名、型情報、関数名、行番号などのデバッグ情報をDWARF形式で生成し、実行可能ファイルに埋め込みます。デバッガや、今回のcgoのように型情報を必要とするツールは、このDWARF情報を解析して、プログラムの構造を理解します。

sizeofoffsetof

  • sizeof: C言語の演算子で、指定された型または変数のメモリ上のサイズ(バイト単位)を返します。例えば、sizeof(int)int型のサイズを返します。
  • offsetof: C言語のマクロで、構造体(struct)の先頭から、指定されたメンバーまでのオフセット(バイト単位)を返します。例えば、offsetof(MyStruct, myMember)MyStruct構造体内のmyMemberフィールドのオフセットを返します。 これらの演算子は、メモリレイアウトを理解し、ポインタ演算を行う上で非常に重要です。

ポインタ型と配列型

  • ポインタ型: メモリ上の特定のアドレスを指す変数の型です。例えば、int *p;int型へのポインタpを宣言します。ポインタ自体のサイズは、システムのアドレス空間のサイズ(32ビットシステムでは4バイト、64ビットシステムでは8バイト)に依存します。
  • 配列型: 同じ型の要素が連続してメモリに配置されたデータ構造です。例えば、int arr[10];は10個のint型要素を持つ配列を宣言します。配列のサイズは、要素の型サイズと要素数の積で決まります。
  • ポインタの配列: ポインタを要素とする配列です。例えば、int *arr_ptr[10];は10個のint型へのポインタを要素とする配列を宣言します。この場合、配列全体のサイズは、ポインタのサイズと要素数の積になります。

Clangの挙動

ClangはLLVMプロジェクトの一部であるC/C++/Objective-Cコンパイラです。コミットメッセージが指摘するように、Clangは特定の状況下でポインタ型のサイズ情報をDWARFエントリに明示的に記録しないことがあります。これは、ポインタのサイズがプラットフォームに依存し、コンパイル時に決定されるため、DWARFに固定値を記録するよりも、デバッガが実行時にポインタのサイズを決定する方が柔軟であるという設計思想によるものかもしれません。しかし、cgoのようなツールにとっては、この情報が欠落していると型変換の際に問題を引き起こす可能性があります。

技術的詳細

このバグは、src/cmd/cgo/gcc.go内のTypeメソッドのロジックに起因していました。このメソッドは、CのDWARF型情報をGoの型に変換する役割を担っています。

元のコードでは、Typeメソッドの冒頭でt.Size = dtype.Size()という行があり、ここでDWARF型から直接サイズを取得しようとしていました。しかし、Clangがポインタ型のサイズをDWARFに記録しないため、ポインタ型の場合、dtype.Size()は0を返していました。

単一のポインタ型(例: int8 *)の場合、cgoは後続のロジックでこの0バイトのサイズを検出し、正しいポインタサイズ(例: 8バイト)に修正していました。

問題は、int8 *array3[20];のような「ポインタの配列」の場合に発生しました。

  1. cgoはまず、配列型array3のDWARF情報を処理します。
  2. 配列のサイズを計算するために、cgoは要素の型(int8 *)のサイズを必要とします。
  3. この時点で、ネストされたポインタ型int8 *Typeメソッドが再帰的に呼び出されます。
  4. 再帰呼び出しされたTypeメソッドの冒頭で、t.Size = dtype.Size()が実行されますが、Clangの挙動によりdtype.Size()はポインタ型に対して0を返します。
  5. しかし、この0バイトのサイズが正しいポインタサイズに修正されるロジックは、Typeメソッドの後半に位置していました。
  6. そのため、配列型array3のサイズ計算が実行される時点では、要素であるポインタのサイズがまだ0のままであり、結果として配列全体のサイズも20 * 0 = 0と誤って計算されてしまっていたのです。

この修正は、このサイズ修正ロジックの実行タイミングを調整することで問題を解決しています。具体的には、t.Size <= 0の場合に再度dtype.Size()を呼び出すロジックを、Typeメソッドのより後方、つまりネストされたポインタ型のサイズが既に正しく設定された後に実行されるように移動しました。これにより、配列のサイズを計算する際には、要素であるポインタの正しいサイズが利用可能になり、配列全体のサイズも正確に計算されるようになります。

また、この修正には、misc/cgo/testcdefsディレクトリ内のテストケースの更新も含まれています。以前のテストは、cgoが何らかの出力を生成することしか確認していませんでしたが、新しいテストは、生成されたCの構造体定義のsizeofoffsetofが、元のCの定義と一致するかどうかを明示的にチェックすることで、セマンティクスが正しいことを検証しています。これにより、将来同様のサイズ計算のバグが導入されるのを防ぐための、より堅牢なテストカバレッジが提供されます。

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

変更は主に以下のファイルで行われています。

  1. src/cmd/cgo/gcc.go: Typeメソッドのロジック修正
  2. misc/cgo/testcdefs/main.c: 新しいテストケースの追加
  3. misc/cgo/testcdefs/main.go: テスト実行のためのmain関数追加
  4. misc/cgo/testcdefs/test.bash: テストスクリプトの更新

src/cmd/cgo/gcc.go の変更

--- a/src/cmd/cgo/gcc.go
+++ b/src/cmd/cgo/gcc.go
@@ -1046,21 +1046,11 @@ func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type {
 	}
 
 	t := new(Type)
-	t.Size = dtype.Size()
+	t.Size = dtype.Size() // note: wrong for array of pointers, corrected below
 	t.Align = -1
 	t.C = &TypeRepr{Repr: dtype.Common().Name}
 	c.m[dtype] = t
 
-	if t.Size < 0 {
-		// Unsized types are [0]byte
-		t.Size = 0
-		t.Go = c.Opaque(0)
-		if t.C.Empty() {
-			t.C.Set("void")
-		}
-		return t
-	}
-
 	switch dt := dtype.(type) {
 	default:
 		fatalf("%s: unexpected type: %s", lineno(pos), dtype)
@@ -1207,6 +1197,9 @@ func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type {
 		return t
 
 	case *dwarf.StructType:
+		if dt.ByteSize < 0 { // opaque struct
+			break
+		}
 		// Convert to Go struct, being careful about alignment.
 		// Have to give it a name to simulate C "struct foo" references.
 		tag := dt.StructName
@@ -1325,6 +1318,25 @@ func (c *typeConv) Type(dtype dwarf.Type, pos token.Pos) *Type {
 		}
 	}
 
+	if t.Size <= 0 {
+		// Clang does not record the size of a pointer in its DWARF entry,
+		// so if dtype is an array, the call to dtype.Size at the top of the function
+		// computed the size as the array length * 0 = 0.
+		// The type switch called Type (this function) recursively on the pointer
+		// entry, and the code near the top of the function updated the size to
+		// be correct, so calling dtype.Size again will produce the correct value.
+		t.Size = dtype.Size()
+		if t.Size < 0 {
+			// Unsized types are [0]byte
+			t.Size = 0
+			t.Go = c.Opaque(0)
+			if t.C.Empty() {
+				t.C.Set("void")
+			}
+			return t
+		}
+	}
+
 	if t.C.Empty() {
 		fatalf("%s: internal error: did not create C name for %s", lineno(pos), dtype)
 	}

misc/cgo/testcdefs/main.c の追加

このファイルは、cgoによって生成されたCの定義(CdefsTest構造体)と、元のCの定義(CdefsOrig構造体)のsizeofoffsetofを比較するテストコードを含んでいます。これにより、cgoが正しく型情報を変換しているかを確認します。

misc/cgo/testcdefs/main.go の変更

テストを実行するためのGoのエントリポイントが追加されました。

misc/cgo/testcdefs/test.bash の変更

テストスクリプトが更新され、go build . の後に ./testcdefs を実行して、main.cで定義されたテストが実行されるようになりました。

コアとなるコードの解説

src/cmd/cgo/gcc.goType メソッド

このメソッドは、CのDWARF型をGoの型に変換する中心的なロジックを含んでいます。

  1. 初期のサイズ計算の変更:

    -	t.Size = dtype.Size()
    +	t.Size = dtype.Size() // note: wrong for array of pointers, corrected below
    

    t.Size = dtype.Size()という行はそのまま残されていますが、コメントが追加され、ポインタの配列の場合にはこの初期計算が不正確である可能性が示唆されています。これは、ClangがポインタのサイズをDWARFに記録しないため、dtype.Size()が0を返すことがあるためです。

  2. 初期のサイズチェックとOpaque型への変換ロジックの削除: 元のコードでは、t.Size < 0(未定義サイズ)の場合にt.Sizeを0に設定し、Opaque型(Go側で不透明な型として扱う)に変換するロジックが、メソッドの早い段階にありました。このブロックが削除されました。これは、ポインタの配列のサイズ計算が遅延されるようになったため、この早期のチェックが不要になったか、あるいは誤ったタイミングで実行される可能性があったためと考えられます。

  3. dwarf.StructType の処理における変更:

    case *dwarf.StructType:
    	if dt.ByteSize < 0 { // opaque struct
    		break
    	}
    

    dwarf.StructTypeを処理する際に、dt.ByteSize < 0(不透明な構造体)の場合にbreakする条件が追加されました。これは、不透明な構造体(定義が完全でない構造体)のサイズ計算をスキップするためのものです。

  4. 遅延されたサイズ計算とOpaque型への変換ロジックの追加:

    	if t.Size <= 0 {
    		// Clang does not record the size of a pointer in its DWARF entry,
    		// so if dtype is an array, the call to dtype.Size at the top of the function
    		// computed the size as the array length * 0 = 0.
    		// The type switch called Type (this function) recursively on the pointer
    		// entry, and the code near the top of the function updated the size to
    		// be correct, so calling dtype.Size again will produce the correct value.
    		t.Size = dtype.Size()
    		if t.Size < 0 {
    			// Unsized types are [0]byte
    			t.Size = 0
    			t.Go = c.Opaque(0)
    			if t.C.Empty() {
    				t.C.Set("void")
    			}
    			return t
    		}
    	}
    

    これがこのコミットの最も重要な変更点です。Typeメソッドの最後に近い位置に、t.Size <= 0の場合に再度dtype.Size()を呼び出すロジックが追加されました。

    • t.Size <= 0 の条件: これは、初期のdtype.Size()の呼び出しでサイズが0(Clangのポインタ型の場合)または負の値(未定義サイズ)であった場合にトリガーされます。
    • 再度のdtype.Size()呼び出し: この時点で、もしdtypeがポインタの配列であり、その要素であるポインタ型が既に再帰的なType呼び出しによって正しくサイズが設定されている場合、dtype.Size()は配列の正しいサイズを返すようになります。これは、dtype.Size()が内部的に要素のサイズに依存しているためです。
    • Opaque型への変換ロジックの再配置: t.Size < 0の場合にt.Sizeを0に設定し、Opaque型に変換するロジックが、この新しいブロック内に移動されました。これにより、未定義サイズの型が適切に処理されることが保証されます。

この変更により、ポインタの配列のサイズ計算が、ネストされたポインタのサイズが確定した後に実行されるようになり、Clang環境下でのcgoの型変換の正確性が向上しました。

テストケースの変更 (misc/cgo/testcdefs/)

  • main.cでは、CdefsOrigという元のC構造体と、cgoが生成するCdefsTest構造体を定義し、それぞれのメンバーのsizeofoffsetofを比較しています。特に、int8 *array3[20];int8 **array5[20][20];のようなポインタの配列に対して、サイズとオフセットが一致するかを厳密にチェックしています。これにより、cgoが生成する型定義のセマンティクスが正しいことを検証しています。
  • main.goは、main.cで定義されたtest()関数を呼び出し、その戻り値(テスト結果)をGoのos.Exitに渡すシンプルなラッパーです。
  • test.bashは、go tool cgo -cdefsコマンドでCの定義を生成し、その後go build . && ./testcdefsでコンパイルとテストの実行を行うように変更されました。

これらのテストの追加により、cgoが生成するCの型定義が、元のCの型定義とメモリレイアウトに関して互換性があることが保証されるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (cgoに関する情報)
  • DWARF Debugging Information Format (DWARFの仕様に関する情報)
  • Clang Compiler User's Manual (Clangの挙動に関する情報)
  • C言語のsizeofoffsetof演算子に関する一般的な情報
  • ポインタと配列に関するC言語の基本的な概念