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

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

このコミットは、Go言語のcmd/cgoツールにおいて、C言語のmalloc関数の呼び出し方法を改善するためのものです。具体的には、C.mallocを直接使用する代わりに、Goランタイムが提供する独自のラッパー関数を介してmallocを呼び出すように変更しています。これにより、mallocの引数型の互換性問題(特にWindows環境でのsize_tulongの違い)を解決し、mallocがメモリ確保に失敗した場合にプログラムを安全に終了させる(パニックではなくruntime.throwを呼び出す)メカニズムを導入しています。

コミット

commit 397ba2cb4a10ec5e383f1df7617b4b8bccf8dfab
Author: Russ Cox <rsc@golang.org>
Date:   Wed Sep 11 11:30:08 2013 -0400

    cmd/cgo: replace C.malloc with our own wrapper
    
    This allows us to make two changes:
    
    1. Force the argument type to be size_t, even on broken
       systems that declare malloc to take a ulong.
    
    2. Call runtime.throw if malloc fails.
       (That is, the program crashes; it does not panic.)
    
    Fixes #3403.
    Fixes #5926.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/13413047

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

https://github.com/golang/go/commit/397ba2cb4a10ec5e383f1df7617b4b8bccf8dfab

元コミット内容

cmd/cgo: replace C.malloc with our own wrapper

この変更により、以下の2つの変更が可能になります。

  1. mallocの引数型をsize_tに強制します。これは、malloculongを引数として宣言されているような問題のあるシステムでも適用されます。
  2. mallocが失敗した場合にruntime.throwを呼び出します。(つまり、プログラムはクラッシュし、パニックは発生しません。)

Fixes #3403. Fixes #5926.

変更の背景

