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

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

このコミットは、Go言語のcgoツールにおける、Goの文字列(string)とスライス(slice)の構造体定義において、C言語側で使用されるサイズおよび長さの型をintからintgoに変更するものです。これにより、異なるアーキテクチャ間でのGoとCの相互運用性における潜在的なバグ(特にポインタサイズと整数サイズの不一致に起因する問題)を修正します。具体的には、GoのstringsliceがC言語にエクスポートされる際に、その長さや容量を表すフィールドの型が、C側で正しく解釈されるように調整されます。

コミット

commit 8f7863d6382d92267bff313564d3e077d5e68ac3
Author: Ian Lance Taylor <iant@golang.org>
Date:   Thu May 23 22:51:07 2013 -0700

    cmd/cgo: use intgo, not int, for string and slice structures
    
    Fixes #5548.
    
    R=golang-dev, minux.ma
    CC=golang-dev
    https://golang.org/cl/9643044

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

https://github.com/golang/go/commit/8f7863d6382d92267bff313564d3e077d5e68ac3

元コミット内容

cmd/cgo: use intgo, not int, for string and slice structures

Fixes #5548.

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

変更の背景

この変更は、Go言語のIssue #5548を修正するために行われました。Issue #5548は、cgoを使用してGoの関数をCから呼び出す際に、Goのstring型やslice型が引数として渡されると、C側でそれらの長さや容量が正しく解釈されない可能性があるという問題でした。

具体的には、Goのstring型は内部的にポインタと長さ(len)のペアで、slice型はポインタ、長さ(len)、容量(cap)のトリプルで表現されます。これらの長さや容量のフィールドはGoのint型ですが、C言語のint型はシステムによってサイズが異なる場合があります(例: 32ビットシステムでは4バイト、64ビットシステムでは4バイトまたは8バイト)。

問題は、C側でGoのstringslice構造体を受け取る際に、CのintがGoのintと同じサイズであると仮定してしまうことにありました。特に、64ビットシステムでCのintが32ビットである場合、Goのint(通常64ビット)で表現された長さや容量がC側で切り詰められたり、誤ったメモリレイアウトでアクセスされたりする可能性がありました。これにより、GoからCへのコールバックで文字列やスライスの情報が破損し、クラッシュや不正な動作を引き起こす可能性がありました。

このコミットは、cgoが生成するCのコードにおいて、Goのintに対応する型として、ポインタサイズと同じサイズの整数型であるintgoを使用することで、この問題を解決します。

前提知識の解説

cgo

cgoは、GoプログラムからC言語のコードを呼び出したり、C言語のコードからGoの関数を呼び出したりするためのGoのツールです。GoとCの間のデータ型の変換や関数呼び出しのメカニズムを提供し、異なる言語間の相互運用性を可能にします。cgoは、Goのソースコード内にCのコードを直接記述したり、既存のCライブラリをリンクしたりするために使用されます。

Goの文字列(string)とスライス(slice)の内部表現

Goのstring型は、読み取り専用のバイト列へのポインタと、そのバイト列の長さ(バイト数)のペアとして内部的に表現されます。 例: type stringStruct struct { ptr *byte; len int }

Goのslice型は、基底配列へのポインタ、スライスの長さ(len)、そしてスライスの容量(cap)のトリプルとして内部的に表現されます。 例: type sliceStruct struct { array unsafe.Pointer; len int; cap int }

ここで、lencapはGoの組み込み型intで表現されます。Goのint型は、実行環境のCPUアーキテクチャに依存し、32ビットシステムでは32ビット、64ビットシステムでは64ビットの整数型となります。

C言語の整数型と__PTRDIFF_TYPE__

C言語のint型は、そのサイズが標準で保証されていません。通常、16ビット、32ビット、または64ビットのいずれかですが、具体的なサイズはコンパイラやターゲットアーキテクチャに依存します。 一方、__PTRDIFF_TYPE__はGCCなどのコンパイラで定義されるマクロで、2つのポインタの差を保持できる符号付き整数型を表します。これは通常、ポインタと同じサイズ(32ビットシステムでは32ビット、64ビットシステムでは64ビット)になります。この特性から、ポインタサイズに依存するGoのint型とC言語側で安全にやり取りするために適しています。

intgoの導入

このコミットで導入されるintgoは、cgoがGoとCの間でデータをやり取りする際に、Goのint型に対応するCの型として定義されます。intgoは、__PTRDIFF_TYPE__が存在すればそれを使用し、そうでなければlong long(64ビットシステムの場合)またはint(32ビットシステムの場合)を使用するように定義されます。これにより、Goのintが32ビットでも64ビットでも、C側で常に適切なサイズの整数型として扱われるようになります。

技術的詳細

このコミットの核心は、cgoがGoのstringsliceをC言語の構造体として表現する際に、その長さや容量のフィールドの型をintからintgoに変更することです。