このコミットは、Go言語のCgoツールにおける2つの主要な問題を解決するために導入されました。

  1. mallocの引数型に関するプラットフォーム間の非互換性(Issue #3403): C標準ではmallocの引数型はsize_tと定義されています。しかし、一部のシステム(特に当時のWindows環境)では、mallocunsigned longを引数として宣言されていることがありました。GoのCgoは、Cの関数をGoから呼び出す際に、Cの型定義に基づいてGoの型を生成します。この型定義の不一致が原因で、GoからC.mallocを呼び出す際に型変換の問題や、場合によっては不正なメモリ割り当てが発生する可能性がありました。このコミットは、Go側でmallocを呼び出す際に、常にsize_tとして扱うように強制することで、このプラットフォーム間の差異を吸収しようとしています。

  2. malloc失敗時の挙動の改善(Issue #5926): C言語のmalloc関数は、メモリ割り当てに失敗した場合にNULLを返します。GoのプログラムがCgoを通じてmallocを呼び出し、それがNULLを返した場合、Go側でそのNULLを適切に処理しないと、後続のメモリアクセスでセグメンテーション違反などのクラッシュを引き起こす可能性がありました。このコミット以前は、C.mallocが失敗してもGoランタイムがそれを検知して安全に終了させるメカニズムがありませんでした。この変更は、mallocが失敗した際にGoランタイムのruntime.throw関数を呼び出すことで、プログラムを即座に、かつ制御された形で終了させるようにします。これにより、未定義動作や予測不能なクラッシュを防ぎ、デバッグを容易にします。runtime.throwはGoのパニックとは異なり、回復不能なエラーを示すもので、スタックトレースを出力してプログラムを終了させます。

これらの問題は、GoプログラムがCライブラリと連携する際の堅牢性と移植性を向上させる上で重要でした。

前提知識の解説

  • Cgo: Cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoのツールです。Goのソースファイル内にimport "C"という特殊なインポート文を記述し、その直後にCのコードをコメント形式で記述することで、Cの関数や型をGoから利用できるようになります。Cgoは、GoとCの間のデータ型変換や呼び出し規約の調整を自動的に行いますが、時にはプラットフォーム固有の差異やCの標準ライブラリ関数の挙動に関する深い理解が必要となります。

  • malloc関数: mallocはC標準ライブラリ(stdlib.h)で提供されるメモリ割り当て関数です。指定されたサイズのメモリブロックをヒープから割り当て、そのブロックへのポインタを返します。メモリ割り当てに失敗した場合はNULLを返します。

  • size_tunsigned long:

    • size_t: C言語でオブジェクトのサイズや配列のインデックスを表すために使用される符号なし整数型です。sizeof演算子の結果の型であり、システムによってその具体的なサイズ(ビット幅)は異なりますが、常に十分な大きさを持つことが保証されています。mallocの引数型として標準で推奨されています。
    • unsigned long: 符号なし長整数型です。そのサイズはシステムによって異なりますが、通常は32ビットまたは64ビットです。一部の古いシステムや特定のコンパイラ環境では、mallocの引数がunsigned longとして宣言されていることがありました。この型はsize_tと互換性がない場合があり、特に異なるビット幅を持つシステム間で問題を引き起こす可能性があります。
  • runtime.throwpanic: Go言語には、プログラムの異常終了を扱うための2つの主要なメカニズムがあります。

    • panic: Goの組み込み関数で、回復可能なエラーや予期せぬ状況を示すために使用されます。panicが発生すると、現在のゴルーチンの実行が停止し、遅延関数(defer)が実行され、スタックがアンワインドされます。recover関数を使ってpanicを捕捉し、プログラムの実行を継続することも可能です。
    • runtime.throw: Goランタイム内部で使用される関数で、回復不能な致命的なエラーが発生した場合に呼び出されます。panicとは異なり、runtime.throwdefer関数を実行せず、recoverで捕捉することもできません。これは通常、ランタイムの内部的な整合性が損なわれた場合や、Cgo呼び出しで致命的なエラーが発生した場合など、プログラムが安全に続行できない状況で呼び出され、スタックトレースを出力してプログラムを即座に終了させます。このコミットでは、mallocの失敗をこのような回復不能なエラーとして扱っています。
  • CgoのProlog: Cgoは、GoのコードとCのコードを結合する際に、Goのソースファイルに記述されたCコードや、Cgoが内部的に必要とする定義(ヘルパー関数、型定義など)をまとめたCのコードブロックを生成します。このコードブロックは「Prolog」と呼ばれ、最終的にコンパイルされるCファイルの一部となります。このコミットでは、mallocのラッパー関数をこのPrologに追加しています。

技術的詳細

このコミットの技術的詳細は、CgoがCのmallocをどのように扱うかを根本的に変更することにあります。

  1. C.mallocの内部的なリライト: src/cmd/cgo/ast.goの変更により、Goのコード内でC.mallocが参照された場合、Cgoはそれを内部的にC._CMallocという名前にリライトします。これは、Go開発者が引き続きC.mallocという慣用的な名前を使用できるようにしつつ、Cgoの内部処理で特別なラッパー関数を呼び出すためのメカニズムです。エラーメッセージ表示の際には、_CMallocが再びmallocに変換されるようにfixGo関数が導入されています。

  2. _CMallocラッパー関数の導入: src/cmd/cgo/out.goでは、Cgoが生成するCコードのProlog部分に_CMallocという新しいC関数が定義されています。この関数は、GoからC.mallocが呼び出された際に実際に実行されるC側のラッパーです。

    void *_CMalloc(size_t n); // builtinPrologに追加
    

    そして、その実装は以下のようになります。

    void
    ·_Cfunc__CMalloc(uintptr n, int8 *p)
    {
    	p = runtime·cmalloc(n);
    	FLUSH(&p);
    }
    

    この_Cfunc__CMallocは、Goランタイムの内部関数であるruntime·cmallocを呼び出します。

  3. runtime·cmallocの強化: src/pkg/runtime/cgocall.cにあるruntime·cmalloc関数は、GoランタイムがCのmallocを呼び出すための内部的なヘルパー関数です。このコミットでは、runtime·cmalloc_cgo_malloc(これは最終的にCのmallocを呼び出す)を呼び出した後、返されたポインタがnil(CのNULLに相当)であるかどうかをチェックするロジックが追加されました。

    runtime·cmalloc(uintptr n)
    {
    	struct {
    		uintptr n;
    		void *ret;
    	} a;
    	a.n = n;
    	a.ret = nil;
    	runtime·cgocall(_cgo_malloc, &a);
    	if(a.ret == nil) // ここが追加されたチェック
    		runtime·throw("runtime: C malloc failed"); // 失敗時にruntime·throwを呼び出す
    	return a.ret;
    }
    

    もし_cgo_mallocNULLを返した場合、runtime·throw("runtime: C malloc failed")が呼び出され、プログラムは致命的なエラーとして終了します。これにより、mallocの失敗がGoプログラム内で適切に処理され、未定義動作を防ぎます。

  4. Cmallocラッパーの追加(GCCGO向け): src/cmd/cgo/out.goのGCCGO(GoコンパイラのGCCフロントエンド)向けのPrologには、Cmallocという別のラッパー関数が追加されています。これは、GCCGOのコンテキストでmallocの失敗を処理するためのものです。

    extern void runtime_throw(const char *);
    void *Cmalloc(size_t n) {
            void *p = malloc(n);
            if(p == NULL)
                    runtime_throw("runtime: C malloc failed");
            return p;
    }
    

    このCmallocは、直接Cのmallocを呼び出し、NULLが返された場合にruntime_throwを呼び出します。これは、Goの標準コンパイラ(gc)とは異なるGCCGOのコンパイルパスにおけるmalloc失敗時の安全性を確保するためのものです。

これらの変更により、GoのCgoはmallocの呼び出しをより堅牢にし、プラットフォーム間の差異を吸収し、メモリ割り当て失敗時の挙動を予測可能にしています。

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

このコミットにおける主要なコード変更は以下のファイルに集中しています。

  1. src/cmd/cgo/ast.go:

    • File.saveRef関数内で、C.mallocへの参照が_CMallocに内部的に書き換えられるロジックが追加されました。
    --- a/src/cmd/cgo/ast.go
    +++ b/src/cmd/cgo/ast.go
    @@ -187,6 +187,13 @@ func (f *File) saveRef(x interface{}, context string) {
     			error_(sel.Pos(), "cannot refer to errno directly; see documentation")
     			return
     		}
    +		if goname == "_CMalloc" {
    +			error_(sel.Pos(), "cannot refer to C._CMalloc; use C.malloc")
    +			return
    +		}
    +		if goname == "malloc" {
    +			goname = "_CMalloc"
    +		}
     		name := f.Name[goname]
     		if name == nil {
     			name = &Name{
    
  2. src/cmd/cgo/out.go:

    • Cgoが生成するCコードのPrologに_CMallocが組み込み関数として追加されました。
    • エラーメッセージで_CMallocmallocに戻すためのfixGo関数が追加されました。
    • isBuiltinマップに_Cfunc__CMallocが追加され、重複定義を防ぐようになりました。
    • builtinProlog#include <sys/types.h>void *_CMalloc(size_t);が追加されました。
    • cProlog_Cfunc__CMallocの実装が追加されました。
    • GCCGO向けのcPrologGccgoCmallocラッパー関数が追加されました。
    --- a/src/cmd/cgo/out.go
    +++ b/src/cmd/cgo/out.go
    @@ -331,7 +331,7 @@ func (p *Package) writeDefsFunc(fc, fgo2 *os.File, n *Name) {
     	}
     
     	// Builtins defined in the C prolog.
    -	inProlog := name == "CString" || name == "GoString" || name == "GoStringN" || name == "GoBytes"
    +	inProlog := name == "CString" || name == "GoString" || name == "GoStringN" || name == "GoBytes" || name == "_CMalloc"
     
     	if *gccgo {
     		// Gccgo style hooks.
    @@ -476,9 +476,27 @@ func (p *Package) writeOutput(f *File, srcfile string) {
     	fgcc.Close()\n
     }\n
     
    +// fixGo convers the internal Name.Go field into the name we should show
    +// to users in error messages. There's only one for now: on input we rewrite
    +// C.malloc into C._CMalloc, so change it back here.
    +func fixGo(name string) string {
    +\tif name == "_CMalloc" {
    +\t\treturn "malloc"
    +\t}
    +\treturn name
    +}\n+\n+var isBuiltin = map[string]bool{
    +\t"_Cfunc_CString":   true,\n
    +\t"_Cfunc_GoString":  true,\n
    +\t"_Cfunc_GoStringN": true,\n
    +\t"_Cfunc_GoBytes":   true,\n
    +\t"_Cfunc__CMalloc":  true,\n
    +}\n+\n func (p *Package) writeOutputFunc(fgcc *os.File, n *Name) {
     	name := n.Mangle
    -	if name == "_Cfunc_CString" || name == "_Cfunc_GoString" || name == "_Cfunc_GoStringN" || name == "_Cfunc_GoBytes" || p.Written[name] {
    +	if isBuiltin[name] || p.Written[name] {
     		// The builtins are already defined in the C prolog, and we don't
     		// want to duplicate function definitions we've already done.\n
     		return
    @@ -1101,6 +1119,8 @@ __cgo_size_assert(double, 8)\n `\n \n const builtinProlog = `\n+#include <sys/types.h> /* for size_t below */\n+\n /* Define intgo when compiling with GCC.  */\n #ifdef __PTRDIFF_TYPE__\n typedef __PTRDIFF_TYPE__ intgo;\n@@ -1116,6 +1136,7 @@ _GoString_ GoString(char *p);\n _GoString_ GoStringN(char *p, int l);\n _GoBytes_ GoBytes(void *p, int n);\n char *CString(_GoString_);\n+void *_CMalloc(size_t);\n `\n \n const cProlog = `\n@@ -1153,6 +1174,13 @@ void\n \tp[s.len] = 0;\n \tFLUSH(&p);\n }\n+\n+void\n+·_Cfunc__CMalloc(uintptr n, int8 *p)\n+{\n+\tp = runtime·cmalloc(n);\n+\tFLUSH(&p);\n+}\n `\n \n const cPrologGccgo = `\n@@ -1193,6 +1221,14 @@ Slice GoBytes(char *p, int32_t n) {\n \tstruct __go_string s = { (const unsigned char *)p, n };\n \treturn __go_string_to_byte_array(s);\n }\n+\n+extern void runtime_throw(const char *):\n+void *Cmalloc(size_t n) {\n+        void *p = malloc(n);\n+        if(p == NULL)\n+                runtime_throw("runtime: C malloc failed");\n+        return p;\n+}\n `\n \n func (p *Package) gccExportHeaderProlog() string {
    
  3. src/pkg/runtime/cgocall.c:

    • runtime·cmalloc関数内で、_cgo_mallocの戻り値がnilNULL)である場合にruntime·throwを呼び出すロジックが追加されました。
    --- a/src/pkg/runtime/cgocall.c
    +++ b/src/pkg/runtime/cgocall.c
    @@ -198,6 +198,8 @@ runtime·cmalloc(uintptr n)\n \ta.n = n;\n \ta.ret = nil;\n \truntime·cgocall(_cgo_malloc, &a);\n+\tif(a.ret == nil)\n+\t\truntime·throw("runtime: C malloc failed");\n \treturn a.ret;\n }\n \n    ```
    
    

コアとなるコードの解説

このコミットの核心は、GoのCgoがC.mallocを扱う方法を、透過的なラッパーを通じて変更することにあります。

  1. src/cmd/cgo/ast.goにおけるC.mallocのリライト: GoのソースコードでC.mallocが記述されている場合、CgoのAST(抽象構文木)処理段階で、その参照が内部的に_CMallocという名前に変更されます。これは、Go開発者にはC.mallocという馴染みのあるインターフェースを提供しつつ、Cgoの内部で特別な処理をフックするための巧妙な方法です。これにより、Cgoはmallocの呼び出しを制御し、カスタムの挙動を注入できるようになります。

  2. src/cmd/cgo/out.goにおける_CMallocの定義とfixGo: out.goはCgoが最終的に生成するCコードの構造を定義する部分です。

    • builtinPrologvoid *_CMalloc(size_t);が追加されることで、Cgoが生成するCファイル内で_CMallocがCの関数として宣言されます。これにより、Goから呼び出されるC._CMallocがCの関数としてリンクできるようになります。
    • cPrologに追加された·_Cfunc__CMallocの実装は、Goから_CMallocが呼び出された際に実行される実際のCコードです。この関数は、Goランタイムの内部関数であるruntime·cmallocを呼び出します。FLUSH(&p)は、GoとCの間でポインタの受け渡しを行う際のメモリバリアのような役割を果たす可能性があります。
    • fixGo関数は、Cgoが生成するエラーメッセージにおいて、内部的な_CMallocという名前をユーザーが理解しやすいmallocに戻すために使用されます。これにより、ユーザーはC.mallocに関するエラーメッセージを受け取ることになり、内部的な実装の詳細に惑わされることがありません。
  3. src/pkg/runtime/cgocall.cにおけるruntime·cmallocの失敗処理: runtime·cmallocは、GoランタイムがCのmallocを呼び出すための低レベルなインターフェースです。この関数は、Cgoの内部関数_cgo_mallocを呼び出して実際のメモリ割り当てを行います。このコミットの最も重要な変更点の一つは、_cgo_mallocNULLを返した場合(つまり、メモリ割り当てに失敗した場合)に、runtime·throw("runtime: C malloc failed")を呼び出すようにしたことです。

    • runtime·throwは、Goランタイムが回復不能なエラーを検出した際に使用するメカニズムです。これはGoのpanicとは異なり、deferの実行やrecoverによる捕捉は行われず、プログラムは即座に終了します。これにより、mallocの失敗という致命的な状況が、Goプログラム内で安全かつ予測可能な形で処理されるようになります。以前は、NULLポインタがGoに渡され、その後のアクセスでセグメンテーション違反などのクラッシュを引き起こす可能性がありました。この変更により、そのような未定義動作が回避されます。
    • また、size_tへの引数型の強制も、このruntime·cmallocの呼び出しパスを通じて行われます。Go側から渡されるサイズ引数は、Cgoによって適切にsize_tとして扱われるようになります。

これらの変更は、GoとCの間のメモリ管理の境界をより明確にし、Cgoを使用するGoプログラムの堅牢性と信頼性を大幅に向上させています。

関連リンク

参考にした情報源リンク