変更前は、cgoはGoのstringをCの_GoString_構造体(char *p; int n;)として、GoのsliceをCの_GoBytes_構造体(char *p; int n; int c;)として定義していました。ここで、n(長さ)やc(容量)のフィールドはCのint型でした。

問題は、64ビットシステムにおいてGoのintが64ビットであるのに対し、Cのintが32ビットである場合に発生しました。この場合、GoからCへstringsliceが渡されると、Goの64ビットの長さ/容量がCの32ビットのintにコピーされる際に上位ビットが切り捨てられ、C側で不正な長さや容量として解釈される可能性がありました。

このコミットでは、src/cmd/cgo/out.go内のbuiltinProlog定数に、intgoの定義が追加されました。

/* Define intgo when compiling with GCC.  */
#ifdef __PTRDIFF_TYPE__
typedef __PTRDIFF_TYPE__ intgo;
#elif defined(_LP64)
typedef long long intgo;
#else
typedef int intgo;
#endif

この定義により、intgoは以下のように決定されます。

  1. __PTRDIFF_TYPE__が定義されていれば、それを使用します。これは通常、ポインタと同じサイズ(64ビットシステムでは64ビット)になります。
  2. _LP64(64ビットシステムを示すマクロ)が定義されていれば、long long(通常64ビット)を使用します。
  3. それ以外の場合(主に32ビットシステム)、intを使用します。

このintgo型が、_GoString__GoBytes_構造体のnおよびcフィールドの型として使用されるようになりました。

typedef struct { char *p; intgo n; } _GoString_;
typedef struct { char *p; intgo n; intgo c; } _GoBytes_;

さらに、GoStringNGoBytesといったGoとCの間で文字列やバイト列を変換するヘルパー関数の引数型もint32からintgoに変更されました。

これにより、Goのint型が32ビットであろうと64ビットであろうと、C側でそのサイズに合わせたintgo型で長さや容量が表現されるため、データの切り捨てや誤った解釈を防ぐことができます。

また、src/cmd/cgo/out.gocgoType関数内のGoのstringsliceのサイズ計算も調整されました。

  • stringのサイズは、以前は「1ポインタ + 1 int」で「アライメントにより常に2ポインタに丸められる」とコメントされていましたが、新しいコメントでは「1ポインタ + 1 (ポインタサイズの) int」と明確化され、2 * p.PtrSizeという計算は維持されています。これは、intgoがポインタサイズと同じになるため、GoのstringのメモリレイアウトがC側で正しく解釈されることを保証します。
  • sliceのサイズは、以前はp.PtrSize + 8(ポインタ + 8バイト)とされていましたが、p.PtrSize * 3(ポインタ + 2つのポインタサイズの整数)に変更されました。これは、スライスがポインタ、長さ、容量の3つのポインタサイズの要素で構成されることを正確に反映しています。

これらの変更により、GoとCの間で文字列やスライスが受け渡される際のメモリレイアウトと型の整合性が保証され、Issue #5548で報告されたような問題が解決されます。

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

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

  1. src/cmd/cgo/out.go: cgoツールがCのヘッダーやスタブコードを生成する際のロジックが含まれています。

    • builtinProlog定数にintgo型の定義が追加されました。
    • _GoString_および_GoBytes_構造体のn(長さ)およびc(容量)フィールドの型がintからintgoに変更されました。
    • _Cfunc_GoStringNおよび_Cfunc_GoBytes関数の引数型がint32からintgoに変更されました。
    • GoのstringslicecgoTypeにおけるサイズ計算のコメントと値が更新されました。
    • GoStringGoSliceのC側のtypedef定義で、nlen/capの型がintからGoIntに変更されました。
  2. misc/cgo/test/cgo_test.go: cgoのテストスイートのメインファイル。

    • Test5548という新しいテスト関数が追加されました。
  3. misc/cgo/test/issue5548.go: Issue #5548を再現し、修正を検証するためのGoのテストファイル。

    • issue5548FromCというGo関数が定義されており、Cから呼び出されることを想定しています。この関数はstringintを引数にとり、それらの値が期待通りであるかを検証します。
    • test5548関数がCの関数issue5548_in_cを呼び出し、結果を検証します。
  4. misc/cgo/test/issue5548_c.c: Issue #5548を再現し、修正を検証するためのCのテストファイル。

    • issue5548_in_cというC関数が定義されており、Goの関数issue5548FromCを呼び出します。
    • call_go関数内で、GoのGoString構造体をC側で手動で構築し、Go関数に渡しています。ここでGoStringnフィールドにGoのintに対応する値が設定されます。
    • clobber_stack関数は、スタックを破壊することで、メモリレイアウトの不整合が顕在化しやすい状況を作り出しています。

コアとなるコードの解説

src/cmd/cgo/out.go

このファイルはcgoのバックエンドの一部であり、Goの型とCの型の間で相互運用するためのCコードを生成する役割を担っています。

  • builtinPrologintgo定義:

    const builtinProlog = `
    /* Define intgo when compiling with GCC.  */
    #ifdef __PTRDIFF_TYPE__
    typedef __PTRDIFF_TYPE__ intgo;
    #elif defined(_LP64)
    typedef long long intgo;
    #else
    typedef int intgo;
    #endif
    // ...
    typedef struct { char *p; intgo n; } _GoString_;
    typedef struct { char *p; intgo n; intgo c; } _GoBytes_;
    // ...
    _GoString_ GoStringN(char *p, intgo l);
    _GoBytes_ GoBytes(void *p, intgo n);
    `
    

    この部分が、Goのint型に対応するC側の型intgoを定義しています。__PTRDIFF_TYPE__はポインタの差を表現できる型であり、通常ポインタと同じサイズです。これにより、Goのintが64ビットの場合でも、C側で正しく64ビットの整数として扱われるようになります。_GoString__GoBytes_構造体のncフィールドがintgoに変更されたことで、Goの文字列やスライスの長さ・容量がC側で正しく解釈されるようになります。また、関連するヘルパー関数GoStringNGoBytesの引数型もintgoに変更されています。

  • cgoType関数内のサイズ計算の修正:

    case *ast.ArrayType:
        if t.Len == nil {
            // Slice: pointer, len, cap.
            return &Type{Size: p.PtrSize * 3, Align: p.PtrSize, C: c("GoSlice")}
        }
    // ...
    if t.Name == "string" {
        // The string data is 1 pointer + 1 (pointer-sized) int.
        return &Type{Size: 2 * p.PtrSize, Align: p.PtrSize, C: c("GoString")}
    }
    

    Goのslice型はポインタ、長さ、容量の3つの要素で構成されるため、そのサイズはp.PtrSize * 3と正確に計算されるようになりました。string型についても、ポインタとポインタサイズの整数(intgo)で構成されることがコメントで明確化され、サイズ計算2 * p.PtrSizeがその表現と一致することが示されています。これにより、cgoがGoの型をCの型にマッピングする際のメモリレイアウトの整合性が向上します。

  • _Cfunc_GoStringN_Cfunc_GoBytesの引数型変更:

    void
    ·_Cfunc_GoStringN(int8 *p, intgo l, String s)
    // ...
    void
    ·_Cfunc_GoBytes(int8 *p, intgo l, Slice s)
    

    これらの関数は、CからGoの文字列やスライスを生成する際に使用される内部的なヘルパー関数です。引数l(長さ)の型がint32からintgoに変更されたことで、Goのintの実際のサイズ(32ビットまたは64ビット)に関わらず、C側で正しく長さが渡されるようになります。

misc/cgo/test/issue5548.gomisc/cgo/test/issue5548_c.c

これらのファイルは、Issue #5548で報告された問題を再現し、修正が正しく機能するかを検証するためのテストケースです。

  • issue5548.go:

    //export issue5548FromC
    func issue5548FromC(s string, i int) int {
        if len(s) == 4 && s == "test" && i == 42 {
            return 1
        }
        return 0
    }
    

    このGo関数issue5548FromCは、Cから呼び出されることを想定しています。string型の引数sint型の引数iを受け取り、それらの値が期待通りであるか(文字列が"test"で長さが4、整数が42)を検証します。もしcgoが文字列の長さを正しく扱えない場合、len(s)が期待値と異なる結果となり、テストが失敗します。

  • issue5548_c.c:

    static int call_go() {
        GoString s;
        s.p = "test";
        s.n = 4; // ここでGoStringの長さを設定
        return issue5548FromC(s, 42);
    }
    
    int issue5548_in_c() {
        clobber_stack(); // スタックを破壊して問題が顕在化しやすい状況を作る
        return call_go();
    }
    

    このCコードでは、GoのGoString構造体をC側で手動で構築し、Go関数issue5548FromCに渡しています。特にs.n = 4;の部分で、C側でGoの文字列の長さを設定しています。変更前は、このnフィールドがCのint型として定義されていたため、64ビットシステムでGoのintが64ビットである場合に、Cのintが32ビットだと、Go関数が受け取る長さが不正になる可能性がありました。clobber_stack()関数は、スタックを意図的に破壊することで、メモリレイアウトの不整合がより顕著に現れるようにしています。このテストケースは、intgoの導入によって、CからGoへ文字列が渡される際の長さ情報が正しく伝達されることを確認します。

これらの変更とテストケースにより、cgoがGoの文字列やスライスをCと相互運用する際の型の整合性が大幅に向上し、異なるアーキテクチャ間での潜在的なバグが修正されました。

関連リンク

参考にした情報源リンク